Na wstępie muszę zacząć od problemu niestabilnych testów. Nie dopuszczaj do udawania, że niektóre testy takie są i wystarczy powtórzyć test. W dłuższym terminie doprowadzi to do spadku zaufania zespołu do testów, obniżenia jakości procesu pisania i utrzymywania testów i potencjalnego przepuszczenia realnych błędów na produkcję.
Jeśli test jest niestabilny, zespół musi znaleźć przyczynę i naprawić ten problem. Dopuszczanie flaky tests prowadzi do degradacji jakości kodu i zmniejszenia wartości biznesowej produktu, szczególnie gdy brzegowy scenariusz wystąpi na produkcji.
Jak rozpoznać źle napisany komponent, który ciężko testować?
Komponent, który trudno testować zwykle charakteryzuje się jednym lub więcej z poniższych składowych: potrzeba długiego setupu testu, zależność od wielu dostawców lub kontekstów, konieczność mockowania wielu bibliotek, lekkomyślne używanie typu any lub wartości hardcodowanych, aby test zakończył się sukcesów.
Jak wygląda dobry test?
Struktura Arrange, Act, Assert (czasem nazywana też Given–When–Then) jest wzorcem organizowania testów. Jej celem jest rozdzielenie przygotowania testu, wykonania akcji i weryfikacji wyniku w trzech wyraźnych krokach, co poprawia czytelność, przewidywalność i łatwość utrzymania testów. Nazwa testu powinna opisywać zachowanie. Jeżeli jest to możliwe to pojedynczy test powinien skupiać się na jednej akcji i jednej weryfikacji, unikając sytuacji, gdy część asercji pojawia się we wcześniejszych krokach.
test('shows greeting message when user is logged in', () => {
// Arrange
const mockUser = { name: 'Test user' };
render(<UserGreeting user={mockUser} />);
// Act
const greeting = screen.getByRole('heading');
// Assert
expect(heading).toHaveTextContent(mockUser.name);
});
Oddziel logikę od UI
Logika biznesowa powinna być wyciągnięta z komponentu do osobnych funkcji lub hooków tak, aby można było testować je bez renderowania DOM.
// złe podejście
export const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return <button onClick={increment}>Count: {count}</button>;
}
// lepsza separacja
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount(c => c + 1);
return { count, increment };
}
export const Counter = () => {
const { count, increment } = useCounter();
return <button onClick={increment}>Count: {count}</button>;
}
// przykład testu
test('useCounter hook increments', () => {
const { result } = renderHook(() => useCounter(1));
act(() => { result.current.increment(); });
expect(result.current.count).toBe(2);
});
Twórz komponenty “smart” oraz “dumb”
Smart komponenty (komponenty złożone) zarządzają logiką i stanem. Mogą pobierać dane, wykonywać side-effecty, sterować propsami i przekazywać je do innych komponentów. Dumb komponenty (nazywane bezstanowymi, prostymi) są odpowiedzialne tylko za prezentację UI na podstawie propsów, bez logiki biznesowej czy pobierania danych. Podział komponentów istotnie wspiera utrzymanie testów, ale również całej architektury aplikacji (np. coupling).
Wybieraj sematyczne znaczniki nad data-testid
Użycie selektorów zgodnych z zasadami WCAG takich jak getByRole, tworzy testy bardziej odporne na zmiany implementacji.
// unikaj
expect(getByTestId('submit-btn')).toBeInTheDocument();
// rekomendowane
expect(getByRole('button')).toBeInTheDocument();
Kompozycja nad warunkami
Unikaj zbyt wielu warunków if w komponencie, zamiast tego rozbij przypadki na osobne komponenty.
// unikaj
export const Status = ({ state }: StatusType) => {
if (state === 'loading') return <Loading />;
if (state === 'error') return <Error />;
return <Data />;
}
Każdy komponent można testować oddzielnie, redukując liczbę wariantów w jednym teście.
export const Loading = () => <Loading />;
export const ErrorPage = () => <Error />;
export const SuccessPage = ({data}) => <Data data={data} />;
Pamiętaj, że testowanie pojedynczych komponentów nie wyklucza testów integracyjnych. Rozbicie logiki na mniejsze komponenty upraszcza testy, ale nadal musisz sprawdzić jak elementy aplikacji współpracują ze sobą w szerszym kontekście. Przy teście integracyjnym sprawdzisz czy komponent renderuje się odpowiednio dla danego stanu.
Nie testuj tylko happy paths
Testy muszą obejmować również sytuacje skrajne jak błędy, brak danych, timeouty itp. Testowanie tylko pozytywnego scenariusza daje fałszywe poczucie bezpieczeństwa, bo większość błędów pojawia się przy scenariuszach brzegowych.
Mockuj tylko to, co trzeba
Mockowanie zależności w testach służy izolacji, ale mockuj wyłącznie to, co realnie destabilizuje testy, np. sieciowe wywołania API lub zależności zewnętrznych usług.
Powrót do artykułów