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ć:
willSet — Zostanie wywołane zaraz przed przypisanie nowej wartości
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:
Dla willSet jest to newValue — jest to wartość, która ma zostać przypisana do zmiennej.
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 :
Nie może to być stała let , tylko var .
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.
Nie możesz użyć tego wspólnie z computed property . Ponieważ computed property nie przechowuje wartości.
Nie musisz używać willSet i didSet razem. Jeśli potrzebujesz tylko jednej z nich, użyj tylko tej.
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 () => {
render(<MyComponent /> );
const btnIncrement = getByRole('button' , { name : "Increment" });
await userEvent.click(btnIncrement);
expect(getByText('1' )).toBeInTheDocument();
});
test('should decrement counter' , async () => {
render(<MyComponent /> );
const btnDecrement = getByRole('button' , { name : "Decrement" });
await userEvent.click(btnDecrement);
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 () => {
renderComponent();
const btnIncrement = getByRole('button' , { name : "Increment" });
...
});
test('should decrement counter' , async () => {
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 () => {
const { btnIncrement, userClicksOn } = renderComponent();
await userClicksOn(btnIncrement);
expect(getByText('1' )).toBeInTheDocument();
});
test('should decrement counter' , async () => {
const { btnDecrement, userClicksOn } = renderComponent();
await userClicksOn(btnDecrement);
expect(getByText('-1' )).toBeInTheDocument();
});
Podsumowanie
Takie podejście do pisania testów sprawia, że rozwiązujesz kilka problemów:
Centralizacja — Ewentualne zmiany, będziesz robić w funkcji renderującej komponent. Nie musisz "skakać" po wielu testach, żeby coś naprawić.
Testy są bardziej czytelne.
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 () => {
const { getByRole } = screen;
render(<MyComponent /> );
const list = getByRole('list' );
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 () => {
const { getAllByRole } = screen;
render(<MyComponent /> );
const listItems = getAllByRole('listitem' ).map((item ) => item.textContent);
expect(listItems).toEqual(['Task 1' , 'Task 2' , 'Task 3' ]);
});
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 () => {
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;
await userEvent.click(removeTaskBtn);
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!
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);
Sposób 2: length
items.length = 3 ;
console .log(items);
Nowsze wpisy
Poprzednie wpisy