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

Made with Slides.com