Testing web APIs

71
jakobm.com @jakobmattsson I’m a coder first and foremost. I also help companies recruit coders, train coders and architect soware. Sometimes I do technical due diligence and speak at conferences. Want more? Read my story or blog.

description

Jakob Mattsson «Testing web APIs»​ Frontend Dev Conf'14 www.fdconf.by

Transcript of Testing web APIs

Page 1: Testing web APIs

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.

Page 2: Testing web APIs
Page 3: Testing web APIs
Page 4: Testing web APIs

Social coding

Page 5: Testing web APIs

FishBrain is the fastest, easiest way to log and share your fishing.!Get instant updates from anglers on nearby lakes, rivers, and the coast.

Page 6: Testing web APIs

Testing web APIsSimple and readable

Page 7: Testing web APIs

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

Page 8: Testing web APIs

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 }); }); }); }); }); }); }); }); }); });

Page 9: Testing web APIs

*No, this is not a tutorial

Promises 101*

Page 10: Testing web APIs

getJSON({ url: '/somewhere/over/the/rainbow', success: function(result) { // deal with it } });

Page 11: Testing web APIs

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' });

Page 12: Testing web APIs

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);

Page 13: Testing web APIs

Why is that a good idea?

• Loosen up the coupling • Superior error handling • Simplified async

Page 14: Testing web APIs

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)

Page 15: Testing web APIs

That’s enough 101(even though people barely talk about

the last - and most important - idea)

Page 16: Testing web APIs

Promises are not new

Page 17: Testing web APIs

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

Page 18: Testing web APIs

They’ve even made it into ES6

Page 19: Testing web APIs

They’ve even made it into ES6

Already implemented natively in

Firefox 30Chrome 33

Page 20: Testing web APIs

So why are you not using them?

Page 21: Testing web APIs

So why are you not using them?

Page 22: Testing web APIs

How to draw an owl

Page 23: Testing web APIs

1. Draw some circles

How to draw an owl

Page 24: Testing web APIs

1. Draw some circles

2. Draw the rest of the owl

How to draw an owl

Page 25: Testing web APIs

We want to draw owls.

!

Not circles.

Page 26: Testing web APIs

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

Page 27: Testing web APIs

That’s circles. !

Nice circles! !

Still circles.

Page 28: Testing web APIs

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

Page 29: Testing web APIs

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

Page 30: Testing web APIs

Used to be callback-hell.

!

Now it is then-hell.

Page 31: Testing web APIs
Page 32: Testing web APIs
Page 33: Testing web APIs

These examples are just a different way

of doing async. !

It’s still uncomfortable. It’s still circles!

Page 34: Testing web APIs

The point of promises:

Page 35: Testing web APIs

!

Make async code as straightforward

as sync code

The point of promises:

Page 36: Testing web APIs

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

Page 37: Testing web APIs

Let me elaborate

Page 38: Testing web APIs

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

Page 39: Testing web APIs

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.

Page 40: Testing web APIs

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.

Page 41: Testing web APIs

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 }); }); }); }); });

Page 42: Testing web APIs

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.

Page 43: Testing web APIs

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.

Page 44: Testing web APIs
Page 45: Testing web APIs

1 Promises out: Always return promises - not callback

2 Promises in: Functions should accept promises as well as regular values

Three requirements

Page 46: Testing web APIs

Awesome!

Page 47: Testing web APIs

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

Page 48: Testing web APIs

Augmenting objects is a sensitive topic

Page 49: Testing web APIs

Extending prototypes !

vs !

wrapping objects

Page 50: Testing web APIs

You’re writing Course of action

An app Do whatever you want

A library Do not modify thedamn prototypes

Complimentary decision matrix

Page 51: Testing web APIs
Page 52: Testing web APIs

Promises are already wrapped objects

Page 53: Testing web APIs

_(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)

Page 54: Testing web APIs

Promises already have a well-defined way of

unwrapping.

Page 55: Testing web APIs

_(names) .unique() .shuffle() .first(3) .then(function(values) { // do stuff with values... })

If underscore/lodash wrapped promises

Page 56: Testing web APIs

But they don’t

Page 57: Testing web APIs

Enter !

Z

Page 58: Testing web APIs

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?

Page 59: Testing web APIs

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

Page 60: Testing web APIs

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

Page 61: Testing web APIs

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

Page 62: Testing web APIs

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”

Page 63: Testing web APIs
Page 64: Testing web APIs

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

Page 65: Testing web APIs

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

Page 66: Testing web APIs

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

Page 67: Testing web APIs

Make async code as straightforward

as sync code

Enough with the madness

Page 68: Testing web APIs

You don’t need a lib to do these things

Page 69: Testing web APIs

But please

Page 70: Testing web APIs
Page 71: Testing web APIs

www.jakobm.com @jakobmattsson !

github.com/jakobmattsson/z-core

Testing web APIsSimple and readable