In the previous post, we explored the Page Object pattern and rewrote our tests to use this technique. Sometimes, it can be that the tests appear to be a bit verbose due to the usage of promises. Additionally, promises and asynchronous programming in general can be somewhat confusing to developers. Let’s see some ways of making the tests shorter and easier to read.

First of all, we’re working with promises. There’s a nice chai plugin, chai-as-promised, that can help us our here. Let’s install it with npm install --save-dev chai-as-promised.

In order to use it, we’d have to include it and tell chai about it. This is done with chai.use(require('chai-as-promised'));. The question is, where should we do that? We could do it on the test file, but we’ll be repeating that in every test file we’d like to use it. To avoid repetition, we can put this statement on the top of our WebDriverHelper class, since this is a class that all of our tests will be including:

var chai = require('chai');

Now this plugin is available in all of our tests. Let’s use it for the simple test that checks the title of the homepage:

it('should verify the title of Google', function() {
    return expect(page.getTitle()).to.eventually.equal('Google');

The test is reduced to a readable one-liner. It’s almost an English sentence: “I expect that the page title will eventually equal to Google”. The most important word here is eventually. The developer(s) of this plugin picked a very accurate name here. It describes exactly what promises are about. Call the function and it will return you the value, eventually, once the asynchronous operation has completed.

The first thing to note is that we are returning the whole thing. The usage of eventually is also a promise that needs to be returned. This is very important as a common source of problems with functional tests is forgetting to return a promise.

Second, the argument of expect needs to be a promise. If you get an error message like argument is not a thenable, then it means you’re trying to combine eventually with something that is not a promise. In this case, the getTitle method returns a promise so we’re good to go.

Finally, the assertion. What can we use after the .to.eventually. part? The answer is, you can still use all of chai’s assertions. This plugin intelligently discovers the available assertions that chai offers and extends them. So everything mentioned by chai can still be used. As an example, let’s see the test that verifies we have a search text box:

it('should have a text search box', function() {
    return expect(page.searchTextBox.isVisible());

Here we used the .be.true assertion. Again, the test was reduced to an one-liner. You might end up with quite long lines, so it’s okay to break them down into multiple lines for readability.

There is another way we can write our tests. We can use ES6 Generators. This changes significantly the style of the tests and makes them look more like they’re synchronous code. Let’s see the test that performs the search, written with this style:

it('should search for pokemon', function * () {
    yield page.searchTextBox.setValue('Pokemon');
    var text = yield page.searchResults.getText();
    expect(text).to.equal('Ongeveer 330.000.000 resultaten (0,37 seconden)', 'unexpected search result message');

Two obvious things: there are yield keywords all over the place and there is a star in the first line. What’s up with that?

The star ( function * () ) indicates that this is a generator function: a function that can be exited and later re-entered. This fits well with the promises: exit the function and re-enter it once the promise has been resolved.

The yield keyword works inside a generator function and it allows for pausing and resuming the execution. This allows us to yield promises and collect their values when they’re done. On the first line, we just yield the promise that sets the value of the text field, so we don’t care about a return value. Same on the second line, we just click the button. Next line though, we have an ordinary variable that collects the result of the getText method. And finally, a regular assertion, without the eventually plugin.

One gotcha: mocha does not support this out of the box. If you try to run this, you’ll actually get evergreen tests. I’m not sure if they plan to support it. In the mean time, there are some workarounds. I’ve tried the mocha-generators package and it seems to work.

This coding style looks quite strange at first. If you’ve done any .NET programming with async/await, you can probably relate to that. Which style should you use? Let’s see them side by side:

Using generators is perhaps a bit alien. However, the code is much shorter. If you get used to it, it starts to feel like synchronous programming. It doesn’t save you from the danger of not returning promises. You can easily forget to add a yield keyword. Any style you pick, you have to make sure that the developers are async-savvy and aware of what they’re doing.

In my current project at work, we had decided to not use generators for a couple of reasons:

  • we had already a big code base with many functional tests written in a specific style: returning promises and using chai-as-promised whenever it makes the code shorter. Having two (or more) ways of writing the tests could be confusing to the developers and create unmaintainable code.
  • allowing to use ES6 features would open up the appetite for more experienced developers to start using more of these features. As our developer expertise levels vary, this would be a problem to the junior developers and in the end to the project and the code base as a whole.
  • lack of confidence that the mocha-generators patch solution will cover all cases. Maybe some test stays evergreen even after applying that package's fix?
  • at that point, WebDriverIO team were working on version 4 of their product, which would bring a different way of writing tests that look more like synchronous code.

I’ve left for last one more technique, that involves using the integrated test runner of WebDriverIO. We’ll have a look at that on the next post.