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.


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();
});