Swift: Property Observers

Opublikowano: 26.10.2023 - tagi: Swift Atrybut Zmienna

Czym są Property Observers?

Swift pozwala na obserwowanie ewentualnych zmian przypisywanych do zmiennej. Dzięki property observers możesz zareagować na przypisanie wartości zmiennej tuż przed lub zaraz *po.

Możesz użyć:

  1. willSet — Zostanie wywołane zaraz przed przypisanie nowej wartości
  2. didSet — Zostanie wywołane tuż po przypisaniu wartości

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

var someProperty: Int = 0 {
    willSet {
        print("New value: \(newValue)")
    }
    didSet {
       print("Old value: \(oldValue)")
    }
}

Zarówno willSet jak i didSet przyjmują domyślnie parametry:

  1. Dla willSet jest to newValue — jest to wartość, która ma zostać przypisana do zmiennej.
  2. Dla didSet jest to oldValue — jest to poprzednia wartość, którą zawierała ta zmienna.

Możesz też sam określić nazwy tych parametrów:

var someProperty: Int = 0 {
    willSet(valueToSet) {
        print("New value: \(valueToSet)")
    }
    didSet(previousValue) {
       print("Old value: \(previousValue)")
    }
}

Przykład

struct CaloriesCounter {
    var totalCalories: Int = 0 {
        willSet(newTotalCalories) {
            print("Total calories is \(newTotalCalories)")
        }
        didSet {
            if totalCalories > oldValue  {
                print("Added \(totalCalories - oldValue) calories")
            }
        }
    }
}

Warto wiedzieć

Jeśli oznaczysz zmienną jako property observer:

  1. Nie może to być stała let, tylko var.
  2. Musi zawierać wartość domyślną. Jeśli nie wiesz, co przypisać przypisz nil. Możesz też oznaczyć zmienną jako optional — wtedy domyślnie zostanie przypisane nil do zmiennej.
  3. Nie możesz użyć tego wspólnie z computed property. Ponieważ computed property nie przechowuje wartości.
  4. Nie musisz używać willSet i didSet razem. Jeśli potrzebujesz tylko jednej z nich, użyj tylko tej.

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]