Testing

Front-end tech leader

Best Practices

Stefano Magni

Objective / Subjective

// ❌ 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

Thank you! ❤️

Front-end tech leader

Stefano Magni

Testing Best Practices - ReactJs Milano 2022

By Stefano Magni

Testing Best Practices - ReactJs Milano 2022

Cosa succeede se i test che scriviamo nel nostro team/azienda non sono facilmente leggibili (o addirittura "decifrabili")? Succede che i test stessi, invece che garantire confidenza sul fatto che il codice e l'app funzionino, aggiungono carico cognitivo invece che toglierlo, non permettono di capire cosa il codice e l'app dovrebbero fare perché sono piú complessi del codice stesso che dovrebbero testare, non danno confidenza nel fare refactoring, sono obsoleti rispetto al codice che devono testare Senza parlare che quando falliscono, non permettono di identificare il problema alla base del fallimento - non permettono di capire se é il codice che non funziona o sono i test stessi che non funzionano, ci portano ad accettare i continui fallimenti Riasumendo, il costo di avere test (CI piú lente, sviluppo piú lento, librerie esterne da mantenere aggiornate) non é ripagato dal vantaggio di averli. Senza dimenticarsi che tutti i punti di cui sopra frustrano non poco il team Durante il talk condivideró con voi le best practice generiche che ho imparato nel tempo per evitare di incappare nei problemi sopracitati, applicabili ad ogni tipo di test (Unit test, Component test, Integration test, Story test, E2E test).

  • 1,673