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.
Stefano Magni (@NoriSte)
Front-end Developer for
Organised by:
Hosted by:
It's all about our (and deploy) time. Slow tests will be more eligible to be discarded.
Unreliable tests are the worst ones.
Sooner or later we will discard them if they aren't reliable.
Tests are useful to avoid the cognitive load of remembering what we did months ago. They must be a useful tool, not another tool to be maintained.
Well... Avoiding test overkill is hard to learn, that's why I want to share my experience with you 😊
Unit testing:
Integration testing:
Component testing:
UI integration testing:
Snapshot testing means checking the consistency of the generated markup.
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
A test like the following one
Snapshot testing means checking the consistency of the generated markup.
exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
Produces a snapshot file like this
Snapshot testing means checking the consistency of the generated markup.
// Updated test case with a Link to a different address
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.instagram.com">Instagram</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
And a change in the test
Snapshot testing means checking the consistency of the generated markup.
Makes the test fail
Regression testing means checking the consistency of the visual aspect of the component.
Take a look at this component
If we change its CSS
Regression testing means checking the consistency of the visual aspect of the component.
A regression test prompts us with the differences
Regression testing means checking the consistency of the visual aspect of the component.
Callback testing means checking the component respects the callback contract.
it('Should invoke the passed callback when the user clicks on it', () => {
const user = {
email: "user@conio.com",
user_id: "534583e9-9fe3-4662-9b71-ad1e7ef8e8ce"
};
const mock = jest.fn();
const { getByText } = render(
<UserListItem clickHandler={mock} user={user} />
);
fireEvent.click(getByText(user.user_id));
expect(mock).toHaveBeenCalledWith(user.user_id);
});
✅ performance
⬜️ reliability
✅ maintainability
✅ assertions quality
Otherwise: the more time we spend writing them, the higher the chance that we won't write them 😉
import initStoryshots from "@storybook/addon-storyshots";
initStoryshots({
storyNameRegex: /^((?!playground|demo|loading|welcome).)*$/
});
* I used the regex to filter out useless component stories, like the ones where there is an animated loading
import initStoryshots from "@storybook/addon-storyshots";
import { imageSnapshot } from "@storybook/addon-storyshots-puppeteer";
// regression tests (not run in CI)
// they don't work if you haven't launched storybook
if (!process.env.CI) {
initStoryshots({
storyNameRegex: /^((?!playground|demo|loading|welcome).)*$/,
suite: "Image storyshots",
test: imageSnapshot({ storybookUrl: "http://localhost:9005/" })
});
} else {
test("The regression tests won't run", () => expect(true).toBe(true));
}
* I used the regex to filter out useless component stories, like the ones where there is an animated loading
At least automate the component and test creation with templates.
They are super fast because the server is stubbed/mocked, we won't face network latencies and server slowness/reliability.
✅ performance
✅ reliability
✅ maintainability
⬜️ assertions quality
Otherwise: we write a lot of E2E tests but remember that they are not practical at all 😠
We can test every UI flow/state with confidence.
A Cypress screenshot
They are slow, even because we need to clear data before and after the tests.
✅ performance
✅ reliability
⬜️ maintainability
⬜️ assertions quality
Otherwise: our tests will be 10x slower and we make our back-enders and devOps accountable for our own tests.
* A happy path is a default/expected scenario featuring no errors.
Show a success feedback
Go to the login page
Write username and password
Click "Login"
Redirect to the home page
// Cypress example
cy.visit("/login");
cy.get(".username").type(user.email);
cy.get(".password").type(user.password);
cy.get(".btn.btn-primary").click();
cy.wait(3000); // no need for that, I added it on purpose
cy.getByText("Welcome back").should("exist");
cy.url().should("be", "/home");
⬜️ performance
⬜️ reliability
✅ maintainability
✅ assertions quality
Otherwise: we need to inspect the DOM frequently in case of failures (adding unique and self-explicative IDs isn't an easy task compared with looking for content in a screenshot).
// Cypress example
cy.get(".username").type(user.email);
cy.get(".password").type(user.password);
cy.get(".btn.btn-primary").click();
// Cypress example
cy.getByPlaceholderText("Username").type(user.email);
cy.getByPlaceholderText("Password").type(user.password);
cy.getByText("Login").click();
⬜️ performance
✅ reliability
✅ maintainability
⬜️ assertions quality
Otherwise: we risk to base your tests on attributes with a different purpose, like classes or ids.
// Cypress example
// The login button now has an emoticon
// cy.getByText("Login").click();
cy.getByTestId("login-button").click();
<button data-testid="login-button">✅</Button>
<button
class="btn btn-primary btn-sm"
id="ok-button">
✅
</Button>
Go to the login page
Write username and password
Click "Login"
Redirect to the home page
Show a success feedback
Go to the login page
Write username and password
Click "Login"
Redirect to the home page
Show a success feedback
✅ performance
✅ reliability
⬜️ maintainability
⬜️ assertions quality
Otherwise: we make our tests slower and slower.
"3 seconds sleeping are enough for an AJAX call!" 👍
⌛️ Then, at the first Wi-Fi slowness...
"I'll sleep the test for 10 seconds..." 😎
⌛️ Then, at the first server wakeup...
"... 30 seconds?" 😓
⌛️ ...
⌛️ ...
⌛️ ...
Even if our AJAX request usually lasts less than a second...
// Cypress example
cy.route({
method: "POST",
url: '/auth/token'
}).as("route_login");
cy.getByPlaceholderText("Username").type(user.email);
cy.getByPlaceholderText("Password").type(user.password);
cy.getByText("Login").click();
// it doesn't matter how long it takes
cy.wait("@route_login");
cy.getByText("Welcome back").should("exist");
cy.url().should("be", "/home");
// Cypress example
cy.route({
method: "POST",
url: '/auth/token'
}).as("route_login");
cy.getByPlaceholderText("Username").type(user.email);
cy.getByPlaceholderText("Password").type(user.password);
cy.getByText("Login").click();
// it doesn't matter how long it takes
cy.wait("@route_login");
cy.getByText("Welcome back").should("exist");
cy.url().should("be", "/home");
We (me and Tommaso) have written a dedicated plugin! 🎉 Please thank the Open Source Saturday community for that, we developed it three days ago 😊
cy.waitUntil(() => cy.window().then(win => win.foo === "bar"));
For example, Cypress waits (by default, we can adjust it) up to:
Go to the login page
Write username and password
Click "Login"
Show a success feedback
Wait for the network request
Redirect to the home page
Go to the login page
Write username and password
Click "Login"
Show a success feedback
Wait for the network request
Redirect to the home page
✅ performance
✅ reliability
⬜️ maintainability
✅ assertions quality
Otherwise: we waste your time debugging our test/UI just to realize that the server payloads are changed.
cy.wait("@route_login").then(xhr => {
const params = xhr.request.body;
expect(params).to.have.property("grant_type", AUTH_GRANT);
expect(params).to.have.property("scope", AUTH_SCOPE);
expect(params).to.have.property("username", user.email);
expect(params).to.have.property("password", user.password);
});
cy.wait("@route_login").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");
});
⬜️ performance
⬜️ reliability
✅ maintainability
✅ assertions quality
Otherwise: we spend more time than needed to find why a test failed.
cy.getByPlaceholderText("Username").type(user.email);
cy.getByPlaceholderText("Password").type(user.password);
cy.getByText("Login").click();
cy.wait("@route_login").then(xhr => {
const params = xhr.request.body;
expect(params).to.have.property("grant_type", AUTH_GRANT);
expect(params).to.have.property("scope", AUTH_SCOPE);
expect(params).to.have.property("username", user.email);
expect(params).to.have.property("password", user.password);
});
cy.getByText("Welcome back").should("exist");
cy.url().should("be", "/home");
⬜️ performance
⬜️ reliability
✅ maintainability
✅ assertions quality
Otherwise: we waste time relaunching the test to discover what's wrong with some data.
// wrong
expect(xhr.request.body.username === user.email).to.equal(true);
// right
expect(xhr.request.body).to.have.property("username", user.email);
Failure feedback
Go to the login page
Write username and password
Click "Login"
Check both request and response payloads
Wait for the network request
Show a success feedback
Redirect to the home page
⬜️ performance
✅ reliability
✅ maintainability
✅ assertions quality
Otherwise: we break the tests more often than needed.
cy.getByText("Welcome back").should("exist");
cy.url().should("be", "/home");
cy.getByText("Welcome back").should("exist");
cy.url().should("be", "/home");
cy.getByText("Welcome back").should("exist");
cy.url().should("not.include", "/login");
⬜️ performance
✅ reliability
✅ maintainability
✅ assertions quality
Otherwise: we break the tests with minor changes.
cy.getByText("Welcome back").should("exist");
cy.url().should("not.include", "/login");
import { LOGIN } from "@/navigation/routes";
// ...
cy.url().should("not.include", LOGIN);
⬜️ performance
✅ reliability
✅ maintainability
✅ assertions quality
Otherwise: we are nagged by failing tests every time we make minor changes to your app.
Go to the login page
Write username and password
Click "Login"
Check both request and response payloads
Wait for the network request
Check the Redux state
Redirect to the home page
Show a success feedback
Check the Redux state
// Cypress example
cy.window().then((win: any) => {
const appState = win.globalReactApp.store.getState();
expect(appState.auth.usernme).to.be(user.email);
});
⬜️ performance
✅ reliability
✅ maintainability
⬜️ assertions quality
Otherwise: we lose the type guard safety while writing tests.
⬜️ performance
✅ reliability
✅ maintainability
✅ assertion balance
Otherwise: we will debug someone else's faults or miss any important update that can affect our front-end.
That highlights the changes in a text document
⬜️ performance
✅ reliability
⬜️ maintainability
⬜️ assertion balance
Otherwise: we block the back-enders from running them and check themselves they haven't broken anything.
"scripts": {
"test:e2eonly": "CYPRESS_TESTS=e2e npm-run-all build:staging test:cypress"
}
✅ performance
✅ reliability
⬜️ maintainability
⬜️ assertion balance
Otherwise: we won't leverage the power of an automated tool.
Add the username input field ⌨️
Try it in the browser 🕹
Add the password input field ⌨️
Try it in the browser 🕹
Add the submit button ⌨️ ➡️ 🕹
Add the XHR request ⌨️ ➡️ 🕹
Manage the error case ⌨️ ➡️ 🕹
Check we didn't break anything 🕹🕹🕹🕹
Fix it ⌨️ ⌨️ ⌨️ ➡️ 🕹🕹🕹🕹
Check again 🕹🕹🕹🕹
Fix it ⌨️ ➡️ 🕹🕹🕹🕹
Fill the username input field 🤖
Fill the password input field 🤖
Click the submit button 🤖
Check the XHR request/response 🤖
Check the feedback 🤖
Re-do it for every error flow 🤖
Add the username input field ⌨️
Make Cypress fill the username input field 🤖
Add the password input field ⌨️
Make Cypress fill the password input field 🤖
Add the submit button ⌨️ ➡️ 🤖
Add the XHR request ⌨️ ➡️ 🤖
Manage the error case ⌨️ ➡️ 🤖
Check we didn't break anything
Fix it
• https://www.blazemeter.com/blog/top-15-ui-test-automation-best-practices-you-should-follow • https://medium.freecodecamp.org/why-end-to-end-testing-is-important-for-your-team-cb7eb0ec1504 • https://blog.kentcdodds.com/write-tests-not-too-many-mostly-integration-5e8c7fff591c • https://hackernoon.com/testing-your-frontend-code-part-iii-e2e-testing-e9261b56475 • https://gojko.net/2010/04/13/how-to-implement-ui-testing-without-shooting-yourself-in-the-foot-2/ • https://willowtreeapps.com/ideas/how-to-get-the-most-out-of-ui-testing • http://www.softwaretestingmagazine.com/knowledge/graphical-user-interface-gui-testing-best-practices/ • https://www.slideshare.net/Codemotion/codemotion-webinar-protractor • https://frontendmasters.com/courses/testing-javascript/introducing-end-to-end-testing/ • https://medium.com/welldone-software/an-overview-of-javascript-testing-in-2018-f68950900bc3 • https://medium.com/yld-engineering-blog/evaluating-cypress-and-testcafe-for-end-to-end-testing-fcd0303d2103
🙏 Please, give me any kind of feedback 😊
By Stefano Magni
In April 2019 I had a talk for the Milano Frontend community (https://www.meetup.com/it-IT/milano-front-end/events/256620617/). The talk aimed to share my own experience with UI testing 🤙You can find the recording (in Italian) at https://www.facebook.com/milanofrontendmeetup/videos/2312725798938924/
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.