We at Viaboxx have been using Selenium combined with Cucumber for a couple of years in most of our projects. Some negative things that we encountered are the shakiness of web tests developed with Selenium. Quite often Selenium is waiting for a DOM change that has already happened and therefore fails with a timeout.
With the new features in modern browsers, one can get notified on DOM changes which led to new web testing frameworks using these features and avoiding unnecessary waits. One of these interesting new frameworks is Cypress.
Cypress is a frontend testing tool for developing end-to-end tests, integration tests and unit tests. The main features are ‘real-time reloads’, ‘automatic waiting’ and also ‘debuggability’ of your tests.
In this blog post, we will explain how we have used Cypress to create a small unit test suite for our Angular 8 application ‘logistics-tracker’. The application contains a table that shows a list of pallets with corresponding IDs and x, y and z positions. Above the table is a filter that allows a user to filter all columns of the table for specific content.
To get started with the testing, we install the newest Cypress version within our npm based project, which currently is 3.4.1.
npm install cypress --save-dev
This will also create a ‘cypress’ folder inside your project and add a lot of example tests to it.
We use ng serve for serving our application, which is part of the Angular-CLI. Serving the application is required to make it available for Cypress.
ng serve
We start with our first test inside the ‘/cypress/integration’ folder and call it ‘logistictracker_spec.js’. Cypress is using the Mocha testing framework for Javascript and uses its BDD syntax. Within the test file, we use the describe method to structure our different test specs in one test suite and describe its content.
describe('Logistics tracker page test suite', () => {
});
To test our frontend behaviour in the first place, we add mock-data to it using the fixture feature of Cypress. Therefore, we add a JSON file named ‘logistics_tracker.json’ with data to the folder ‘/cypress/fixtures’ and add the following code inside our test:
beforeEach(() => {
cy.server();
cy.fixture('logistics_tracker.json').then((json) => {
cy.route('GET', '/api/pallet', json);
});
});
The beforeEach hook will be executed before each separate test spec in our test file. Whenever the application retrieves data with a GET request on the route ‘/api/pallet’, Cypress uses the fixture file which we added to replace the result with our provided data.
It is Best Practise to use the beforeEach hook also for cleaning up your state before other tests are executed and don’t use the afterEach hook for this.
Then we add our first spec to test if the main heading is displayed on our page. In order to structure our tests, we use the Page Object Pattern. We use the cy.get() and cy.find() methods to get the rows of our table and the input field of our filter from the DOM. For the header, we use the cy.contains() method, which searches the DOM for an element and fails if it does not find any.
export class LogisticsTrackerPage {
static open() {
cy.visit('/');
}
static tableBodyRows() {
return cy.get('tbody').find('tr');
}
static tableFilter() {
return cy.get('input');
}
static assertHeader(name: string) {
cy.contains('h1', name);
}
}
To create the first spec, we use the it() method and add our first test code. We use the method from our Page Object to check our heading.
it('contains header', () => {
LogisticsTrackerPage.assertHeader('Logistics tracker');
});
Our second test will test the filter on the table values. At the start we expect the table to have all four rows of our mocked data. After we type a filter with the value ‘Standing’, we expect that the filter itself should have the value ‘Standing’ and that the table now only contains two rows. Then we clear the filter, type a filter with the value ‘Moving’ and expect the table to contain two rows again. Then we reload the webpage and expect the table to contain all four rows again.
it('filter logistics tracker table values', () => {
LogisticsTrackerPage.tableBodyRows()
.should('have.length', 4);
LogisticsTrackerPage.tableFilter()
.type('Standing')
.should('have.value', 'Standing');
LogisticsTrackerPage.tableBodyRows()
.should('have.length', 2);
LogisticsTrackerPage.tableFilter()
.clear()
.type('Moving')
.should('have.value', 'Moving');
LogisticsTrackerPage.tableBodyRows()
.should('have.length', 2);
cy.reload();
LogisticsTrackerPage.tableBodyRows()
.should('have.length', 4);
});
To start the tests and do further development, we use the ‘cypress open’ command. Within the started Cypress application, you can start and debug your test suits. Cypress will show the state of the webpage after each line of test code which is super useful to debug your tests.
When you are finished developing your tests you can start all tests with command ‘cypress run’. If you run your tests with this command, Cypress will take screenshots on failed tests and use configured test reporter to create reports.
Cypress supports all reporters from Mocha out of the box. Therefore we can use the JUnit reporter without any additional installation.
The JUnit reporter can be configured in the ‘cypress.json’ file in the root folder of the project. We are using the JUnit reporter, which saves the report to the ‘/build/test-reports/’ folder and logs the output also to the console.
{
"reporter": "junit",
"reporterOptions": {
"mochaFile": "build/test-reports/report-[hash].xml",
"toConsole": true,
"jenkinsMode":true
}
}
Cypress also provides Docker images which make it easy to run the tests in CI/CD Pipelines and also provide examples for different CI providers. So it is an easy next step to include the tests into a pipeline which could then use the generated test-reports.
All in all, Cypress has rich documentation which describes all the features and how to use them properly.
Coming from Selenium, it is a big improvement to have the new features that are built-in cypress. The biggest improvement from our perspective is the automatic waiting, which improves the readability of the code a lot by getting rid of code which is waiting for your webpage to react on inputs. The development of tests is also much more comfortable and less time consuming because cypress adds new tests to the cypress UI and executes them automatically while developing. Compared to Selenium, debugging your tests with cypress is way easier, because cypress saves a snapshot of your website after each executed line of test code. This makes it comfortable to find issues in your code.
The final code of our test can be found on Github.