Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Promise to test it - jsday Verona

Promise to test it - jsday Verona

Jakob Mattsson

May 14, 2014
Tweet

More Decks by Jakob Mattsson

Other Decks in Programming

Transcript

  1. The lack and/or
    misuse of promises
    Promise
    to test it
    jakobm.com
    @jakobmattsson

    View Slide

  2. View Slide

  3. View Slide

  4. Promises 101*
    *No, this is not a tutorial

    View Slide

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

    View Slide

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

    View Slide

  7. 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); // such preprocessing!

    View Slide

  8. Why is that
    a good idea?
    • Loosen up the coupling
    • Superior error handling
    • Simplified async

    View Slide

  9. That’s enough 101

    View Slide

  10. Promises are not new
    http://github.com/kriskowal/q
    http://www.html5rocks.com/en/tutorials/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

    View Slide

  11. They’ve even
    made it into ES6

    View Slide

  12. They’ve even
    made it into ES6
    Already implemented
    natively in
    Firefox 30
    Chrome 33

    View Slide

  13. So why are you not
    using them?

    View Slide

  14. So why are you not
    using them?

    View Slide

  15. How to draw an owl

    View Slide

  16. 1. Draw some
    circles
    How to draw an owl

    View Slide

  17. 1. Draw some
    circles
    2. Draw the rest of
    the fucking owl
    How to draw an owl

    View Slide

  18. We want to
    draw owls.
    !
    Not circles.

    View Slide

  19. 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

    View Slide

  20. That’s circles.
    !
    Nice circles!
    !
    Still circles.

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. Used to be
    callback-hell.
    !
    Now it is
    then-hell.

    View Slide

  24. View Slide

  25. View Slide

  26. These examples are
    just a different way
    of doing async.
    !
    It’s still uncomfortable.
    It’s still circles!

    View Slide

  27. The point of promises:

    View Slide

  28. !
    Make async code
    as easy to write
    as sync code
    The point of promises:

    View Slide

  29. 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

    View Slide

  30. Let me elaborate

    View Slide

  31. 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

    View Slide

  32. 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.

    View Slide

  33. 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.

    View Slide

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

    View Slide

  35. 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.

    View Slide

  36. 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.

    View Slide

  37. View Slide

  38. 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

    View Slide

  39. Augmenting objects
    is a sensitive topic

    View Slide

  40. Extending prototypes
    !
    vs
    !
    wrapping objects

    View Slide

  41. You’re writing Course of action
    An app Do whatever you want
    A library
    Do not modify the

    god damn prototypes
    Complimentary
    decision matrix

    View Slide

  42. View Slide

  43. Promises are already
    wrapped objects

    View Slide

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

    View Slide

  45. Promises already have a
    well-defined way of
    unwrapping.

    View Slide

  46. _(names)
    .unique()
    .shuffle()
    .first(3)
    .then(function(values) {
    // do stuff with values...
    })
    If underscore/lodash
    wrapped promises

    View Slide

  47. But they don’t

    View Slide

  48. Enter
    !
    Z

    View Slide

  49. 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?

    View Slide

  50. 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

    View Slide

  51. 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

    View Slide

  52. 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

    View Slide

  53. 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”

    View Slide

  54. View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. 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

    View Slide

  58. Make async code
    as easy to write
    as sync code
    Enough with
    the madness

    View Slide

  59. You don’t have to use
    a lib to do these things

    View Slide

  60. But FFS

    View Slide

  61. View Slide

  62. #noandthen
    www.jakobm.com @jakobmattsson
    !
    github.com/jakobmattsson/z-core
    Enough with
    the madness

    View Slide