Blog
Granice i zależności w Symfony i React: jak dobrze zacząć nową aplikację
Jak podzielić odpowiedzialności Symfony i React, dobierać wzorce do rzeczywistych potrzeb i chronić granice testami bez budowania nadmiarowych warstw.
Zacznij od zmiany, która powinna pozostać lokalna
Wyobraźmy sobie aplikację do obsługi napraw w domu kultury. Pracownik zgłasza uszkodzone wyposażenie, technik opisuje wykonane prace, a koordynator zamyka zgłoszenie. Wybrane kategorie napraw wymagają weryfikacji przez drugą osobę. Po zamknięciu zewnętrzna usługa tablicy ogłoszeń może zaktualizować informację dotyczącą sali.
Pierwsza wersja nie potrzebuje mikroserwisów ani całego katalogu wzorców. Potrzebuje odpowiedzi: kto decyduje o dopuszczalności zamknięcia, kto je zapisuje i kto zna API dostawcy tablicy? Jeżeli wymiana tego dostawcy wymaga później zmian w kontrolerze, regule zamknięcia i React, zależność zdążyła rozlać się zbyt szeroko.
Na tej jednej operacji oprzemy dalsze przykłady. To fikcyjny kontrakt aplikacji, a nie opis wdrożenia GiSoft. Kod zakłada PHP 8.2 lub nowszy; kontekstem są Symfony 7.4 i React 19. Przy fragmentach wskazujemy pominięte typy projektowe.
Przypisz odpowiedzialności konkretnym miejscom
React odpowiada za interakcję: pokazuje zgłoszenie, wysyła żądanie zamknięcia i przedstawia wynik. Symfony rozpoznaje użytkownika, sprawdza uprawnienia i pilnuje zmiany stanu. Operacja aplikacyjna łączy te decyzje z zapisem. Adapter wie, jak dostawca zewnętrzny opisuje ogłoszenie.
Mapa rozróżnia wywołania od implementacji interfejsów. Nie przedstawia obowiązkowej kolejności listenerów frameworka:
React repairs -- JSON --> RepairController --> CloseRepair
CloseRepair używa ClosurePolicy i RepairTickets
DoctrineRepairTickets implementuje RepairTickets
Handler powiadomienia używa RoomBulletin
RemoteRoomBulletin implementuje RoomBulletinAplikacja definiuje potrzebne jej możliwości. Infrastruktura je realizuje, a kontener Symfony dobiera implementacje. Wywołania nadal trafiają do Doctrine i dostawcy, ale ich API nie musi stać się zależnością reguły biznesowej. Całkowita niezależność od frameworka też kosztuje. Izoluj te miejsca, w których ułatwia to zmiany lub testy.
Od akcji HTTP do operacji aplikacyjnej
Początkowy kontroler, który pobiera zgłoszenie i aktualizuje prosty opis, jest zrozumiały. Kłopot zaczyna się, gdy dodatkowo ustala obowiązek weryfikacji, zamyka rekord, wywołuje SDK tablicy i serializuje encję. Późniejszy import CLI musi wtedy powtórzyć regułę albo korzystać z kodu dopasowanego do żądania HTTP.
Wystarczy wydzielić CloseRepair. Routing, odczyt i walidacja danych wejściowych oraz mapowanie wyniku na odpowiedź HTTP pozostają w kontrolerze. Tożsamość wykonawcy pochodzi z uwierzytelnionego kontekstu, nigdy z niezweryfikowanego identyfikatora użytkownika w JSON. Do operacji przekazujemy identyfikator zgłoszenia i poprawnie odczytaną rewizję. Kontroler nadal może mieć kilka sensownych linii.
Poniższy fragment definiuje kontrakty projektu, nie API Symfony ani Doctrine. RepairTicket, Actor, ClosurePermission i wyjątki to pominięte typy projektowe. ClosurePermission sprawdza uprawnienie do zamykania oraz dostęp do lokalizacji zgłoszenia; requireAwaitingClosure() odrzuca niedozwolony stan bieżący.
interface RepairTickets
{
public function get(string $id): RepairTicket;
public function closeIfCurrent(
string $id, int $revision, string $actorId,
): bool;
}
final readonly class CloseRepair
{
public function __construct(
private RepairTickets $tickets,
private ClosurePolicy $policy,
private ClosurePermission $permission,
) {}
public function __invoke(Actor $actor, string $id, int $revision): void
{
$ticket = $this->tickets->get($id);
$this->permission->requireClosure($actor, $ticket);
$ticket->requireAwaitingClosure();
if (($reason = $this->policy->rejection($ticket->closureFacts())) !== null) {
throw new ClosureRejected($reason);
}
if (!$this->tickets->closeIfCurrent($id, $revision, $actor->id)) {
throw new StaleRepair();
}
}
}get() musi jednoznacznie zgłosić brak rekordu. Kontrakt closeIfCurrent() jest istotny: atomowe porównanie oczekiwanej rewizji i stanu dopuszczającego zamknięcie, następnie zapis zamknięcia, nowej rewizji oraz informacji o wykonawcy w historii zmian. Każda zmiana danych wpływających na decyzję musi uczestniczyć w tym mechanizmie wersjonowania. Inaczej wcześniejsze sprawdzenie może stracić aktualność przed zapisem. Adapter może użyć blokady optymistycznej lub równoważnego zapisu warunkowego; sam interfejs tego nie zapewnia.
Implementacja wymaga też ustalenia granic transakcji i mapowania spodziewanych odmów na bezpieczne odpowiedzi HTTP. Celowo pomijamy tutaj kod bazy danych. Serwis uprawnień i polityka są konkretnymi klasami: samo wstrzykiwanie zależności nie uzasadnia interfejsu dla każdej z nich.
Reguła zamknięcia wspólna dla HTTP i CLI
W tej aplikacji opis wykonanych prac jest obowiązkowy. Kategoria wymagająca weryfikacji musi dodatkowo mieć sprawdzającego innego niż wykonawca naprawy. Polityka podejmuje tę decyzję bez HTTP, Doctrine i zegara:
final readonly class ClosureFacts
{
public function __construct(
public string $workNote,
public bool $needsReview,
public string $performedBy,
public ?string $reviewedBy,
) {}
}
final class ClosurePolicy
{
public function rejection(ClosureFacts $facts): ?string
{
if (trim($facts->workNote) === '') {
return 'work_note_missing';
}
if ($facts->needsReview && (
trim($facts->reviewedBy ?? '') === ''
|| $facts->reviewedBy === $facts->performedBy
)) {
return 'independent_review_required';
}
return null;
}
}Dane pochodzą z zaufanych zapisów oraz reguł kategorii. Przeglądarka nie może sama ogłosić zakończenia weryfikacji. Model pilnuje niepustych identyfikatorów pracowników oraz ważności zapisanych ocen. Zmiana zweryfikowanych prac unieważnia ich ocenę i zwiększa rewizję. Uprawnienie do zamknięcia zgłoszenia pozostaje odrębnym sprawdzeniem.
Osobna polityka ma tu sens, bo decyzję wykorzystuje również proces CLI, a reguła ma kilka niezależnych przypadków. Metoda encji lub niewielka metoda serwisu aplikacyjnego też może być dobrym rozwiązaniem. Tworzenie Specification dla każdego if dodawałoby plików bez wyjaśnienia reguły.
W Pest 3 tę decyzję można przetestować bez uruchamiania Symfony. Powyższe klasy muszą być dostępne przez autoloader projektu testowego; przykład nie zakłada buildera danych ani ukrytego helpera.
it('wymaga opisu prac i wymaganej niezależnej weryfikacji', function (
string $note, bool $review, ?string $reviewer, ?string $expected,
): void {
$facts = new ClosureFacts($note, $review, 'worker-7', $reviewer);
expect((new ClosurePolicy())->rejection($facts))->toBe($expected);
})->with([
['', false, null, 'work_note_missing'],
['hinge adjusted', true, null, 'independent_review_required'],
['hinge adjusted', true, 'worker-7', 'independent_review_required'],
['hinge adjusted', true, 'worker-9', null],
['hinge adjusted', false, null, null],
]);Przypadki dopuszczone są równie potrzebne jak odmowy. Ten test nie sprawdza blokad bazy danych ani integracji uprawnień z HTTP.
Oddziel zapis od danych publicznych tam, gdzie to potrzebne
Repository może opisywać operacje pobierania i zamykania zgłoszenia. Interfejs jest tutaj przydatny, ponieważ aplikacja potrzebuje określonego sposobu zapisu z kontraktem obsługi współbieżności. Nie jest obowiązkowy dla każdej encji ani każdego odczytu. Wstrzykiwanie EntityManager w całej aplikacji lub zwracanie QueryBuilder, który wywołujący ma dokończyć, przenosi decyzje o zapisie i odczycie na kolejne klasy.
Dla listy napraw w sali Query object może zamknąć złożone, powtarzalne zapytanie, a read repository — pogrupować powiązane odczyty. Projekcja/read model wybiera ograniczony zestaw pól, np. identyfikator, nazwę sali i status. Lista nie potrzebuje wszystkich notatek, załączników i danych pracowników. Prosty odczyt nie wymaga tych trzech abstrakcji naraz. Limity liczby zapytań i rozmiaru odpowiedzi warto testować tam, gdzie chronią przed konkretną regresją.
Modele mają różne zadania. Encja opisuje przechowywany stan i może też zawierać zachowanie biznesowe. Request DTO reprezentuje niezaufane dane wejściowe. Response DTO lub jawna tablica określa publiczny wynik. Read model służy zapytaniu, a view model w React — ekranowi. Value object pomaga, gdy pojęcie, np. kod sali, ma istotne niezmienniki. Jedna funkcja nie potrzebuje siedmiu kopii tych samych pól.
Encje Doctrine mogą pozostać wewnątrz aplikacji, jeśli jest to świadomy kompromis. Nie wystawiaj całego ich grafu jako API tylko dlatego, że serializer potrafi go przejść. Często wystarczy krótka funkcja mapująca; osobna klasa ma sens, gdy transformacja rzeczywiście jej potrzebuje.
Zatrzymaj język dostawcy na granicy integracji
Wzorce granic i integracji rozwiązują problemy różnej wielkości. Port, zwykle interfejs należący do projektu, nazywa potrzebną możliwość. Adapter przekłada ją na wywołanie dostawcy i tłumaczy wynik. ClosureNotice to pominięty projektowy DTO zawierający wyłącznie dane potrzebne do ogłoszenia:
interface RoomBulletin
{
public function recordClosure(
ClosureNotice $notice,
string $operationId,
): void;
}Facade pomaga, jeśli publikacja ogłoszenia wymaga kilku operacji niskiego poziomu. Niewiele wnosi, gdy tylko zmienia nazwę jednej metody. Anti-corruption layer (ACL) przydaje się przy sprzecznych modelach pojęciowych: „closed” u dostawcy może oznaczać wycofanie ogłoszenia, a u nas zakończoną naprawę. Zwykła zamiana kilku pól rzadko wymaga całej warstwy.
DTO/View Model ogranicza dane przechodzące przez granicę transportu lub prezentacji. Przepisanie odpowiedzi dostawcy do DTO o identycznych polach nie usuwa automatycznie zależności od znaczenia tych danych. Przy drobnej, już odizolowanej integracji wystarczy czasem jedna funkcja adaptera. Parametr operationId określa potrzebę deduplikacji; nie nadaje dowolnemu dostawcy idempotencji.
React korzysta z kontraktu modułu napraw
Przykładowe API przyjmuje POST /api/repairs/{id}/close z rewizją. Jego dokumentowany kontrakt przewiduje 204 po sukcesie, 409 przy konflikcie rewizji lub stanu oraz 422 przy niespełnionej regule zamknięcia. To decyzje tego projektu. Brak uwierzytelnienia i odmowa uprawnień mają własne odpowiedzi, w tym JSON API umownie 401 i 403, oraz testy użytkownika anonimowego, niedopuszczonego i uprawnionego.
Funkcja closeRepair, należąca do modułu napraw, obsługuje szczegóły HTTP: wybrane poświadczenie sesyjne lub bearer, odpowiednią ochronę CSRF, weryfikację odpowiedzi i mapowanie bezpiecznych publicznych kodów błędów. Zwraca niewielki wynik pokazany niżej. Nie ujawnia nazw wyjątków PHP ani nie wyświetla bezkrytycznie dowolnego tekstu z serwera. Deklaracja TypeScript nie sprawdzi zewnętrznego JSON w czasie wykonania.
Komponent odpowiada za oczekiwanie, odmowę i sukces. To zawartość pliku komponentu; adapter sieciowy pominięto. Typ funkcji jest naszym kontraktem, a nie helperem frameworka.
import { useState } from 'react';
export type CloseResult =
| { kind: 'closed' }
| { kind: 'blocked'; message: string };
export type CloseRepair = (id: string, revision: number) => Promise<CloseResult>;
type Props = { id: string; revision: number; closeRepair: CloseRepair };
export function CloseRepairButton({ id, revision, closeRepair }: Props) {
const [phase, setPhase] = useState<'idle' | 'pending' | 'closed'>('idle');
const [notice, setNotice] = useState('');
async function submit() {
if (phase !== 'idle') return;
setPhase('pending');
setNotice('');
try {
const result = await closeRepair(id, revision);
setPhase(result.kind === 'closed' ? 'closed' : 'idle');
if (result.kind === 'blocked') setNotice(result.message);
} catch {
setPhase('idle');
setNotice('Nie udało się potwierdzić zamknięcia. Sprawdź zgłoszenie przed ponowieniem.');
}
}
return (
<>
<button type="button" disabled={phase !== 'idle'} onClick={submit}>
{phase === 'closed' ? 'Zgłoszenie zamknięte' : 'Zamknij zgłoszenie'}
</button>
{notice && <p role="alert">{notice}</p>}
</>
);
}W Next.js komponent należy importować w poddrzewie klienckim. Rodzic z 'use client' może przekazać callback z przeglądarkowego adaptera modułu napraw. Zwykłego callbacka nie przekazujemy przez tę granicę z Server Component. Żądania wykonywane po stronie serwera również wymagają jawnego ustalenia sposobu przekazania poświadczeń; nie dziedziczą ich automatycznie z przeglądarki.
Zablokowany przycisk poprawia obsługę, ale nie egzekwuje uprawnień, współbieżności ani idempotencji backendu. React może od razu wskazać pusty opis prac; Symfony nadal musi go odrzucić. Przy niepewnym wyniku żądania sieciowego przed ponowieniem należy sprawdzić lub odświeżyć zgłoszenie. Automatyczne ponawianie operacji zmieniającej stan wymaga osobnego kontraktu bezpieczeństwa.
Małe moduły i widoczne zależności
To jeden z możliwych układów. Katalogi opisują fikcyjny moduł, nie propozycję reorganizacji tego repozytorium:
src/Repairs/
Application/ CloseRepair, RepairTickets
Domain/ ClosurePolicy, RepairTicket
Infrastructure/ DoctrineRepairTickets, RemoteRoomBulletin
UI/ RepairController
features/repairs/
api/ closeRepair
model/ CloseResult
ui/ CloseRepairButton
shared/ui/ ButtonReact nie potrzebuje kopii warstw backendu. Mapowanie endpointu trzymaj przy module; wspólny mechanizm HTTP wydziel wtedy, gdy kilka modułów rzeczywiście potrzebuje tego samego zachowania transportu. Globalny ApiService obsługujący wszystkie endpointy staje się kolejnym nadmiernie rozbudowanym modułem.
Moduł sal może wystawić niewielki kontrakt aplikacyjny sprawdzający dostęp do lokalizacji. Moduł napraw nie powinien importować jego prywatnego repozytorium ani samodzielnie zmieniać jego encji. Bezpośrednie wywołanie serwisu aplikacyjnego bywa całkowicie wystarczające; zdarzenie nie jest obowiązkowe. Cykl zależności często wskazuje na niejasny podział odpowiedzialności, a nie brak kolejnego interfejsu.
Argumenty konstruktora ujawniają zależności. Pobieranie usług z kontenera je ukrywa. Shared, Common, Utils i Helpers też potrzebują gospodarza: świadomie umieszczaj tam stabilne pojęcia wspólne dla modułów. Dwie podobne funkcje bywają tańsze niż związanie modułów abstrakcją, której znaczenie dopiero się kształtuje.
Wzorce decyzji i tworzenia obiektów: dobierz najmniejsze narzędzie
Policy, Strategy, Specification i przejścia stanów odpowiadają na różne pytania. Nasza Policy decyduje, czy można zamknąć zgłoszenie. Strategy mogłaby wybierać faktycznie wymienne algorytmy przydziału technika. Przy jednym algorytmie wystarczy zwykły serwis. Specification nadaje nazwę wielokrotnie używanemu, składanemu warunkowi; lokalne sprawdzenie rzadko wymaga takiej konstrukcji.
Proste przejścia stanu można jawnie zapisać w encji lub operacji. Maszyna stanów pomaga, gdy wiele stanów, przejść i warunków dopuszczenia wymaga wspólnego modelu. Trzy oczywiste przejścia nie uzasadniają jej same w sobie. Te decyzje należą do backendu, nawet jeśli React wygodnie podpowiada wynik.
Named constructor, Factory i Builder dotyczą tworzenia. RepairTicket::reported(...) może jasno określać stan początkowy i jego niezmienniki. Factory pomaga, gdy utworzenie obiektu wymaga danych referencyjnych lub istotnego wyboru implementacji. Zwykły DTO nie potrzebuje fabryki ani serwisu. Builder sprawdza się zwłaszcza w testach z opcjonalnymi ocenami i notatkami, o ile pokazuje ważny stan zamiast chować go za rozbudowanymi wartościami domyślnymi.
Każda abstrakcja to kolejne pojęcie, konfiguracja zależności i pliki do przeglądania. Warto zapłacić ten koszt, gdy decyzja lub zasady tworzenia stają się dzięki temu czytelniejsze.
Koordynuj operację bez budowania frameworka komend
Serwis aplikacyjny/use case przechowuje pokazaną wcześniej kolejność: odczyt, kontrolę dostępu, ocenę reguły i zapis. Nie musi znać Request, React ani typu odpowiedzi dostawcy. Prosta operacja CRUD może być znacznie mniejsza.
Command może reprezentować zamiar zamknięcia zgłoszenia, a Query — odczyt otwartych napraw w sali. Oddzielaj odczyt od zapisu tam, gdzie ułatwia to zrozumienie kodu, bez obowiązkowego obiektu i busa dla każdej metody. Rozdzielenie komend i zapytań nie wymaga pełnego CQRS, osobno przechowywanych modeli odczytowych ani aktualizacji asynchronicznych.
CQRS warto rozważyć, gdy potrzeby odczytu i zapisu różnią się na tyle, by uzasadnić osobne modele oraz, jeśli dotyczy, ich synchronizację. Message handler obsługuje pracę świadomie skierowaną do mechanizmu wiadomości. Nie jest obowiązkową otoczką każdego serwisu aplikacyjnego.
Techniczne rozszerzenia nie powinny ukrywać decyzji biznesowych
Decorator może mierzyć działanie adaptera tablicy albo buforować odczyt przy zachowaniu kontraktu. Middleware nakłada wspólne zachowanie na ścieżkę transportu lub wiadomości. Oba pomagają przy powtarzalnym logowaniu, metrykach i śledzeniu wywołań; wokół jednej prostej operacji mogą bardziej zaciemniać kod, niż pomagać.
Event subscriber/listener pasuje do reakcji technicznej o zrozumiałej kolejności. Schowanie kontroli uprawnień lub decyzji o zamknięciu w listenerze Doctrine utrudnia odtworzenie przebiegu operacji. Krytyczne decyzje synchroniczne powinny pozostać widoczne. Pipeline porządkuje rzeczywiście uporządkowane etapy przetwarzania; krótka metoda nie potrzebuje rejestru etapów.
Cache i retry również zmieniają zachowanie. Klucze oraz unieważnianie cache muszą uwzględniać uprawnienia i wymaganą świeżość. Mechanizm ponawiania musi rozpoznawać błędy i operacje, które wolno powtórzyć. Nazwanie ich aspektami przekrojowymi nie usuwa tej odpowiedzialności.
Ustal znaczenie zamknięcia, zanim dodasz kolejkę
Zgłoszenie jest zamknięte, gdy zatwierdzono uprawnioną zmianę stanu. Aktualizacja tablicy jest w tym przykładzie czynnością dodatkową. Symfony Messenger może wykonać ją później; przeniesienie sprawdzenia uprawnień do opóźnionego handlera zmieniłoby kontrakt. Kolejka zmienia miejsce i czas wykonania oraz wprowadza problemy dostarczania, zamiast automatycznie poprawiać projekt.
Jeżeli utrata ogłoszenia jest niedopuszczalna, Outbox może zapisać zamiar wysłania w tej samej transakcji bazy co zamknięcie. Worker dostarczy go później. Nadal możliwe jest dostarczenie wielokrotne. Klucz idempotencji identyfikuje to samo logiczne powiadomienie przy kolejnych próbach; ochrona przed duplikatami wymaga atomowej koordynacji lokalnej lub właściwego wsparcia dostawcy, nie samego pola operationId.
Retry policy powinna ograniczać ponowienia po błędach przejściowych, a nie powtarzać trwałe odmowy uprawnień. Circuit breaker może czasowo wstrzymać wywołania stale niedziałającego dostawcy. Fallback musi oznaczać zaakceptowany wynik, np. „ogłoszenie oczekuje”, zamiast udawać skuteczną wysyłkę. Te mechanizmy kosztują w przechowywaniu stanu i obsłudze. Wprowadzaj je zgodnie z wymaganiami dostarczania, nie jako obowiązkowy szablon startowy.
Wzorzec jest wyborem, za który płacisz
Przy ocenie proponowanej abstrakcji pomaga takie zestawienie. Prostsza opcja pozostaje poprawna, jeśli chroni tę samą odpowiedzialność.
Problem Kandydat Prostsza opcja
Model dostawcy Port + Adapter / ACL Lokalne mapowanie
Wspólna decyzja Policy / Specification Zwykła metoda
Kilka algorytmów Strategy Jeden serwis
Złożona lista Query + projekcja Prosty odczyt
Stałe pomiary Decorator / Middleware Pomiar w kodzie
Reguły tworzenia Factory KonstruktorTo nie lista zakupów. Dla pierwszej funkcji zamykania wystarczą czasem konkretna polityka, jeden serwis aplikacyjny, kontrakt zapisu i funkcja API należąca do modułu. Adapter dostawcy dodaj wraz z integracją. Nie buduj z wyprzedzeniem interfejsów dla każdej klasy, fabryk dla DTO, zdarzeń domenowych dla każdej zmiany ani infrastruktury Messenger/CQRS do synchronicznego formularza.
Testuj granicę, na której może wystąpić błąd
Analiza statyczna daje szybki sygnał: PHPStan i TypeScript wykrywają problemy typów oraz błędne założenia o null. Reguły importów lub testy architektury mogą pilnować uzgodnionego kierunku zależności, np. zakazu SDK dostawcy w kodzie aplikacyjnym lub importów serwerowych w module klienckim. Wymagają jawnych reguł i odpowiednich narzędzi; same nie odgadną architektury ani nie dowiodą zachowania biznesowego.
Test polityki tanio sprawdza decyzje. Test integracyjny powinien uruchomić DoctrineRepairTickets na izolowanej bazie testowej: sprawdzić rzeczywiste filtrowanie, konflikt rewizji i wymagane wycofanie transakcji. Mock QueryBuilder tego nie potwierdzi. Testy adaptera używają kontrolowanych odpowiedzi dostawcy do sprawdzenia mapowania i obsługi błędów, bez połączenia z żywą usługą w zwykłym CI.
Test funkcjonalny API Symfony obejmuje routing, dekodowanie, uprawnienia, walidację, publiczny wynik i wybrane skutki uboczne. Oprócz odmów sprawdź koordynatora, który może wykonać operację. Artykuł GiSoft „Jak testować API Symfony za pomocą Pest” rozwija szczegółowo przypadki testowe HTTP.
W React testuj widoczne zachowanie. Przykład Vitest/React Testing Library zakłada środowisko DOM i @testing-library/jest-dom/vitest w konfiguracji testów. Korzysta z powyższego komponentu oraz celowo nierozstrzygniętej obietnicy, bez zegara i żądania sieciowego:
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, it, vi } from 'vitest';
import { CloseRepairButton, type CloseResult } from './CloseRepairButton';
it('blokuje zamknięcie podczas oczekiwania na odpowiedź', async () => {
const user = userEvent.setup();
let finish!: (result: CloseResult) => void;
const response = new Promise<CloseResult>((resolve) => { finish = resolve; });
const closeRepair = vi.fn().mockReturnValue(response);
render(<CloseRepairButton id="repair-42" revision={8} closeRepair={closeRepair} />);
const button = screen.getByRole('button', { name: 'Zamknij zgłoszenie' });
await user.click(button);
expect(button).toBeDisabled();
expect(closeRepair).toHaveBeenCalledWith('repair-42', 8);
await act(async () => { finish({ kind: 'closed' }); });
expect(screen.getByRole('button', { name: 'Zgłoszenie zamknięte' })).toBeDisabled();
});Dodaj przypadek odmowy, który zachowuje pracę użytkownika i pozwala poprawić dane. Test kontraktu lub adaptera sprawdza mapowanie statusów, strukturę JSON, wartości nullable, daty i bezpieczne kody błędów względem faktycznego kontraktu backendu. Rzutowanie na typ TypeScript nie jest takim testem.
Jeden scenariusz przeglądarkowy może przejść przez zgłoszenie, weryfikację i zamknięcie. Przypadki brzegowe polityki zostaw w testach jednostkowych. Duże snapshoty, współdzielone fixtures, zależność od czasu rzeczywistego i ukryte helpery zwiększają koszt utrzymania bez pewności, że chronią kolejne ryzyko. Używaj deterministycznych danych i izoluj stan; szybkie kontrole uruchamiaj wcześnie, potem odpowiednie testy integracyjne, API i wybrane przeglądarkowe. Dokładna kolejność CI zależy od projektu.
Granica / ryzyko Narzędzie projektu Wiarygodny test
Decyzja zamknięcia Policy Jednostkowy
Rewizja i zapis Kontrakt persystencji Integracyjny DB
Model dostawcy Port + Adapter Kontraktu adaptera
Uprawnienia HTTP Granica bezpieczeństwa Funkcjonalny API
Oczekiwanie / błąd UI Komponent modułu Komponentowy
Cały przebieg Kilka granic Wybrany przeglądarkowyZacznij od jednej kompletnej funkcji
Zaimplementuj zgłoszenie i zamknięcie przez rzeczywiste granice HTTP oraz zapisu, zanim zbudujesz rozległe drzewo katalogów. Zapisz, gdzie znajduje się reguła, co obiecuje API, co oznacza równoczesna zmiana oraz czy awaria powiadomienia wpływa na wynik. Wróć do tych decyzji, gdy drugi przypadek użycia stworzy faktyczną potrzebę współdzielenia kodu.
Szczegóły optymalizacji Doctrine, diagnozowania firewalla, ponownego dostarczania wiadomości i katalogi testów API zostają w osobnych artykułach. Tutaj sprawdzian projektu jest prostszy: wymiana dostawcy tablicy powinna ograniczyć się do adaptera, a zmiana reguły zamknięcia mieć oczywiste miejsce i precyzyjny test. Dodawaj strukturę wtedy, gdy zwiększa bezpieczeństwo takiej zmiany.
Dokumentacja techniczna
Szczegóły zależne od wersji sprawdzono w poniższej dokumentacji; scenariusz napraw i przykłady opracowano dla tego artykułu.
- Doctrine ORM 3.6, transakcje i współbieżność: https://www.doctrine-project.org/projects/doctrine-orm/en/3.6/reference/transactions-and-concurrency.html
- React, granice modułów klienckich: https://react.dev/reference/rsc/use-client
- Symfony 7.4, dostarczanie i ponawianie wiadomości: https://symfony.com/doc/7.4/messenger.html
