React Testing Library: Jak mockować requesty?

Opublikowano: 03.10.2023 - tagi: JavaScript React Testowanie Test Mock Request

Testowanie requestów

Pisanie testów dla funkcjonalności, które wykorzystują requesty może być irytujące. W zależności, jak taki test napiszesz.

Możesz mockować funkcje biblioteki, którą używasz do obsługi reqestów.

Albo przyjąć inne podejście: mockujesz tylko same requesty. To znaczy: jeśli w kodzie zostanie wywołany GET /api/user, ustalasz, że ma zwrócić konkretne dane i tyle!

Na pierwszy rzut oka oba podejścia mogą wydawać się podobne, ale tak nie jest.

W pierwszym podejściu skupiasz się niepotrzebnie na szczegółach: mockujesz konkretną bibliotekę i metodę. Co jeśli w przyszłości będziesz chciał zmienić bibliotekę do obsługi requestów? Po zmianie testy oczywiście się załamią.

Znacznie lepszym rozwiązaniem jest mockowanie samych requestów. Dzięki temu skupiasz się na testowaniu tego, co w rzeczywistości jest ważne: na testowaniu funkcjonalności, a nie szczegółach implementacji

Więc jak mockować requesty?

Jest kilka gotowych bibliotek. W tym wpisie opiszę, jak działa Nock.

Nock

Jest to prosta biblioteka, która pozwala w banalny sposób mockować requesty.

Instalacja

Żeby zainstalować nock'a wywołaj komendę:

npm i nock -d

Jak korzystać z Nock?

Poniżej przykład mockowania request'a typu GET:

import nock from 'nock';

nock(host)
	.get(endpoint)
	.reply(200, response);

Gdzie odpowiednio:

  1. host — To adres do naszego API.
  2. endpoint — To, co chcemy zamokować.
  3. response — Możemy też określi, co zwróci endpoint po jego wywołaniu.

Przykładowa aplikacja

Do stestowania jest: wczytywanie listy zadań oraz zapis nowego zadania.

Do obsługi requestów w tym przykładzie korzystam z biblioteki axios.

Komponent:

import {useState, useEffect} from "react";
import axios from "axios";

interface Task {
    id: number;
    title: string;
};

export function MyComponent() {
    const [tasks, setTasks] = useState<Task[]>([]);
    const [task, setTask] = useState<string>('');

    useEffect(() => {
        axios.get('/api/task').then(res => setTasks(res.data));
    }, []);

    const onAddTask = () => {
        axios.post('/api/task', { task }).then(res => {
            const { id } = res.data;
            setTask('');
            setTasks([{ id, title: task }, ...tasks]);
        })
    }
    return (
        <>
            <div>
                <label htmlFor="task">Task title:</label>
                <input name="task" id="task" value={task} onChange={(e) => setTask(e.target.value)} />
                <button onClick={onAddTask}>Add</button>
            </div>
            <ul>
                {
                    tasks.map((task: Task) => <li key={task.id}>{task.title}</li>)
                }
            </ul>
        </>
    )
}

Testowanie GET: wczytywanie listy

Test:

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

test('should display list of tasks', async () => {
    // given
    nock('http://localhost')
        .get('/api/task')
        .reply(200, [
            {
                id: 1,
                title: "Task 1"
            },
            {
                id: 2,
                title: "Task 2"
            },
            {
                id: 3,
                title: "Task 3"
            }
        ]);

    // when
    render(<MyComponent />)

    // then
    expect(
        await findByText('Task 1')
    ).toBeInTheDocument();

    expect(
        await findByText('Task 2')
    ).toBeInTheDocument();

    expect(
        await findByText('Task 3')
    ).toBeInTheDocument();
});

Testowanie POST: zapis danych

Test:

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

test('should add new task', async () => {
    // given
    nock('http://localhost')
        .get('/api/task')
        .reply(200, [
            {
                id: 1,
                title: "Task 1"
            },
            {
                id: 2,
                title: "Task 2"
            },
            {
                id: 3,
                title: "Task 3"
            }
        ])
        .post('/api/task')
        .reply(200, {
            id: 4
        });
    render(<MyComponent />);
    const taskInput = getByLabelText('Task title:');
    await userEvent.type(taskInput, 'Task 4');
    const taskAddBtn = getByRole('button', { name: 'Add' });

    // when
    await userEvent.click(taskAddBtn);

    // then
    expect(
        await findByText('Task 4')
    ).toBeInTheDocument();
});

