Working with Multiple Values in a Promise Chain using Stateful Context

We explore JavaScript promise chains with multiple values within a stateful context.

In one of our project’s test code, we have to create many objects asynchronously and use them later in our tests. Our codebase is promise-based, so we have to chain these promises in a way that makes it possible to access all the variables we want in later time.

There are many ways to do this, which we’ll explore.

A Gentle Example

Our example will be based on this scenario: To test that the “Following” feature works in a Twitter-like application, we must ensure that when A follows B and B posts a status update, A sees B’s status in A’s timeline.

This involves steps such as:
  1. Create the users A and B.
  2. Make A follow B.
  3. Make B post a status update.
  4. Verify that A’s timeline contains B’s status.

In our actual project, setting up a scenario is more complicated than that. It involves creating an access key, accounts of different types, and several transactions.

Let’s come back to our simplified example, and explore the ways we can code it.

Nesting the Promises

Assuming an ES6 environment, one can implement the test code like this:
it('should display the status of B in A\'s timeline when A follows B', function() {
  return Promise.all([createUser('foo'), createUser('bar')])
  .then(function([user1, user2]) {
    return user1.follow(user2)
    .then(function() {
      return user2.updateStatus('My status')
    })
    .then(function(status) {
      return user1.getTimeline()
      .then(function(timeline) {
        expectTimelineToContainStatus(timeline, status)
      })
    })
  })
})

See how messy it is! Now the callback hell becomes a promise hell.

Passing Them Through the Chain

Another approach is to chain the promises linearly, and pass all the value we need in future steps through the current step.
it('should display the status of B in A\'s timeline when A follows B', function() {
  return Promise.all([createUser('foo'), createUser('bar')])
  .then(function([user1, user2]) {
    return Promise.all([user1, user2, user1.follow(user2)])
  })
  .then(function([user1, user2]) {
    return Promise.all([user1, user2.updateStatus('My status')])
  })
  .then(function([user1, status]) {
    return Promise.all([user1.getTimeline(), status])
  })
  .then(function([timeline, status]) {
    expectTimelineToContainStatus(timeline, status)
  })
})

Now, you have to make sure that we are passing the correct variables in every step.

Creating Lots of Local Variables

Yet another approach is to create many local variables to hold everything we need.
it('should display the status of B in A\'s timeline when A follows B', function() {
  var user1, user2, status
  return Promise.all([createUser('foo'), createUser('bar')])
  .then(function([_user1, _user2]) {
    user1 = _user1
    user2 = _user2
    return user1.follow(user2)
  })
  .then(function() {
    return user2.updateStatus('My status')
  })
  .then(function(_status) {
    status = _status
    return user1.getTimeline()
  })
  .then(function(timeline) {
    expectTimelineToContainStatus(timeline, status)
  })
})

Here, you see the manual work of creating these variables, and storing them manually.

Using a Context Object

How about creating just one object to hold all the things we need (a context)?
it('should display the status of B in A\'s timeline when A follows B', function() {
  var the = { }
  return Promise.all([createUser('foo'), createUser('bar')])
  .then(function([user1, user2]) {
    the.firstUser = user1
    the.secondUser = user2
    return the.firstUser.follow(the.secondUser)
  })
  .then(function() {
    return the.secondUser.updateStatus('My status')
  })
  .then(function(status) {
    the.status = status
    return the.firstUser.getTimeline()
  })
  .then(function(timeline) {
    expectTimelineToContainStatus(timeline, the.status)
  })
})

Now, we only have to create just one variable. However, after each asynchronous step, we need to store the values we need in the future in the next step manually.

What If?

What if we have some kind of function that
  • takes an object of promises,
  • adds the results of each promise to the context, and
  • returns a promise that will resolve when all these promises have resolved?

Then our code would look like this:
it('should display the status of B in A\'s timeline when A follows B', function() {
  var the = { }
  return set(the, {
    firstUser: createUser('foo'),
    secondUser: createUser('bar')
  })
  .then(function() {
    return the.firstUser.follow(the.secondUser)
  })
  .then(function() {
    return set(the, { status: the.secondUser.updateStatus('My status') })
  })
  .then(function() {
    return set(the, { timeline: the.firstUser.getTimeline() })
  })
  .then(function() {
    expectTimelineToContainStatus(the.timeline, the.status)
  })
})

