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.

React Testing Library: Jak sprawdzić kolejność wyświetlanych elementów na liście?

Opublikowano: 17.10.2023 - tagi: JavaScript React Testowanie Test Lista

Jaka kolejność na liście?

Można to sprawdzić na kilka sposobów.

Najpierw kod przykładowej aplikacji:

import { useState } from "react";

export function MyComponent() {
    const [tasks, setTasks] = useState(['Task 1', 'Task 2', 'Task 3']);
    const [task, setTask] = useState('');

    const onChangeTask = (e) => {
        setTask(e.target.value)
    };

    const onAddTask = (e) => {
        e.preventDefault();

        setTasks([task, ...tasks]);
        setTask('');
    }

    return (
        <>
            <form onSubmit={onAddTask}>
                <label htmlFor="newTask">Task title:</label>
                <input type="text" id="newTask" value={task} onChange={onChangeTask} />
                <button>Add</button>
            </form>
            <ul>
                {
                    tasks.map((task, index) => (
                        <li key={index}>
                            {task}
                        </li>
                    ))
                }
            </ul>
        </>
    )
}

Przykłady

Sposób 1: toMatchInlineSnapshot

Jest udostępnia funkcję toMatchInlineSnapshot testowania struktury DOM.

import { render, screen } from '@testing-library/react';
import { MyComponent} from "./MyComponent";

test('should check order of list items', async () => {
    // given
    const { getByRole } = screen;
    render(<MyComponent />);

    // when
    const list = getByRole('list');

    // then
    expect(list).toMatchInlineSnapshot(`
        <ul>
          <li>
            Task 1
          </li>
          <li>
            Task 2
          </li>
          <li>
            Task 3
          </li>
        </ul>
    `)
});

Jeśli element listy jest dość rozbudowany takie testowanie może być uciążliwe. Ale można to obejść w prosty sposób!

Wywołaj najpierw test z toMatchInlineSnapshot bez żadnych argumentów:

expect(list).toMatchInlineSnapshot();

po chwili zostanie dodany argument do toMatchInlineSnapshot z listą elementów.

Sposób 2: Tablica elementów

Możesz też pobrać nazwy elementów i umieścić je w tablicy:

import { render, screen } from '@testing-library/react';
import { MyComponent} from "./MyComponent";

test('should check order of list items', async () => {
    // given
    const { getAllByRole } = screen;
    render(<MyComponent />);

    // when
    const listItems = getAllByRole('listitem').map((item) => item.textContent);

    // then
    expect(listItems).toEqual(['Task 1', 'Task 2', 'Task 3']);
});

React Testing Library: Within

Opublikowano: 14.10.2023 - tagi: JavaScript React Testowanie Test Lista

Do czego służy within?

React Testing Library udostępnia funkcję o nazwie within, która pozwala na pisanie zapytań tylko w obrębie danego fragmentu z DOM.

Załóżmy, że mamy listę i każdy element listy składa się z innych elementów. Na przykład:

<ul>
	<li>
		<h2>Item 1</h2>
		<button type="button">Edit</button>
		<button type="button">Remove</button>
	</li>
	<li>
		<h2>Item 2</h2>
		<button type="button">Edit</button>
		<button type="button">Remove</button>
	</li>
</ul>

Teraz chcemy napisać test, który sprawdzi usuwanie elementu z listy. Jak pobrać referencję do przycisku: "Remove"? Można każdemu przycisku nadać unikalne id za pomocą data-testid.

Można też użyć funkcji within wystarczy, że przekażemy do niej pojedynczy element listy. Następnie za pomocą zapytania zdobyć referencję do przycisku.

Przykład

Poniżej znajduje się prosty kod TODO listy.

Komponent:

import {useState} from "react";

export function MyComponent() {
    const [tasks, setTasks] = useState(['Task 1', 'Task 2', 'Task 3']);
    const [task, setTask] = useState('');

    const onChangeTask = (e) => {
        setTask(e.target.value)
    };

    const onAddTask = (e) => {
        e.preventDefault();

        setTasks([task, ...tasks]);
        setTask('');
    }

    const onRemoveTask = (taskToRemove) => {
        setTasks(tasks.filter(currentTask => currentTask != taskToRemove))
    }

    return (
        <>
            <form onSubmit={onAddTask}>
                <label htmlFor="newTask">Task title:</label>
                <input type="text" id="newTask" value={task} onChange={onChangeTask} />
                <button>Add</button>
            </form>
            <ul>
                {
                    tasks.map((task, index) => (
                        <li key={index}>
                            <span data-testid="name">{task}</span>
                            <button type="button" onClick={() => onRemoveTask(task)}>Remove</button>
                        </li>
                    ))
                }
            </ul>
        </>
    )
}