Podsumowanie: Wrzesień 2023

Opublikowano: 30.09.2023 - tagi: Blog Podsumowanie Wrzesień 2023

We wrześniu opublikowałem 7 wpisów:


React

  1. Material UI: Jak ustawić kontrolkę na pełną szerokość?
  2. React Testing Library: Jak wyczyścić dane z kontrolki?
  3. React Testing Library: Jak zasymulować wybór opcji z listy?
  4. React Testing Library: Jak zasymulować wpisanie danych do kontrolki?

Swfit

  1. Closure
  2. Struktury

Dodałem też nowe responsywne menu.


Przeczytałem dwie książki:

  1. Imperium bólu — Patrick Radden Keefe
  2. Była raz wojna — John Steinbeck

Przesłuchałem trzy audiobooki:

  1. Sydonia. Słowo się rzekło — Elżbieta Cherezińska
  2. Oko Jelenia. Pan Wilków — Andrzej Pilipiuk
  3. Kozioł ofiarny — Daphne Du Maurier

Blog: Nowe responsywne menu

Opublikowano: 22.09.2023 - tagi: Blog Menu Responsywność

Wrzuciłem nowe responsywne menu (wg mnie lepsze):

Podgląd nowego responsywnego menu

React Testing Library: Jak zasymulować wybór opcji z listy?

Opublikowano: 21.09.2023 - tagi: JavaScript React Testowanie Test Komponent Formularz Kontrolka Lista

Lista jednego wyboru

Do pracy z listami biblioteka user-event udostępnia dwie funkcje: selectOptions i deselectOptions.

W tym wpisie do testów posłużę się takim przykładem:

import {useState} from "react";

const mealMap = {
    1: "Kebab",
    2: "Salad",
    3: "Soup"
}
export function MyComponent({mealId}) {
    const [meal, setMeal] = useState(mealId);

    const onListChange = e => {
        setMeal(e.target.value);
    }

    return (
        <>
            { meal &&
                <div>
                    Here we go! Your meal: {mealMap[meal]}
                </div>
            }
            <div>
                <label htmlFor="meal">Choose your meal:</label>
                <select id="meal" onChange={onListChange}>
                    <option value=""></option>
                    <option value="1">A</option>
                    <option value="2">B</option>
                    <option value="3">C</option>
                </select>
            </div>
        </>
    )
}

Wybieranie opcji — selectOptions

Test:

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

const { getByText, getByLabelText } = screen;

test('should display meal chosen by user', async () => {
    // given
    render(<MyComponent />);
    const select = getByLabelText('Choose your meal:');

    // when
    await userEvent.selectOptions(select, ['1'])

    // then
    expect(getByText('Here we go! Your meal: Kebab')).toBeInTheDocument();
});

Odznaczanie opcji

Jeśli masz listę jednego wyboru, to jak można przetestować odznaczanie opcji? Musisz zrobić dwie rzeczy.

Do swojej listy dodaj pustą opcję:

<option value=""></option>

Następnie za pomocą metody selectOptions przekaż pusty string.

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

const { queryByText, getByLabelText } = screen;

test('should deselect meal', async () => {
    // given
    render(<MyComponent mealId='3' />);
    const select = getByLabelText('Choose your meal:');

    // when
    await userEvent.selectOptions(select, [''])

    // then
    expect(queryByText('Here we go! Your meal: Soup')).not.toBeInTheDocument();
});

Lista wielokrotnego wyboru

Za pomocą atrybutu multiple sprawisz, że użytkownik będzie mógł wybrać więcej niż jedną opcję.

Komponent:

export function MyComponent() {
   return (
        <>
            <label htmlFor="meal">Choose your meal:</label>
            <select id="meal" multiple>
                <option value="1">Kebab</option>
                <option value="2">Salad</option>
                <option value="3">Soup</option>
            </select>
        </>
    )
}

