Abstract: have you ever wrote a successful UI test? How many times did you end up discarding a UI test because it's brittle and unmaintainable? And even if you don't write tests, tell me: do you click/type/click/click/setcookie/click/click while developing your UI?
Β
I'm going to explain you the most important best practices that allow you to master the UI tests hell, sleep at night (because you are sure your project works as expected), test the back-end responses too and completely revolutionize the way you develop your UI!
Β
I'm going to talk about - UI testing best practices - live demos with Cypress - improve your workflow using a UI test framework as your everyday working tool (so it's not only about testing)
(or just run $ npx noriste)
β’ 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
})
Β
At the moment, the Conio's back office test suite is composed by 110 UI integration tests and 15 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 |
---|---|---|
UI 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.findByText(USERNAME).type("stefano@conio.com")
cy.findByText(PASSWORD).type("mysupersecretpassword")
cy.findByText(LOGIN).click()
cy.findByText(SUCCESS_FEEDBACK).should("be.visible")
# 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 π