Skip to main content
fingers typing on a keyboard

01 May 2019

UI testing done right

john-reilly.jpg

John Reilly | TypeScript enthusiast

UI testing done right

It's possible that you may have written user interface tests before. Tests that spin up a version of your application, and progressively click and tap their way through, identifying issues that may have been introduced into the codebase.

 

There's a number of players in this space; Selenium, Coded UI and so forth. Then there's Cypress. As someone who has had a whirl with each of these technologies, let me tell you something: Cypress tests are the tests you want to write. They are fast, they are deterministic, they are simple to write, they're stubbable (you can hit your back-end or stub it out).

 

Did I mention they're fast? The Cypress test debugging experience is second-to-none; with all the power of Chrome DevTools at your elbow. Cypress is quite frankly a whole stack of awesome.
But enough flannel. Let's take a look at the goodies.

Developers... automate!

Here in Investec-land, we're beavering away on a new Business Banking platform. It's so new and shiny it doesn't even have an official name yet. But what we have got is the beginnings of our platform.

 

Tech-wise we're running on ASP.Net Core on the back-end, with a front end written in React with TypeScript. Because we don't like to find bugs in Production, we invest in automated testing. Every change we make to the codebase only gets to be merged when it has run the gauntlet of our automated test suites.

 

XUnit tests for ASP.Net Core, check. End-to-end tests for APIs, check. Database tests, check. Jest unit tests for TypeScript and React, complete with snapshots, check. We had this all plugged into our CI pretty early. But there was hesitancy about what to do about UI testing, UI testing is famously slow.

 

It's not unusual to have a UI test suite that takes 8 hours+ to run. What we really wanted was UI tests that could run as part of our CI flow.

 

We now have a rather beautiful development approach for our front end code

Around this time, we happened upon the very talented Zoltan, a dev that truly groks automated testing. He plugged Cypress into our dev workflow and our CI server and the rest is automated history.

 

Thanks to his work, we now have a rather beautiful development approach for our front end code. We fire up our application in dev mode, which includes spinning up our Cypress test suite. We can write Cypress tests at the same time as we're implementing the actual code.

 

As we make a change, we can re-run our relevant Cypress test in seconds without breaking out of our dev setup. To be clear; we're writing our test at the same time as we're implementing our code, and changes we make to the codebase are repeatedly checked by the tests being re-run again and again.

 

This is a productivity boost which leaves you with automated tests and implementation at the point you're ready to submit your pull request. Let me emphasise this point as I think it's super important. At the point when you want to add code to the codebase, you already have automated tests that will from that time forwards, be executed when any further changes to the codebase are attempted.

 

Hence protecting that functionality from day 1, and automating the process of regression testing. It helps you ship quickly and with confidence. This is a massive Wynne.

(function(INVESTEC) { "use strict"; function NoticePlusRatePicker(el) { this.el = el || {}; this.title = this.el.querySelector(".notice-plus-rate-picker__title"); this.init(); } NoticePlusRatePicker.prototype = { constructor: NoticePlusRatePicker, init: function() { this.bindEvents(); this.scrollMagic(); this.angular(); }, scrollMagic: function() { // I replaced the previous function with this this.scrollMagicProperties = {}; this.scrollMagicProperties.functions = []; this.scrollMagicProperties.animations = []; }, bindEvents: function() { var vm = this; var $html = $("html"); window.addEventListener("resize", function() { $html.addClass("notranstition"); vm.setHighlighters(); $html.removeClass("notranstition"); }); }, titleClicked: function() { alert("title clicked"); }, setHighlighters: function(cellParam) { var cell = cellParam && cellParam.length > 0 ? cellParam : $(".notice-plus-rate-picker__product-cell--selected"); if (cell.length === 0) { return false; } var cellLastIndex = cell.closest("tr").children().length - cell.index(); var component = cell.closest(".notice-plus-rate-picker"); var cellHighlighter = component.find( ".notice-plus-rate-picker__cell-highlighter" ); var xHighlighter = component.find( ".notice-plus-rate-picker__x-highlighter" ); var yHighlighter = component.find( ".notice-plus-rate-picker__y-highlighter" ); var table = cell.closest("table"); var rowSiblings = cell .closest("tr") .children() .not(".notice-plus-rate-picker__table-y-caption"); var colSiblings = cell .closest("table") .find("tr") .not(":nth-child(-n+1)") .children() .not(".notice-plus-rate-picker__table-y-caption") .filter(":nth-last-child(" + cellLastIndex + ")"); var yArrowIndicator = component.find( ".notice-plus-rate-picker__y-arrow-indicator" ); var xArrowIndicator = component.find( ".notice-plus-rate-picker__x-arrow-indicator" ); cellHighlighter.width(cell.outerWidth()); cellHighlighter.height(cell.closest("tr").outerHeight()); cellHighlighter.css("left", cell.position().left); cellHighlighter.css("top", cell.position().top); xHighlighter.width( rowSiblings.last().position().left + rowSiblings.last().outerWidth() - rowSiblings.first().position().left ); xHighlighter.height(cell.closest("tr").outerHeight()); xHighlighter.css("left", rowSiblings.first().position().left); xHighlighter.css("top", rowSiblings.first().position().top); } }; });

Demo me this

To take this out of the realms of hyperbole, let's take a look at a simple test. For the purposes of demonstrating our new platform to potential new clients we have a "demo mode" built into our application. It contains stubbed data, and allows the user to get a feel for what the available functionality is. Users can access it by going to https://invest.ec/2OSoEkV 

 

You're very welcome to have a go with this yourself; we're currently releasing new versions of our platform daily and so what's available is constantly changing.

 

When we were implementing this demo mode functionality we wanted to have a Cypress test that covered this. This test should do the following:

 

Go to our application URL with the suffix ?demo=trueTest that we enter the application's demo mode; i.e. do we see the demo disclaimer that users have to accept before they can access the demoWhen they do accept, do they see the application in demo mode? i.e. do we see the demo sash in the corner of the screen / does the URL indicate that we are pointing at stubbed data?

 

When the user reloads; they should remain in demo mode. Implementing this was very easy indeed. This is our complete demo.js test with annotations: 

describe("demo", () => { it("loads the demo page", () => { // entering demo mode sets a value to sessionStorage; so we // should clear it before the test runs sessionStorage.clear(); // go to our app with the demo flag set in the querystring cy.visit("/?demo=true") // click the disclaimer button; remember the button won't // be there in non-demo mode so this would fail .get("[data-testid=demo-disclaimer-button]") .click() // check the URL contains the id of our test data ("1") .url() .should("contain", "/legal-entities/1") // check our demo sash is visible; it should only be .get("[data-testid=demo-sash]") .should("be.visible") // when you reload you should remain in demo mode .reload() .get("[data-testid=demo-sash]") .should("be.visible"); }); });

I'll show you

 

So what does this actually look like? Well, rather like this:

 

Cypress test demo
The show's Cypress's test runner, running our demo test. The test runs in 2.02 seconds. I'll say that again: 2.02 seconds. For a UI test that is superfast. And that's one of the reasons that here at Investec we love Cypress.
Want to work on this cool stuff?

Get in touch

If you want to join a dynamic team, drop us a line.

Visit our jobs portal

To browse our latest vacancies across Investec, visit our jobs board.

More from the Investec Engineering team