testing web apis
DESCRIPTION
Jakob Mattsson «Testing web APIs» Frontend Dev Conf'14 www.fdconf.byTRANSCRIPT
jakobm.com
@jakobmattsson
I’m a coder first and foremost. I also help companies recruit coders, train coders and architect software. Sometimes I do technical due diligence and speak at conferences. !
Want more? Read my story or blog.
Social coding
FishBrain is the fastest, easiest way to log and share your fishing.!Get instant updates from anglers on nearby lakes, rivers, and the coast.
Testing web APIsSimple and readable
Example - Testing a blog API
• Create a blog • Create two blog entries • Create two users • Create a lotal of three comments from
those users, on the two posts • Request the stats for the blog and
check if the given number of entries and comments are correct
post('/blogs', { name: 'My blog' }, function(err, blog) { ! post(concatUrl('blogs', blog.id, 'entries'), { title: 'my first post’, body: 'Here is the text of my first post' }, function(err, entry1) { ! post(concatUrl('blogs', blog.id, 'entries'), { title: 'my second post’, body: 'I do not know what to write any more...' }, function(err, entry2) { ! post('/user', { name: 'john doe’ }, function(err, visitor1) { ! post('/user', { name: 'jane doe' }, function(err, visitor2) { ! post('/comments', { userId: visitor1.id, entryId: entry1.id, text: "well written dude" }, function(err, comment1) { ! post('/comments', { userId: visitor2.id, entryId: entry1.id, text: "like it!" }, function(err, comment2) { ! post('/comments', { userId: visitor2.id, entryId: entry2.id, text: "nah, crap" }, function(err, comment3) { ! get(concatUrl('blogs', blog.id), function(err, blogInfo) { assertEquals(blogInfo, { name: 'My blog', numberOfEntries: 2, numberOfComments: 3 }); }); }); }); }); }); }); }); }); });
*No, this is not a tutorial
Promises 101*
getJSON({ url: '/somewhere/over/the/rainbow', success: function(result) { // deal with it } });
getJSON({ url: '/somewhere/over/the/rainbow', success: function(result) { // deal with it } });
rainbows.then(function(result) { // deal with it });
var rainbows = getJSON({ url: '/somewhere/over/the/rainbow' });
getJSON({ url: '/somewhere/over/the/rainbow', success: function(result) { // deal with it } });
rainbows.then(function(result) { // deal with it });
var rainbows = getJSON({ url: '/somewhere/over/the/rainbow' });f(rainbows);
Why is that a good idea?
• Loosen up the coupling • Superior error handling • Simplified async
Why is that a good idea?
• Loosen up the coupling • Superior error handling • Simplified async
But in particular, it abstracts away the temporal dependencies in your program (or in this case, test)
That’s enough 101(even though people barely talk about
the last - and most important - idea)
Promises are not new
Promises are not newhttp://gi
thub.com/kriskowal
/q
http://www.html5rocks.com/en/t
utorials/es6/promises
http://domenic.me/2012/10/14/youre-missing-the-point-of-promises
http://www.promisejs.org
https://github.com/bellbind/using-promise-q
https://github.com
/tildeio/rsvp.js
https://github.com/cujojs/when
They’ve even made it into ES6
They’ve even made it into ES6
Already implemented natively in
Firefox 30Chrome 33
So why are you not using them?
So why are you not using them?
How to draw an owl
1. Draw some circles
How to draw an owl
1. Draw some circles
2. Draw the rest of the owl
How to draw an owl
We want to draw owls.
!
Not circles.
getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); ! // Map our array of chapter urls to // an array of chapter json promises. // This makes sture they all download parallel. return story.chapterUrls.map(getJSON) .reduce(function(sequence, chapterPromise) { // Use reduce to chain the promises together, // adding content to the page for each chapter return sequence.then(function() { // Wait for everything in the sequence so far, // then wait for this chapter to arrive. return chapterPromise; }).then(function(chapter) { addHtmlToPage(chapter.html); }); }, Promise.resolve()); }).then(function() { addTextToPage('All done'); }).catch(function(err) { // catch any error that happened along the way addTextToPage("Argh, broken: " + err.message); }).then(function() { document.querySelector('.spinner').style.display = 'none'; });
As announced for ES6
That’s circles. !
Nice circles! !
Still circles.
browser.init({browserName:'chrome'}, function() { browser.get("http://admc.io/wd/test-pages/guinea-pig.html", function() { browser.title(function(err, title) { title.should.include('WD'); browser.elementById('i am a link', function(err, el) { browser.clickElement(el, function() { browser.eval("window.location.href", function(err, href) { href.should.include('guinea-pig2'); browser.quit(); }); }); }); }); }); });
Node.js WebDriver Before promises
browser .init({ browserName: 'chrome' }) .then(function() { return browser.get("http://admc.io/wd/test-pages/guinea-pig.html"); }) .then(function() { return browser.title(); }) .then(function(title) { title.should.include('WD'); return browser.elementById('i am a link'); }) .then(function(el) { return browser.clickElement(el); }) .then(function() { return browser.eval("window.location.href"); }) .then(function(href) { href.should.include('guinea-pig2'); }) .fin(function() { return browser.quit(); }) .done();
After promises
Used to be callback-hell.
!
Now it is then-hell.
These examples are just a different way
of doing async. !
It’s still uncomfortable. It’s still circles!
The point of promises:
!
Make async code as straightforward
as sync code
The point of promises:
1 Promises out: Always return promises - not callback
2 Promises in: Functions should accept promises as well as regular values
3 Promises between: Augment promises as you augment regular objects
Three requirements
Let me elaborate
Example - Testing a blog API
• Create a blog • Create two blog entries • Create some users • Create some comments from those
users, on the two posts • Request the stats for the blog and
check if the given number of entries and comments are correct
post('/blogs', { name: 'My blog' }, function(err, blog) { ! post(concatUrl('blogs', blog.id, 'entries'), { title: 'my first post’, body: 'Here is the text of my first post' }, function(err, entry1) { ! post(concatUrl('blogs', blog.id, 'entries'), { title: 'my second post’, body: 'I do not know what to write any more...' }, function(err, entry2) { ! post('/user', { name: 'john doe’ }, function(err, visitor1) { ! post('/user', { name: 'jane doe' }, function(err, visitor2) { ! post('/comments', { userId: visitor1.id, entryId: entry1.id, text: "well written dude" }, function(err, comment1) { ! post('/comments', { userId: visitor2.id, entryId: entry1.id, text: "like it!" }, function(err, comment2) { ! post('/comments', { userId: visitor2.id, entryId: entry2.id, text: "nah, crap" }, function(err, comment3) { ! get(concatUrl('blogs', blog.id), function(err, blogInfo) { assertEquals(blogInfo, { name: 'My blog', numberOfEntries: 2, numberOfComments: 3 }); }); }); }); }); }); }); }); }); });
https:// github.com/
jakobmattsson/ z-presentation/
blob/master/ promises-in-out/
1-naive.js
1
Note: without narration, this slide lacks a lot of context. Open the file above and read the
commented version for the full story.
post('/blogs', { name: 'My blog' }, function(err, blog) { ! var entryData = [{ title: 'my first post', body: 'Here is the text of my first post' }, { title: 'my second post', body: 'I do not know what to write any more...' }] ! async.forEach(entryData, function(entry, callback), { post(concatUrl('blogs', blog.id, 'entries'), entry, callback); }, function(err, entries) { ! var usernames = ['john doe', 'jane doe']; ! async.forEach(usernames, function(user, callback) { post('/user', { name: user }, callback); }, function(err, visitors) { ! var commentsData = [{ userId: visitor[0].id, entryId: entries[0].id, text: "well written dude" }, { userId: visitor[1].id, entryId: entries[0].id, text: "like it!" }, { userId: visitor[1].id, entryId: entries[1].id, text: "nah, crap" }]; ! async.forEach(commentsData, function(comment, callback) { post('/comments', comment, callback); }, function(err, comments) { ! get(concatUrl('blogs', blog.id), function(err, blogInfo) { ! assertEquals(blogInfo, { name: 'My blog', numberOfEntries: 2, numberOfComments: 3 }); }); }); }); }); });
https:// github.com/
jakobmattsson/ z-presentation/
blob/master/ promises-in-out/
2-async.js
2
Note: without narration, this slide lacks a lot of context. Open the file above and read the
commented version for the full story.
https:// github.com/
jakobmattsson/ z-presentation/
blob/master/ promises-in-out/
3-async-more-parallel.js
3
Note: without narration, this slide lacks a lot of context. Open the file above and read the
commented version for the full story.
post('/blogs', { name: 'My blog' }, function(err, blog) { ! async.parallel([ function(callback) { var entryData = [{ title: 'my first post', body: 'Here is the text of my first post' }, { title: 'my second post', body: 'I do not know what to write any more...' }]; async.forEach(entryData, function(entry, callback), { post(concatUrl('blogs', blog.id, 'entries'), entry, callback); }, callback); }, function(callback) { var usernames = ['john doe', 'jane doe’]; async.forEach(usernames, function(user, callback) { post('/user', { name: user }, callback); }, callback); } ], function(err, results) { ! var entries = results[0]; var visitors = results[1]; ! var commentsData = [{ userId: visitors[0].id, entryId: entries[0].id, text: "well written dude" }, { userId: visitors[1].id, entryId: entries[0].id, text: "like it!" }, { userId: visitors[1].id, entryId: entries[1].id, text: "nah, crap" }]; ! async.forEach(commentsData, function(comment, callback) { post('/comments', comment, callback); }, function(err, comments) { ! get(concatUrl('blogs', blog.id), function(err, blogInfo) { assertEquals(blogInfo, { name: 'My blog', numberOfEntries: 2, numberOfComments: 3 }); }); }); }); });
post('/blogs', { name: 'My blog' }).then(function(blog) { ! var visitor1 = post('/user', { name: 'john doe' }); ! var visitor2 = post('/user', { name: 'jane doe' }); ! var entry1 = post(concatUrl('blogs', blog.id, 'entries'), { title: 'my first post', body: 'Here is the text of my first post' }); ! var entry2 = post(concatUrl('blogs', blog.id, 'entries'), { title: 'my second post', body: 'I do not know what to write any more...' }); ! var comment1 = all(entry1, visitor1).then(function(e1, v1) { post('/comments', { userId: v1.id, entryId: e1.id, text: "well written dude" }); }); ! var comment2 = all(entry1, visitor2).then(function(e1, v2) { post('/comments', { userId: v2.id, entryId: e1.id, text: "like it!" }); }); ! var comment3 = all(entry2, visitor2).then(function(e2, v2) { post('/comments', { userId: v2.id, entryId: e2.id, text: "nah, crap" }); }); ! all(comment1, comment2, comment3).then(function() { get(concatUrl('blogs', blog.id)).then(function(blogInfo) { assertEquals(blogInfo, { name: 'My blog', numberOfEntries: 2, numberOfComments: 3 }); }); }); });
https:// github.com/
jakobmattsson/ z-presentation/
blob/master/ promises-in-out/
4-promises-convoluted.js
4
Note: without narration, this slide lacks a lot of context. Open the file above and read the
commented version for the full story.
var blog = post('/blogs', { name: 'My blog' }); !var entry1 = post(concatUrl('blogs', blog.get('id'), 'entries'), { title: 'my first post', body: 'Here is the text of my first post' }); !var entry2 = post(concatUrl('blogs', blog.get('id'), 'entries'), { title: 'my second post', body: 'I do not know what to write any more...' }); !var visitor1 = post('/user', { name: 'john doe' }); !var visitor2 = post('/user', { name: 'jane doe' }); !var comment1 = post('/comments', { userId: visitor1.get('id'), entryId: entry1.get('id'), text: "well written dude" }); !var comment2 = post('/comments', { userId: visitor2.get('id'), entryId: entry1.get('id'), text: "like it!" }); !var comment3 = post('/comments', { userId: visitor2.get('id'), entryId: entry2.get('id'), text: "nah, crap" }); !var allComments = [comment1, comment2, comment2]; !var blogInfoUrl = concatUrl('blogs', blog.get('id')); !var blogInfo = getAfter(blogInfoUrl, allComments); !assertEquals(blogInfo, { name: 'My blog', numberOfEntries: 2, numberOfComments: 3 });
https:// github.com/
jakobmattsson/ z-presentation/
blob/master/ promises-in-out/
5-promises-nice.js
5
Note: without narration, this slide lacks a lot of context. Open the file above and read the
commented version for the full story.
1 Promises out: Always return promises - not callback
2 Promises in: Functions should accept promises as well as regular values
Three requirements
Awesome!
1 Promises out: Always return promises - not callback
2 Promises in: Functions should accept promises as well as regular values
3 Promises between: Augment promises as you augment regular objects
Three requirements
Augmenting objects is a sensitive topic
Extending prototypes !
vs !
wrapping objects
You’re writing Course of action
An app Do whatever you want
A library Do not modify thedamn prototypes
Complimentary decision matrix
Promises are already wrapped objects
_(names) .chain() .unique() .shuffle() .first(3) .value()
Chaining usually requires a method to ”unwrap” or repeated wrapping
u = _(names).unique() s = _(u).shuffle() f = _(s).first(3)
Promises already have a well-defined way of
unwrapping.
_(names) .unique() .shuffle() .first(3) .then(function(values) { // do stuff with values... })
If underscore/lodash wrapped promises
But they don’t
Enter !
Z
1 Deep resolution: Resolve any kind of object/array/promise/values/whatever
2 Make functions promise-friendly: Sync or async doesn’t matter; will accept promises
3 Augmentation for promises: jQuery/underscore/lodash-like extensions
What is Z?
Deep resolution
var data = { userId: visitor1.get('id'), entryId: entry1.get('id'), text: 'well written dude' }; !Z(data).then(function(result) { ! // result is now: { // userId: 123, // entryId: 456, // text: 'well written dude' // } !});
Takes any object and resolves all promises in it.
!
Like Q and Q.all, but deep
1
Promise-friendly functions
var post = function(url, data, callback) { // POSTs `data` to `url` and // then invokes `callback` }; !post = Z.bindAsync(post); !var comment1 = post('/comments', { userId: visitor1.get('id'), entryId: entry1.get('id'), text: 'well written dude' });
bindAsync creates a function that
takes promises as arguments and
returns a promise.
2
Promise-friendly functions
var add = function(x, y) { return x + y; }; !add = Z.bindSync(add); !var sum = add(v1.get('id'), e1.get('id')); !var comment1 = post('/comments', { userId: visitor1.get('id'), entryId: entry1.get('id'), text: 'well written dude', likes: sum });
2
bindSync does that same, for functions that are not async
Augmentation for promises
var commentData = get('/comments/42'); !var text = commentData.get('text'); !var lowerCased = text.then(function(text) { return text.toLowerCase(); });
3
Without augmentation
every operation has to be wrapped
in ”then”
Augmentation for promises
Z.mixin({ toLowerCase: function() { return this.value.toLowerCase(); } }); !var commentData = get('/comments/42'); !commentData.get('text').toLowerCase();
3
Z has mixin to solve this
!
Note that Z is not explicitly applied
to the promise
Augmentation for promises
Z.mixin(zUnderscore); Z.mixin(zBuiltins); !var comment = get('/comments/42'); !comment.get('text').toLowerCase().first(5);
3
There are prepared packages to mixin
entire libraries
1 Promises out: Always return promises - not callback
2 Promises in: Functions should accept promises as well as regular values
3 Promises between: Augment promises as you augment regular objects
Three requirements
Make async code as straightforward
as sync code
Enough with the madness
You don’t need a lib to do these things
But please
www.jakobm.com @jakobmattsson !
github.com/jakobmattsson/z-core
Testing web APIsSimple and readable