Zaznaczanie wielu opcji — selectOptions

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

const { getByText, getByLabelText } = screen;

test('should choose many meals', async () => {
    // given
    render(<MyComponent />)
    const select = getByLabelText('Choose your meal:');

    // when
    await userEvent.selectOptions(select, ['2', '3'])

    // then
    expect(getByText('Salad').selected).toBeTruthy();
    expect(getByText('Soup').selected).toBeTruthy();
    expect(getByText('Kebab').selected).toBeFalsy();
});

Odznaczanie wielu opcji — deselectOptions

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

const { getByText, getByLabelText } = screen;

test('should deselect many meals', async () => {
    // given
    render(<MyComponent />)
    const select = getByLabelText('Choose your meal:');
    await userEvent.selectOptions(select, ['1', '2', '3']);

    // when
    await userEvent.deselectOptions(select, ['1', '3'])

    // then
    expect(getByText('Salad').selected).toBeTruthy();
    expect(getByText('Soup').selected).toBeFalsy();
    expect(getByText('Kebab').selected).toBeFalsy();
});

Swift: Struktury

Opublikowano: 14.09.2023 - tagi: Swift Struktura Dane

Struktury w Swift

Struktury pozwalają na organizację danych i dzięki temu łatwiejsze ich zarządzanie.

Składnia prezentuje się następująco:

struct SomeStruct {
	...
}

Żeby stworzyć strukturę, użyj słowa kluczowego struct, a następnie podajesz jej nazwę.

W ciele struktury określasz jej właściwości. Możesz też dodać do niej funkcje. W kontekście struktura funkcje nazywane są metodami.

struct Product {
	var name: String = ""
	var price: Float = 0.0
	
	func describe() {
		print("Product name: \(name) and price is: \(price)")
	}
}

Tworzenie instancji

Tworzenie instancji struktury wygląda tak:

var product = Product()

product.name = "Some product"
product.price = 9.99

product.describe() // Product name: Some product and price is: 9.99

Można też określić wartości struktury w momencie jej tworzenia:

var product = Product(name: "Some product", price: 9.99)

product.describe() // Product name: Some product and price is: 9.99

Uwaga: ważna jest kolejność parametrów. Jeśli pierwszą właściwością w strukturze jest: A, a potem B podczas tworzenia instancji struktury musisz podać wartości, w takiej kolejności.

Czyli w przypadku Product nie możesz napisać tak:

var product = Product(price: 9.99, name: "Some product") // błąd!

Możesz do tworzenia instancji struktury także użyć metody init:

struct Product {
	var name: String
	var price: Float
	
	init(name: String, price: Float) {
		self.name = name
		self.price = price
	}
	
	func describe() {
		print("Product name: \(name) and price is: \(price)")
	}
}

Dzięki tej metodzie sam możesz określić kolejność podawanych argumentów przy tworzeniu instancji struktury.

Metody

Jeśli potrzebujesz dodać do struktury metodę, która zmieni swój stan, nie możesz napisać tego w taki sposób:

struct Cart {
    var products: [Product] = []

    func addProduct(_ product: Product) {
        products.append(product)
    }
}

var product = Product(name: "Some product", price: 19.99)

var cart = Cart()

cart.addProduct(product) // błąd!

Dlaczego? W Swift struktury są niezmienne ang.: immutable. Oznacza to, że z poziomu struktury nie możesz zmienić jej stanu. No nie do końca.

Wystarczy, że do danej metody dodasz słowo kluczowe mutating:

struct Cart {
    var products: [Product] = []

    mutating func addProduct(_ product: Product) {
        products.append(product)
    }
}

var product = Product(name: "Some product", price: 19.99)

var cart = Cart()

cart.addProduct(product)

print(cart.products.count) // 1

Stałe struktury

Jeśli instancję struktury określisz za pomocą let:

let product = Product(name: "Some product", price: 19.99)

nie możesz napisać tak:

product.name = "Extra product" // błąd!

Za pomocą let mówisz Swift, że stan obiektu nie może się zmienić.