Testing web APIs

Post on 11-May-2015

2.190 views 1 download

Tags:

description

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

Transcript of 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.

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