lecture 6 of 150 completed

One test, many datasets

Ten copies of the same test with different values is a maintenance problem. Loop over the data instead, and keep the failures readable.

A card validation test I inherited had eleven copies of the same test. Same steps every time, only the card number changed. When the error message was reworded, I edited eleven blocks and missed one.

The fix is to describe the cases as data, and let one test body handle all of them.

Start with the cases in a plain array:

const invalidCards = [
  { number: '4111111111111112', reason: 'fails the checksum' },
  { number: '1234567812345678', reason: 'is not a real card number' },
  { number: '411111111111', reason: 'is too short' },
];

Then generate one test per case:

describe('checkout rejects bad cards', () => {
  invalidCards.forEach(({ number, reason }) => {
    it(`rejects a card that ${reason}`, () => {
      cy.visit('/checkout');
      cy.get('[data-testid="card-number"]').type(number);
      cy.get('[data-testid="pay"]').click();
      cy.get('[data-testid="card-error"]').should('be.visible');
    });
  });
});

Read what that produces. Not one test that loops, but three separate tests, each with its own name. That difference matters more than it looks.

Each case fails on its own. If the short-card case breaks, one test goes red and the other two stay green. A single test with a loop inside stops at the first failure and hides the rest.

The name says which case broke. "rejects a card that is too short" tells you the bug without opening the file. This is why the reason belongs in the data and in the test title.

For bigger datasets, move them out of the spec into a fixture file. cypress/fixtures/cards.json:

[
  { "number": "4111111111111112", "reason": "fails the checksum" },
  { "number": "411111111111", "reason": "is too short" }
]

Load it with cy.fixture. One thing catches people here, and it comes straight from the command queue: cy.fixture is asynchronous, so the data is not available while your test file is being read. Generating tests from a fixture at the top level does not work the way you expect. The reliable pattern is to import the JSON directly instead:

import cards from '../fixtures/cards.json';

cards.forEach((card) => {
  it(`rejects a card that ${card.reason}`, () => { /* ... */ });
});

Two limits worth knowing, because data-driven testing is easy to overuse.

It multiplies runtime. Twenty datasets means twenty browser tests. If the logic being checked is pure calculation, a unit test covering twenty cases runs in milliseconds and is the better tool. Use the browser for cases where the screen differs.

Do not hide different behaviour in one loop. If a case needs its own extra step or a different assertion, it is not the same test. Write it separately rather than adding an if inside the loop.

Remember this: data in an array, one generated test per case, and the reason in the test name.

Reference: Cypress fixtures.