React Testing Library: Jak pisać łatwe w utrzymaniu testy?

Opublikowano: 19.10.2023 - tagi: JavaScript React Testowanie Test

Czytelność testów

"Kod częściej się czyta, niż pisze", "Kod nie jest zapisany w skale". Te spostrzeżenia odnoszą się także do testów. W końcu testy to też kod!

W takim razie warto zastanowić się, jak można zwiększyć ich czytelność?

Nie tylko czytelność. Jak pisać testy łatwiejsze w utrzymaniu?

Lepsze testy — krok po kroku

Zacznijmy od przykładu.

Komponent:

export function MyComponent() {
    const [state, setState] = useState(0);

    return (
        <>
            <div>
                {state}
            </div>
            <div>
                <button onClick={() => setState((prevState: number) => prevState + 1)}>Increment</button>
                <button onClick={() => setState((prevState: number) => prevState - 1)}>Decrement</button>
            </div>
        </>
    )
}

Lepsze testy: Krok 1

const { getByRole, getByText } = screen;

test('should increment counter', async () => {
    // given
    render(<MyComponent />);
    const btnIncrement = getByRole('button', { name: "Increment"});

    // when
    await userEvent.click(btnIncrement);

    // then
    expect(getByText('1')).toBeInTheDocument();
});

test('should decrement counter', async () => {
    // given
    render(<MyComponent />);
    const btnDecrement = getByRole('button', { name: "Decrement"});

    // when
    await userEvent.click(btnDecrement);

    // then
    expect(getByText('-1')).toBeInTheDocument();
});

Co można zrobić, żeby test był bardziej czytelny?

Lepsze testy: Krok 2

Za każdym razem trzeba wywoływać funkcję: render().

Wrzućmy ją do funkcji: renderComponent:

const renderComponent = () => render(<MyComponent />);

Refaktoring testu:

test('should increment counter', async () => {
    // given
    renderComponent();
    const btnIncrement = getByRole('button', { name: "Increment"});
    
		...
});

test('should decrement counter', async () => {
    // given
    renderComponent();
    const btnDecrement = getByRole('button', { name: "Decrement"});
		
		...
});

Jest odrobinę lepiej.

Czas na następny krok.

Lepsze testy: Krok 3

Żeby testować element, potrzebujesz referencji do elementów znajdujących się w komponencie.

Pisanie zapytać w stylu:

const btnIncrement = getByRole('button', { name: "Increment"});

Jest męczące.

A co gdyby funkcja renderująca testowany komponent (renderComponent) zwracała referencje do elementów, które posiada?

Nowa wersja renderComponent:

const renderComponent = () => {
    render(<MyComponent />);

    const btnIncrement = getByRole('button', { name: "Increment"});
    const btnDecrement = getByRole('button', { name: "Decrement"});
    const userClicksOn = async (element) => await userEvent.click(element);
    
    return {
        btnIncrement,
        btnDecrement,
        userClicksOn
    }
}

I zaktualizowane testy:

test('should increment counter', async () => {
    // given
    const { btnIncrement, userClicksOn } = renderComponent();

    // when
    await userClicksOn(btnIncrement);

    // then
    expect(getByText('1')).toBeInTheDocument();
});

test('should decrement counter', async () => {
    // given
    const { btnDecrement, userClicksOn } = renderComponent();

    // when
    await userClicksOn(btnDecrement);

    // then
    expect(getByText('-1')).toBeInTheDocument();
});

Podsumowanie

Takie podejście do pisania testów sprawia, że rozwiązujesz kilka problemów:

  1. Centralizacja — Ewentualne zmiany, będziesz robić w funkcji renderującej komponent. Nie musisz "skakać" po wielu testach, żeby coś naprawić.
  2. Testy są bardziej czytelne.