Front-end tech leader
Stefano Magni
// ❌ don't
test('', () => {
cy.findByLabel('Key').type(/* ... */);
});
// ✅ do
test('', () => {
cy.log('**--- Set the key of the first key/value header**');
cy.findByLabel('Key').type(/* ... */);
});
Tell what you are doing
// ❌ don't
test('', () => {
expect(subject({ foo: 'bar' })).toEqual({ baz: 'qux' });
});
// ✅ do
test('', () => {
const params: Params = { foo: 'bar' }; // <-- if Params change, TS throws
const expected: Result = { baz: 'qux' }; // <-- if Params change, TS throws
expect(subject(params)).toEqual(expected);
});
Get the good parts of TypeScript
// ❌ don't
test('', () => {
// action
// action
// action
// action
expect(/*...*/);
});
// ✅ do
test('', () => {
// action
// action
expect(/*...*/);
// action
// action
expect(/*...*/);
});
Alternate actions and assertions
// ❌ don't
test('', () => {
const element = screen.getByTestId('new-db-name');
// ...
});
// ✅ do
test('', () => {
const element = screen.getByLabel('New Database name');
// ...
});
Prefer Testing Library's selectors over data-testids
// ❌ don't
test('', () => {
cy.findByLabel('New Database name').type(/* ... */);
cy.findByLabel('New Database type').type(/* ... */);
cy.findByLabel('New Table name').type(/* ... */);
cy.findByLabel('New Table type').type(/* ... */);
});
// ✅ do
test('When the name is correct, should allow creating the database', () => {
cy.findByTextId('new-database-section').within(() => {
cy.findByLabel('New Database name').type(/* ... */);
cy.findByLabel('New Database type').type(/* ... */);
});
cy.findByTextId('new-table-section').within(() => {
cy.findByLabel('New Table name').type(/* ... */);
cy.findByLabel('New Table type').type(/* ... */);
});
});
Use test-ids for sections
// ❌ don't
test('', () => {
cy.get('textarea').eq(0).type(/* ... */);
// ...
});
// ✅ do
test('', () => {
cy.get('textarea').eq(0).as('graphiQlTextarea');
cy.get('@graphiQlTextarea').type(/* ... */);
// ...
});
Use clear selectors
// ❌ don't
export const expectNotification = (
{
type,
title,
message,
}: {
type: 'success' | 'error';
title: string;
message?: string;
},
timeout = 10000
) => {
const types: Record<string, string> = {
error: '.notification-error',
success: '.notification-success',
};
const el = cy.get(types[type], { timeout });
el.should('be.visible');
el.should('contain', title);
if (message) el.should('contain', message);
};
test('', () => {
// action
expectNotification({type: 'success', title: 'Table created!'}) // <-- if it fails something inside here... Good luck at debugging it
});
// ✅ do
function expectSuccessNotification = (title: string) {
cy.get('.notification-success')
.should('be.visible')
.should('contain', title)
}
test('', () => {
// action
expectSuccessNotification('Table created!') // <-- less complex, more vertical, way dmore debuggable
});
Reduce the abstraction
// ❌ don't
test('', () => {
// ...
expect(result).toMatchSnapshot();
});
// ✅ do
test('', () => {
// ...
expect(result).toHaveProperty('milk', '2');
expect(result).toHaveProperty('eggs', '10');
});
Avoid snapshot testing
// ❌ don't
test('', () => {
// ...
expect(result).toHaveProperty('milk', '2');
expect(result).toHaveProperty('eggs', '10');
expect(result).toMatchInlineSnapshot(`{
milk: 2,
eggs: 10,
}`);
});
// ✅ do
test('', () => {
// ...
expect(result).toHaveProperty('milk', '2');
expect(result).toHaveProperty('eggs', '10');
// Checks that no other properties exist, every added property must be considered an error
expect(result).toMatchInlineSnapshot(`{
milk: 2,
eggs: 10,
}`);
});
If you use snapshot testing, explain why
// ❌ don't
test('When the name is correct, should allow creating the database', () => {
// Happy path testing
});
test('When the name is not correct, should not allow creating the database', () => {
// Error path testing
});
test('When the name is empty, should not allow creating the database', () => {
// Error path testing
});
// ✅ do
test('When the name is correct, should allow creating the database', () => {
// Happy path testing
});
Do not use E2E test apart for the happy paths
// ❌ don't
test('', () => {
// create
});
test('', () => {
// edit
});
// ✅ do
test('', () => {
// create
// edit
});
// or...
// ✅ do
test('', () => {
// create
});
test('', () => {
// create if it does not exist
// edit
});
Tests must not depend on execution order
// ❌ don't
test('', () => {
cy.get('button').click();
cy.wait(10000);
expectSuccessNotification('Database created!');
});
// ✅ do
test('', () => {
cy.intercept('POST', 'http://localhost:8080/createdb').as('createDbRequest');
cy.get('button').click();
cy.wait('@createDbRequest');
expectSuccessNotification('Database created!');
});
Do not make your tests sleeping
// ✅ do
describe('getStatusForForecast', () => {
it.each`
homeScore | awayScore | estimatedHome | estimatedAway | expectedStatus
${2} | ${1} | ${2} | ${1} | ${Forecast_Status_Enum.Perfect}
${2} | ${1} | ${3} | ${0} | ${Forecast_Status_Enum.Partial}
${2} | ${1} | ${2} | ${4} | ${Forecast_Status_Enum.Miss}
${1} | ${2} | ${1} | ${2} | ${Forecast_Status_Enum.Perfect}
${1} | ${2} | ${0} | ${3} | ${Forecast_Status_Enum.Partial}
${1} | ${2} | ${1} | ${4} | ${Forecast_Status_Enum.Partial}
${1} | ${2} | ${3} | ${2} | ${Forecast_Status_Enum.Miss}
${0} | ${0} | ${0} | ${0} | ${Forecast_Status_Enum.Perfect}
${0} | ${0} | ${1} | ${1} | ${Forecast_Status_Enum.Partial}
${0} | ${0} | ${1} | ${2} | ${Forecast_Status_Enum.Miss}
${1} | ${1} | ${0} | ${0} | ${Forecast_Status_Enum.Partial}
${1} | ${1} | ${1} | ${1} | ${Forecast_Status_Enum.Perfect}
${1} | ${1} | ${2} | ${2} | ${Forecast_Status_Enum.Partial}
`(
'should, given a $homeScore:$awayScore match and a $estimatedHome:$estimatedAway forecast, return $expectedStatus as a status',
({ homeScore, awayScore, estimatedHome, estimatedAway, expectedStatus }) => {
expect(
getStatusForForecast({ homeScore, awayScore }, { estimatedAway, estimatedHome, profileId: '', matchId: '' }),
).toEqual(expectedStatus);
},
);
});
test.each special cases
// Jest example
// ❌ don't
test('', () => {
// ...
expect(mock.calls[0]).toEqual(['foo', 'bar']);
});
// ✅ do
test('', () => {
// action
expect(mock).toHaveBeenCalledWith('foo', 'bar');
});
// Cypress example
// ❌ don't
it('', () => {
// ...
expect(response.body).to.have.property('result_type');
});
// ✅ do
it('', () => {
// action
expect(response.body).to.have.property(
'result_type',
'The response does not contain the result_type' // <-- will be printed in case of error
);
});
Opt for speaking assertions and errors
// ❌ don't
test('', () => {
// action
// action
expect(/*...*/);
// action
// action
// ??? What is the expected behaviour?
});
// ✅ do
test('', () => {
// action
// action
expect(/*...*/);
// action
// action
expect(/*...*/);
});
Always close the test with an assertion
// ❌ don't
- cypress/e2e/databases/test.ts
- src/features/databases/components/Create.spec.tsx
// ✅ do
- cypress/e2e/databases/crud.e2e.ts
- src/features/databases/components/Create.test.tsx
Respect naming conventions
Front-end tech leader
Stefano Magni