E2E Tests: Test as a user would

Make your E2E tests less of a time-sink

This post contains a few recommendations on how to write certain aspects Protractor E2E tests. Specifically how to depend less on the internals of the page. You can't entirely not depend on internals: they all depend some amount of HTML, and currently you have to choose and interact with the elements of the page somehow. However, some internals are better than others.

The point of E2E tests

First a quick recap on a/the purpose of E2E tests. They ensure that aspects of the application work as expected even if there are later changes: they protect against regressions as you're developing the application. Essentially they should fail if some aspect of a user-facing feature changes in an undesirable way.

However, they should not fail if some internal of the pages changes that doesn't break a user-facing feature. This doesn't necessarily directly affect the primary purpose of the tests: they can still protect against regressions. However, failing E2E tests when there is no breakage of a user-facing feature can be a horrible time-sink. With a bit of thought, the risk of this can me minimised, or at least reduced.1

To this end, there are a few features of Protractor that I think should not be used.

Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should.

Ian Malcom, Jurassic Park

Don't depend on ng-repeat or ng-model: instead, select elements as a user would

The Protractor documentation suggests selecting HTML elements by ng-repeat or ng-model attribute contents. This means if someone comes along and does a bit of refactoring, or uses something other than ng-repeat or ng-model, the E2E tests will fail, even though the behaviour, from the users' points of view, is perfectly fine. I treat this similarly to the test depending on a variable name or process on the server. It seems quite silly for the test to be fragile with respect to that!

Instead, consider using minimal CSS or XPath selectors to pick elements as close as possible to how a user would, that would survive internal refactorings. For example, the first table after a h2 element with text content "Section 1".

table = element(by.xpath('//h2[text()="Section 1"]/following::table[1]'))

Or, if you have used a custom element for your UI widget directives, the first <my-widget></my-widget> in the first <nav></nav> of the page.

widget = element(by.xpath('//nav[1]//my-widget[1]'))

Consider how specific you want the selectors to be. If you have only one my-widget in the page, you can be more minimal in the selector.

widget = element(by.xpath('//my-widget'))

This makes the test less fragile to moving the element around the page. If it's crucial to test where this is in the page, you could always have an explicit assertion for that, perhaps in a separate test.

Another technique is to choose elements using ARIA roles.

dialog = element(by.css('[role="dialog"]'))

This has the handy benefit of gently pushing you to add accessiblity attributes.

Another technique is selecting elements using cssContainingText, say to find button that also has the text "Go to stage 2".

button = element(by.cssContainingText('button', 'Go to stage 2'))

And as a last, but perhaps still common, resort, using a plain class.

message = element(by.css('.message'))

If using classes I recommend considering that they are as semantic as possible. This isn't just to make the HTML "nice": it's so that the tests continue to pass as long as the element behaves as it should, independent of any changes or internal refactoring or renaming of non-sematic (with respect to the UI) elements on the page.

I don't think it's a perfect solution: you might want to tweak the semantic names for custom elements and classes, and in such cases the tests would still fail, but I suspect it's better for a lot of cases that depend on ng-repeat or ng-model.

Don't wait for Angular: instead, wait for the interface as a user would

If you're using Protractor, consider setting set browser.ignoreSynchronization = true for all tests. Yes, really. The magic Protractor gives you for the simple cases just isn't worth it, because the cases of having some sort of polling, or non-Angular asynchronous code running, just makes you have multiple sorts of tests.

Instead, wait for changes in the page elements to appear on the screen. You can do this explicitly in your first few tests, but then you might want to factor the waiting out once you are clear what you're waiting for and commonality between the cases is clear. Your tests will be less flakey to changes you might make in the application. Even, if you eventually decide to swap out Angular!

browser.ignoreSynchronization = true;
browser.wait(function () {
   return element(by.css('[role="dialog"]')).isPresent()
});

Of course, if you're not using what Protractor builds on top of Selenium, then I would seriously consider just using plain Selenium, or a different framework like WebdriverIO.


1 I realise I don't have quantitative data to back this claim up.