Playing with Node.js, Express and WebDriver I spotted duplication in my Mocha tests. Each test started/stopped an Express application and created/destroyed a WebDriver client:
before(() => { startServer(); createWebdriver(); }); after(() => { stopServer(); destroyWebdriver(); }); var server, driver; const startServer = () => server = app.listen(3000); const stopServer = () => { var deferred = Q.defer(); server.close(() => deferred.resolve()); return deferred.promise; }; const createWebdriver = () => { driver = new webdriver.Builder() .forBrowser('firefox') .build(); }; const destroyWebdriver = () => driver.quit();
Obviously, I could move startServer
/stopServer
and createWebdriver
/destroyWebdriver
pairs to a separate module and require them in each test. This would have partially solved the duplication, since those methods were defined only in one place.
However, test setup and teardown methods (before
and after
) are still duplicated in each test case. They share the state (server handle and WebDriver client) and are sequentially coupled. Each launched server instance must be shut down. Same with the WebDriver client. Moreover, I cannot separate setup and teardown steps and reuse e.g. only the server launch.
How can it be solved?
First, you can have more then one Mocha setup and teardown lifecycle function.
Second, statements in anonymous functions passed to before
and after
are coincidentally cohesive. They have been put together only because they need to be executed at the beginning/end of the test case. This is equivalent to the previous snippet:
before(() => startServer()); before(() => createWebdriver()); after(() => stopServer()); after(() => destroyWebdriver());
Third, functions in JavaScript are first-class citizens. They can be passed around and executed in a different context.
Knowing all this, what’s the final solution?
Coordinate cohesive setup/teardown code in a module. Pass setup and teardown functions to the module and execute them when creating the module instance. Export created or initialised objects if your test methods have to use them.
And the refactored code:
// in test require('./server')(before, after); const {driver} = require('./driver')(before, after); // server.js module.exports = (setupFn, teardownFn) => { var server; const startServer = () => server = app.listen(port); const stopServer = () => { var deferred = Q.defer(); server.close(() => deferred.resolve()); return deferred.promise; }; setupFn(() => startServer()); teardownFn(() => stopServer(); }; // driver.js module.exports = (setupFn, teardownFn) => { var driver; const createWebdriver = () => { driver = new webdriver.Builder() .forBrowser('firefox') .build(); }; const destroyWebdriver = () => driver.quit(); setupFn(() => createWebDriver()); teardownFn(() => destroyWebdriver()); return { driver: () => driver }; };
Notice, that in the original code, driver
was a WebDriver client instance (an object with properties and methods). After the refactoring, driver
is a function that returns the WebDriver client from the module instance. I cannot expose the WebDriver client directly (i.e. as return { driver: driver }
). because after the module evaluation it is still undefined
.
Conclusion
Passing Mocha lifecycle functions around removed duplication, put sequential coupling under control and allowed composability of test setup and teardown routines.