I chcesz stestować usuwanie zadania (element listy).

Test:

import {render, screen, within} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {MyComponent} from "./MyComponent";

test('should remove task from list', async () => {
    // given
    const { getAllByRole, queryByText } = screen;
    render(<MyComponent />);
    const listItems = getAllByRole('listitem').map((item) => ({
        name: within(item).getByTestId('name').textContent,
        removeBtn: within(item).getByRole('button', { name: 'Remove' }),
    }));
    const removeTaskBtn = listItems.find((item) => item.name === 'Task 1').removeBtn;

    // when
    await userEvent.click(removeTaskBtn);

    // then
    expect(queryByText('Task 1')).not.toBeInTheDocument();
});

Najpierw iterujemy po wszystkich elementach listy za pomocą getAllByRole. Tworzona jest tablica, która zawiera nazwę elementu i referencję do przycisku do usuwania zadania.

Przy każdej iteracji korzystamy z funkcji within. Przekazywany jest do niej pojedynczy element listy. Dzięki temu możesz napisać zapytanie: Pobierz mi przycisk o nazwie "Remove". Nie ważne, że na liście znajduje się więcej niż jeden taki przycisk o tej samej nazwie!


JavaScript: Jak pobrać n pierwszych elementów tablicy?

Opublikowano: 10.10.2023 - tagi: JavaScript Tablica Element

N pierwszych elementów

Mamy tablicę:

const items = [1, 2, 3, 4, 5];

I potrzebujesz pobrać trzy pierwsze elementy tablicy.

Jak można to zrobić?

Sposób 1: slice

const itemsB = items.slice(0, 3);
console.log(itemsB); // [1, 2, 3]

Sposób 2: length

items.length = 3;
console.log(items); // [1, 2, 3]

Swift: Computed property

Opublikowano: 07.10.2023 - tagi: Swfit Atrybut Zmienna

Czym jest computed property?

Zmienna, która jest computed property działa na takiej zasadzie, że nie przechowuje wartości, tylko jest ona obliczana przy próbie odczytu.

Domyślnie udostępnia getter do pobierania wartości. Opcjonalnie można przypisać także setter jeśli chcemy przypisywać zmiennej wartość.

Computed property można użyć w klasie, strukturze, enum lub nawet poza wymienionymi.

Najprościej można stworzyć computed property, w taki sposób:

struct User {
	var firstName: String;
	var lastName: String;
	var fullName: String {
		return firstName + " " + lastName
	}
}

var user = User(firstName: "James", lastName: "Bond");
print(user.fullName) // James Bond

Zmienna fullName jest typu: computed property. W momencie odwołania się do niej w kodzie jej wartość jest ustalana "w locie".

Zapis dla fullName można jeszcze skrócić:

var fullName: String {
	firstName + " " + lastName
}

O ile w Swift nie jest wymagane określanie typu danych zmiennej lub stałej, to w przypadku computed property taki typ musi zostać podany.

Getter

Pobieranie wartości dla computed property można zdefiniować jeszcze w inny sposób:

var fullName: String {
	get {
		firstName + " " + lastName
	}
}

Tylko po co tak pisać skoro można obyć się bez get?

Istnieje możliwość dodania setter'a dla computed property.

Setter

Jeśli chcesz zmienić wartość computed property musisz określić mu setter. Bez tego próba przypisania wartości spowoduje błąd.

struct Hotel {
  var rooms: Int
	var allocated: Int = 0
	var roomsRemaining: Int {
		get {
			rooms - allocated
		}
		set {
			rooms = allocated + newValue
		}
	}
}

var hotel = Hotel(rooms: 20)
print(hotel.roomsRemaining) // 20
hotel.allocated = 5
print(hotel.roomsRemaining) // 15
hotel.roomsRemaining = 10 
print(hotel.rooms) // 15
print(hotel.roomsRemaining) // 10

Kiedy tego użyć?

Jeśli tworzysz zmienną, która zależy od wartości innej zmiennej, jest to sygnał, że warto zadeklarować ją jako computed property.

Z drugiej strony należy uważać, bo możemy przesadzić. Wartość computed property jest obliczana za każdym razem, gdy pobierana jest z niej wartość. Więc jeśli umieścisz w takiej zmiennej kosztowne operacje, zapłacisz spadkiem wydajności aplikacji.