See how readable and straightforward it is. It reads like set the first user to (the result of) creating (a) user (called) foo.

Refactoring the setup process into beforeEach is also straightforward:
var the
beforeEach(function() { the = { } })

describe('following another user', function() {

  beforeEach(function() {
    return set(the, {
      firstUser: createUser('foo'),
      secondUser: createUser('bar')
    })
    .then(function() {
      return the.firstUser.follow(the.secondUser)
    })
    .then(function() {
      return set(the, { status: the.secondUser.updateStatus('My status') })
    })
  })

  it('should display the status of B in A\'s timeline when A follows B', function() {
    return the.firstUser.getTimeline().then(function(timeline) {
      expectTimelineToContainStatus(the.timeline, the.status)
    })
  })

})

Get Real

There’s one problem: The set function didn’t exist. Let’s code it up!

when/keys.all
contains a function that resolves the values of promises inside an object. To put it in another way, it’s a function that transforms { key: promise } into { key: resolved value }. Now, we can use that to write the set function.
var keys = require('when/keys')
function set(context, promises) {
  return keys.all(promises).then(function(results) {
    for (var key in results) {
      context[key] = results[key]
    }
  })
}

With ES6’s Object.assign, this code becomes more concise:
function set(context, promises) {
  return keys.all(promises).then(function(results) {
    Object.assign(context, results)
  })
}

And even more concise:
function set(context, promises) {
  return keys.all(promises).then(Object.assign.bind(null, context))
}

stateful-context

I wanted to give it a name, so I named it “stateful context.” The stateful-context module provides this set function:
var set = require('stateful-context').set

You can go grab it on npm. The source code is on GitHub.

StatefulContext

The stateful-context module provides a StatefulContext class. Instantiating an instance of this class gives you an empty context with the set method, already bound to the created context. This helps you eliminate the first the argument to your set calls.
var StatefulContext = require('stateful-context')

var the, set
beforeEach(function() {
  the = new StatefulContext()
  set = the.set
})

describe('following another user', function() {
  beforeEach(function() {
    return set({
      firstUser: createUser('foo'),
      secondUser: createUser('bar')
    })
    .then(function() {
      return the.firstUser.follow(the.secondUser)
    })
  })
  // ...
})

Naming the the

We’ve had discussions inside the team about how to name the context object. The name context is already taken by mocha (a testing framework), so I proposed the name the.

One issue with it is that the name the is not self-explanatory; it doesn’t describe the intent. Finally, we settled with the name locals, which is not too much to type, while preserving readability and intent:
var locals
beforeEach(function() {
  locals = new StatefulContext()
})

describe('following another user', function() {
  beforeEach(function() {
    return locals.set({
      firstUser: createUser('foo'),
      secondUser: createUser('bar')
    })
    .then(function() {
      return locals.firstUser.follow(locals.secondUser)
    })
  })
  // ...
})

But in my own projects, I’d stick with the. Even the name is not self-descriptive, I think it makes a good DSL.

Weakness: It’s Not Declarative

But this approach is not without weaknesses. We still have to set up our scenario imperatively, that is, step by step. We also have a global mutable state (but local to our test) that some people may despise.

What I’d actually would like to be able to do is to create something like RSpec’s let helper that’s aware of asynchronous promises. It lets you declaratively declare actors inside your tests.

I still couldn’t come up with an elegant way to do this in ES5 JavaScript. For CoffeeScript, I have a rough idea that it could look like this:
describe 'following another user', ->

  declare
    firstUser:   -> createUser('foo')
    secondUser:  -> createUser('bar')
    timeline:    (firstUser)  -> firstUser.getTimeline()
    status:      (secondUser) -> secondUser.updateStatus 'My status'

  beforeEach ->
    get (firstUser, secondUser) -> firstUser.follow secondUser

  it 'should display the status of B in A\'s timeline when A follows B', ->
    get (timeline, status) -> expectTimelineToContainStatus timeline, status

Imagine writing that in JavaScript. You’d be distracted by so many functions, parentheses, and curly braces.

So if you have any idea how this could be done in ES5 JavaScript, I’d love to know about them. Until then, enjoy!
Like 127 likes
Thai Pangsakulyanont
Share:

Join the conversation

This will be shown public
All comments are moderated

Get our stories delivered

From us to your inbox weekly.