When you’re running end-to-end tests, chances are that sometimes you need to set up the system before running the actual test code. It can involve cleaning up after previous executions, going through some data setup “wizard” or just calling the raw server API directly. Here’s how you can do it with Protractor.
Protractor is a slick piece of technology that makes end-to-end testing pretty enjoyable. It wires together Node, Selenium (via WebDriverJS) and Jasmine, and on top of that it provides some very useful extensions for testing Angular apps and improving areas where Selenium and Jasmine are lacking.
To make this concrete, let’s say that we want to execute two calls to the server before interacting with the application. One of them removes everything from database, another kicks off a procedure that fills it with some well-known initial state. Let’s write some naive code for it.
Using an HTTP Client
var request = require('request'); describe("Sample test", function() { beforeEach(function() { var jar = request.jar(); var req = request.defaults({ jar : jar }); function post(url, params) { console.log("Calling", url); req.post(browser.baseUrl + url, params, function(error, message) { console.log("Done call to", url); }); } function purge() { post('api/v1/setup/purge', { qs : { key : browser.params.purgeSecret } }); } function setupCommon() { post('api/v1/setup/test'); } purge(); setupCommon(); }); it("should do something", function() { expect(2).toEqual(2); }); });
Since we’re running on Node, we can (and will) use its libraries in our tests. Here I’m using request, a popular HTTP client with the right level of abstraction, built-in support for cookies etc. I don’t need cookies for this test case – but in real life you often do (e.g. log in as some admin user to interact with the API), so I left that in.
What we want to achieve is running the “purge” call first, then the data setup, then move on to the actual test case. However, in this shape it doesn’t work. When I run the tests, I get:
Starting selenium standalone server... Selenium standalone server started at http://192.168.15.120:58033/wd/hub Calling api/v1/setup/purge Calling api/v1/setup/test . Finished in 0.063 seconds 1 test, 1 assertion, 0 failures Done call to api/v1/setup/purge Done call to api/v1/setup/test Shutting down selenium standalone server.
It’s all wrong! First it starts the “purge”, then it starts the data setup without waiting for purge to complete, then it runs the test (the little dot in the middle), and the server calls finish some time later.
Making It Sequential
Well, that one was easy – the HTTP is client is asynchronous, so that was to be expected. That’s nothing new, and finding a useful synchronous HTTP client on Node isn’t that easy. We don’t need to do that anyway.
One way to make this sequential is to use callbacks. Call purge, then data setup in its callback, then the actual test code in its callback. Luckily, we don’t need to visit the callback hell either.
The answer is promises. WebDriverJS has nice built-in support for promises. It also has the concept of control flows. The idea is that you can register functions that return promises on the control flow, and the driver will take care of chaining them together.
Finally, on top of that Protractor bridges the gap to Jasmine. It patches the assertions to “understand” promises and plugs them in to the control flow.
Here’s how we can improve our code:
var request = require('request'); describe("Sample test", function() { beforeEach(function() { var jar = request.jar(); var req = request.defaults({ jar : jar }); function post(url, params) { var defer = protractor.promise.defer(); console.log("Calling", url); req.post(browser.baseUrl + url, params, function(error, message) { console.log("Done call to", url); if (error || message.statusCode >= 400) { defer.reject({ error : error, message : message }); } else { defer.fulfill(message); } }); return defer.promise; } function purge() { return post('api/v1/setup/purge', { qs : { key : browser.params.purgeSecret } }); } function setupCommon() { return post('api/v1/setup/test'); } var flow = protractor.promise.controlFlow(); flow.execute(purge); flow.execute(setupCommon); }); it("should do something", function() { expect(2).toEqual(2); }); });
Now the post
function is a bit more complicated. First it initializes a deferred object. Then it kicks off the request to server, providing it with callback to fulfill or reject the promise on the deferred. Eventually it returns the promise. Note that now purge
and setupCommon
now return promises.
Finally, instead of calling those functions directly, we get access to the control flow and push those two promise-returning functions onto it.
When executed, it prints:
Starting selenium standalone server... Selenium standalone server started at http://192.168.15.120:53491/wd/hub Calling api/v1/setup/purge Done call to api/v1/setup/purge Calling api/v1/setup/test Done call to api/v1/setup/test . Finished in 1.018 seconds 1 test, 1 assertion, 0 failures Shutting down selenium standalone server.
Ta-da! Purge, then setup, then run the test (again, that little lonely dot).
One more thing worth noting here is that control flow not only takes care of executing the promises in sequence, but also it understands the promises enough to crash the test as soon as any of the promises is rejected. Once again, something that would be quite messy if you wanted to achieve it with callbacks.
In real life you would put that HTTP client wrapper in a separate module and just use it wherever you need. Let’s leave that out as an exercise.