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
With
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);
});
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.
Otherwise: we write a lot of E2E tests but remember that they are not practical at all 😠
A Cypress screenshot
They are slow, even because we need to clear data before and after the tests.
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");
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();
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
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.server();
cy.route({
method: "POST",
url: '/auth/token',
response: 'authentication-success.json'
}).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.server();
cy.route({
method: "POST",
url: '/auth/token',
response: 'authentication-success.json'
}).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 (I 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
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");
});
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");
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
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");
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);
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);
});
Otherwise: we lose the type guard safety while writing tests.
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
Otherwise: we block the back-enders from running them and check themselves they haven't broken anything.
"scripts": {
"test:e2eonly": "cypress run --spec \"cypress/**/*.e2e.*\""
}
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
🙏 Please, give me any kind of feedback 😊
Next talks: Working Software and Voxxed Days Ticino
By Stefano Magni
In June 2019 I had a talk for the FEVR community (http://www.fevr.it/eventi/2019/06/ui-testing-best-practices/). The talk aimed to share my own experience with UI testing 🤙
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.