Introduction to awesome UI testing with Cypress
Eversince I started working on user interface code, automated UI tests have been a major pain point. There was selenium for Web interfaces and Squish for a Qt GUI. It was inherently unstable, had lots of timing issues and produced more false positives than it actually discovered any real problems.
All of that changed the first time I started using Cypress for an Angular interface. The first features that immediately grabbed my attention were the awesome test runner UI and its ability to properly wait for the tested UI to be ready, after letting it process all scheduled events and animations. This was complemented by nicely readable test code and a command API which made writing those tests a breeze.
Installation
I would like to focus on the ease of writing the actual tests, so I will skip installation and initial setup. It is well documented on their homepage: Installing Cypress
A first test script
The basic structure of a test file is based on the mocha API using describe()
and it()
. Imagine you have a simple counter web app with a label #counterLabel
displaying a number and a button #countUpButton
which is supposed to increase the number in the label by one for each click. You can see an example for a simple test in this snippet:
describe('The counter', () => {
beforeEach(() => {
cy.visit('/counter');
});
it('should count up by one if the button is pressed', () => {
cy.get('#counterLabel').should('contain', '0');
cy.get('#countUpButton').click();
cy.get('#counterLabel').should('contain', '1');
});
});
First, it sets up descriptive labels for the test steps and initializes each test in the context by first visiting /counter
before each case. Additionally, it contains one testcase to check if the counter displays 0
initially. Afterwards it clicks with the button and checks if the label has changed accordingly.
At this point it does not matter if there are animations or transitions because the cypress test runner waits for events and promises to finish asynchronously. For each interaction, whether it is typing in a textbox or clicking on some element, it checks if the item is visible and enabled first, to prevent the test from performing impossible actions. Quite handy, is it not?
Mocking HTTP calls
Sometimes you might want to mock the actual backend call of your Web frontend to prevent the hassle of seeding the correct data or cleaning up after the test. Furthermore, you might want to focus on testing the behavior of the UI itself instead of mixing server or connection issues into the pool of possible reasons for failures. Whatever the reason might be, with cypress you can easily mock calls to an actual server and prepare fixtures for what the mocked backend should return for a specific HTTP request.
First, you have to initialize the mocked backend by calling cy.server()
. Afterwards you can identify routes using their URL and set a response, including a status code, if needed.
beforeEach(() => {
cy.server();
cy.route('GET', 'https://example.org/api/v1/counter', 55);
});
This setup in beforeEach()
always returns the number 55
if the counter is requested from the backend using an HTTP request. The 'GET'
parameter describes the request method and might be omitted in this case, as 'GET'
is the default method.
If we get back to the counter example from before and imagine it synchronizing the counter with a beackend, we might mock the calls and test the counter like this:
describe('The counter', () => {
beforeEach(() => {
cy.server();
cy.route('GET', 'https://example.org/api/v1/counter', 55).as('getCounter');
cy.route('POST', 'https://example.org/api/v1/counter', '').as('counterUpdate');
cy.visit('/counter');
});
it('should count up by one if the button is pressed', () => {
cy.wait('@getCounter');
cy.get('#counterLabel').should('contain', '55');
cy.get('#countUpButton').click();
cy.route('GET', 'https://example.org/api/v1/counter', 56).as('getCounter');
cy.wait('@counterUpdate');
cy.wait('@getCounter');
cy.get('#counterLabel').should('contain', '56');
});
});
At this point, we see how mocking works, including request aliases. This test should be stable now, as it waits for the network requests to complete and only then runs assertions on the counter in the DOM.
Core concepts
A section of the documentation has spared me from making bad decisions while writing cypress tests and organizing my test code. If this post got you excited about cypress and made you want to try it out yourself, please read the introductory section about cypress in Core Concepts
It should make you aware of how cypress actually parses and evaluates test code and how you can organize your test code by, e.g. writing custom commands.
Thanks for reading!
This should now be a good starting point for you to write your own awesome cypress tests. Check out their extensive documentation and give it a try!