In the past, I tried more than once to automate website testing using Selenium, but I always had to abandon the idea because I found the tests hard to write and the documentation unclear on how to start.
This finally changed recently with Cypress.
Cypress has an excellent foundation, and it’s not based on Selenium core technologies: instead, it’s a new project based on Chromium foundations.
Cypress tests use Javascript and testing frameworks like Mocha, Chai and Sinon. More information here. If you already used testing frameworks on JS, you are basically at home.
Separate test logic from implementation details
One of the biggest problems of Frontend Testing is that, as time passes, tests start to break up because the html, js and css changes, and the test selectors and logic start breaking up.
My suggestion is to split the work this way:
- a support class that implements all the asserts, the logic, and works with selectors
- an integration class that only calls the method from the support class to do all the work.
For example, the integration.js
file will contain high-level functions like these:
it('Save Post', function () {
var data = {
title: 'New Post',
text: 'Lorem Ipsum'
}
// login with the backoffice user and go to the new post
Post.loginAndGoToNewPost('backoffice')
// sets the data in the form
Post.setPostData(data)
// assert the data contained in the unsaved form
Post.assertPostData(data)
// saves the post and retrieves the id
var id = Post.savePost()
// go to the post detail
Post.goToPost(id)
// assert the data contained in the post is the same we wrote
Post.assertPostData(data)
})
The support post.js
file will contain all the low-level functions to work with a post.
Login
This point it’s explained at length in Cypress documentation at this page.
Containerize the application
The first step I take when integrating frontend testing in a project is to containerize the project with Docker. This way, I only need a docker-compose up
command to start the web application with an empty database.
Database should be as empty as possible
The database should also be emptied of all the data. I usually leave a few users and very little data to assert.
For example, if I have three categories of users, the database should contain one user for each role, and I save this data in a fixture:
users.json
{
"backoffice": {
"username": "backoffice",
"password": "backoffice001",
"name": "Back",
"surname": "Office"
},
"frontoffice": {
"username": "frontoffice",
"password": "frontoffice001",
"name": "Back",
"surname": "Office"
},
"user": {
"username": "user",
"password": "user001",
"name": "Mario",
"surname": "Rossi"
},
}
Now, whenever I need to run a test with different users/roles, I can do something like that:
describe('Home page: ' + userType, function () {
beforeEach(function () {
// prepare fixture for all tests
cy.fixture('users').as('users')
});
// iterate between all roles
['backoffice', 'frontoffice', 'user'].forEach((userType) => {
context('User type: ' + userType, function () {
it('Name and surname should be equal to the logged in user', function () {
// Custom command that manages the login
cy.login(this.users[userType].username, this.users[userType].password)
// Visit home page
cy.visit('/')
// UI should reflect this user being logged in
cy.get('#logged-user-name').should('contain', this.users[userType].name)
cy.get('#logged-user-surname').should('have.value', this.users[userType].surname)
})
})
})
})
Tests should never depend on previous tests
The concept is valid for all testing frameworks, but it’s important to reiterate it: A test should never depend on the result of a previous test.
For example, a test that needs to test the editing of a post shouldn’t depend on a post created from a previous test, but it should create a new post and edit it.
The process can seem tedious, but it’s crucial that every test can be executed in isolation. If your project permits it, you can use API calls to prepare and create the content needed for the specific tests.
Run the frontend tests for each commit
Frontend testing is usually slower than Unit tests, and in a Continuous Integration context, they are harder to integrate because they slow the build pipeline a lot. I also think it’s wrong to execute them only before a deploy because, in frontend testing, it’s essential to continuously find and immediately fix the regressions.
My general suggestion is to run the tests at each commit
If your CI setup doesn’t permit this, consider running the regression testing nightly on the latest ‘good’ build present in the system.
Tests must be 100% reproducible
Tests must be written in a way that, no matter how many times you repeat them, they always return the same result. This is one of the most critical part of writing tests in general, and it’s more important in Frontend Testing because they are much slower to execute and debug than Unit Testing and Integration Testing.
Imagine this scenario:
The Frontend Testing finds a couple of regressions, and your team starts analyzing the problem; after hours of hard work, they discover that the problem wasn’t in the source code but in the test code: The tests ‘randomly’ fail because they are not coded correctly. After this happens a couple of times, your team slowly starts to ignore all frontend testing regressions and also stops updating and writing them.
This is why it’s really important to start with a small but excellent test suite that will never fail for ‘random’ causes or timeouts.
After the team understands how to build top-notch tests, the test suite can be expanded and cover more cases.
Like many programming things, the expansion is usually exponential: The first couple of weeks are spent working on a basic test suite and the foundation classes that drives them, but after that phase, the test suite can expand faster.
When in doubt, check the documentation
Cypress technical documentation is excellent, and I highly suggest to read all the introductory articles; they are full of tips and high-level suggestions on how to structure your test suite.