Stefano Magni
I'm a passionate Front-end Engineer, a speaker, and an instructor. I love creating high-quality products, testing and automating everything, learning and sharing my knowledge, helping people. I work remotely for Hasura.
Writing UI tests for the web is hard, a lot of times the tests are brittle and unmaintainable. You must run them multiple times and, sometime, you need to exclude some of them from your pipeline.
Β
I'm going to explain to you the most important best practices that allow you to master the UI tests hell. I'm going to show you how much a tool like Cypress can help you writing and maintaining your UI tests.
Β
β’ HTML - π semantic markup
β’ CSSΒ - π visual regressions
β’ JSΒ Β - πΉ interaction flows
β’ APIΒ - π client/server communication
β’ βοΈ everything is asynchronous
β’ π¦ E2E tests need a lot of reliable data
β’ π debugging E2E tests is hard
β’ π the browser is slow, the UI is slow, the network is slow
β’ β¨οΈ no UI
β’ π no step-by-step test utilities
β’ π no integrated utilities
β’ π€ no clear reasons for failures
Suffers from | Full E2E test | UI integration test |
---|---|---|
the network | Yes π | No π |
the server | Yes π | No π |
data resetting | Yes π | No π |
complex case data | Yes π | No π |
Full E2E test | UI integration test | |
---|---|---|
confidence | 100% πͺ | 90% π€·ββοΈ |
performance* | ~20"/test π | ~2.5"/test π |
* benchmarking the Conio's back office test suite (env: staging)
// Cypress example
cy.server();
cy.route({
url: "/auth/token",
method: "POST",
response: "fixture:authentication-success.json"
})
cy.visit("/login")
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()
// the front-end app is going to receive the content
// of the authentication-success.json fixture
cy.route({
url: "/auth/token",
method: "POST",
response: "fixture:unexisting-user.json"
})
cy.route({
url: "/auth/token",
method: "POST",
response: "fixture:already-registered-user.json"
})
cy.route({
url: "/auth/token",
method: "POST",
response: {},
status: 401
})
cy.route({
url: "/auth/token",
method: "POST",
response: {},
status: 500
})
cy.visit("/login")
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()
* the Conio's back office test suite is composed by 80 UI integration tests and 12 E2E tests
cy.server();
cy.route({
url: "/auth/token",
method: "POST",
response: "fixture:authentication-success.json"
}).as("login-request")
cy.visit("/login")
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()
cy.wait("@login-request").then(xhr => {
expect(xhr.request.body).to.have.property("username", "stefano@conio.com")
expect(xhr.request.body).to.have.property("password", "mysupersecretpassword")
})
cy.server();
cy.route({
url: "/auth/token",
method: "POST"
}).as("login-request")
cy.visit("/login")
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()
cy.wait("@login-request").then(xhr => {
expect(xhr.status).to.equal(200)
expect(xhr.response.body).to.have.property("access_token")
expect(xhr.response.body).to.have.property("refresh_token")
})
Β
It highlights the changes in a text document
// Cypress waits up to 60 seconds for a page load
cy.visit("/login")
// up to 4 seconds for an element appearing
cy.get(".username").type("stefano@conio.com")
// up to 5 seconds for an XHR request and
// up to 30 seconds for the XHR response
cy.wait("@login-request")
// and if cypress cannot wait for something, you can use my own plugin
cy.waitUntil(() => cy.getCookie("token")
.then(cookie => Boolean(cookie && cookie.value)))
// front-end
if(window.Cypress) {
window.conioApp.store = reduxStore
}
// Cypress
window.conioApp.store.dispatch({
type: "LOGIN_REQUEST",
"stafeno@conio.com",
"mysupersecretpassword"
})
// instead of...
cy.visit("/login")
// you can
import {LOGIN_PATH} from "@/navigation/paths";
cy.visit(LOGIN_PATH)
// and if "/login" becomes "/sign-in" you will not
// face an annoying test failure
// Cypress example
cy.server();
cy.route("POST", "/auth/token").as("login-request");
cy.visit(LOGIN_PATH)
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()
cy.wait("@route_login").then(xhr => {
expect(xhr.request.body).to.have.property("username", "stefano@conio.com")
expect(xhr.request.body).to.have.property("password", "mysupersecretpassword")
expect(xhr.status).to.equal(200)
expect(xhr.response.body).to.have.property("access_token")
expect(xhr.response.body).to.have.property("refresh_token")
});
cy.get("#success-feedback").should("exist")
cy.url().should("not.include", LOGIN_PATH)
{
"cy:test:integration": "cypress run --spec \"cypress/**/*.integration.*\"",
"cy:test:e2e": "cypress run --spec \"cypress/**/*.e2e.*\""
}
Type | Filename | When |
---|---|---|
integration tests | <name>.integration.test.js | β’ development β’ CI pipeline |
e2e tests | <name>.e2e.test.js | β’ post CI pipeline β’ cron |
monitoring tests | <name>.monitoring.test.js | β’ cron |
// instead of testing the front-end through selectors...
cy.get(".username").type("stefano@conio.com")
cy.get(".password").type("mysupersecretpassword")
cy.get(".btn-primary").click()
cy.get("#success-feedback").should("exist")
// ... we can test it though contents, like the user does
import {
USERNAME,
PASSWORD,
LOGIN,
SUCCESS_FEEDBACK
} from "@/components/LoginForm/strings"
cy.getByText(USERNAME).type("stefano@conio.com")
cy.getByText(PASSWORD).type("mysupersecretpassword")
cy.getByText(LOGIN).click()
cy.getByText(SUCCESS_FEEDBACK).should("exist")
# headlessly
$ cypress run
# with its UI
$ cypress open
For component development and tests: take a look at Storybook and its storyshots addon.
I and Jaga SantagostinoΒ are preparing a two-day workshop about everything related to JavaScript testing.
Β
The first one will be about React testing.
Β
Drop us a line if you want to know more about it π£
I've just begun writing it, all contributions are welcome π
By Stefano Magni
July 2019, first Working Software conference (https://www.agilemovement.it/workingsoftware) --- Title: Mastering UI testing --- Abstract: Writing UI tests for the web is hard, a lot of times the tests are brittle and unmaintainable. You must run them multiple times and, sometime, you need to exclude some of them from your pipeline. I'm going to explain to you the most important best practices that allow you to master the UI tests hell. I'm going to show you how much a tool like Cypress can help you writing and maintaining your UI tests.
I'm a passionate Front-end Engineer, a speaker, and an instructor. I love creating high-quality products, testing and automating everything, learning and sharing my knowledge, helping people. I work remotely for Hasura.