1. Zarządzane środowisko kontekstu testowego
Jeśli patrzeć na testy jak na żywe stworzenia (tak, ja też czasem rozmawiam z testami — to zawodowa deformacja), to kochają one dwie rzeczy: przewidywalność i ciszę. Przewidywalność — to gdy ID nie są przypadkowe, ścieżki wyjścia nie zależą od komputera, a zachowanie „trybu aplikacji" nie zmienia się przez to, że wczoraj uruchamiałeś dev, a dziś — demo. Cisza — to gdy test nie pisze gigabajtów do konsoli i nie tworzy plików w korzeniu projektu, zostawiając po sobie „warstwy archeologiczne".
Smoke test już pokazał najważniejsze: kontekst w ogóle startuje, wiring nie jest rozwalony. Teraz pojawia się bardziej przyziemne pytanie: jak sprawić, żeby ten kontekst startował w jednym i tym samym trybie test, a nie w przypadkowej mieszance profili, starych właściwości i głośnych implementacji.
Nasz ContextFlow — to aplikacja non-web bez bazy danych, ale i tak żyje w środowisku: profile (dev/demo/test), właściwości (contextflow.*), wybór implementacji (ConsoleAuditWriter vs FileAuditWriter, UuidOrderIdGenerator vs DeterministicOrderIdGenerator). W normalnym uruchomieniu jest to świetne: zmieniasz środowisko — zmienia się zachowanie. W testach może to stać się źródłem chaosu: oczekiwałeś, że test stworzy zamówienie z przewidywalnym ID, a nagle wygenerował UUID; oczekiwałeś, że raporty są pisane do build/, a okazały się w jakiejś „starej" ścieżce z poprzedniego uruchomienia.
Dlatego dziś uczymy się „przykręcać" środowisko do testu na stałe, ale ostrożnie. Do tego mamy trzy główne dźwignie: @ActiveProfiles, @TestPropertySource i test-specific @Configuration (ze stubami beanów). Ważne jest rozumienie: to nie „magia dla magii", lecz sposób na uczynienie warunków testowych częścią kontraktu testu. Czyli żeby każda osoba (w tym ty za dwa tygodnie) otworzyła test i od razu widziała: jaki profil jest aktywny, jakie właściwości są nadpisane i jakie zależności zostały podmienione.
2. @ActiveProfiles: profile dla testów
Profile w Spring już omawialiśmy wcześniej: to sposób na złożenie różnych wariantów tej samej aplikacji bez if-else wewnątrz kodu biznesowego. W testach profile stają się jeszcze ważniejsze, bo testy — to nie „kolejne uruchomienie aplikacji", lecz osobny tryb życia, gdzie najczęściej chcesz: deterministyczność, minimum efektów ubocznych i szybkie weryfikacje.
Adnotacja @ActiveProfiles("test") robi prostą rzecz: mówi Spring Test, jakie aktywne profile należy wystawić w Environment przy starcie kontekstu testowego. I dalej kontener składa się tak, jakby aplikacja startowała w profilu test: podciągają się beany @Profile("test"), wykluczone są @Profile("dev") i @Profile("demo"), i w ogóle cała twoja konfiguracja profile-aware zaczyna pracować na test.
Żeby to nie było abstrakcją, przydatne jest trzymanie w głowie „mapy" naszych profili właśnie w ContextFlow. To nie lista „wszystkich możliwych", lecz dokładnie te punkty, które dla projektu edukacyjnego są zazwyczaj ważne:
| Komponent / port | dev (lokalnie, prosto) | demo (demonstracja) | test (deterministycznie) |
|---|---|---|---|
| OrderIdGenerator | UUID (realistycznie, ale losowo) | może być UUID lub ustalony | deterministyczny generator |
| NotificationSender | konsolowy | konsolowy/„pokazowy" | NoOp lub stub |
| AuditWriter | konsolowy | plikowy w build/ | w build/ lub in-memory/no-op |
| ścieżki wyjścia raportów | build/… | build/… | build/test-… (żeby nie mieszać) |
Główny sens: test często potrzebuje profilu test, nawet jeśli przy „prawdziwym uruchomieniu" częściej używasz dev.
Minimalny test, który sprawdza, że profil naprawdę wpłynął na wiring, może wyglądać tak (zwróć uwagę: sprawdzamy nie „wynik biznesowy", lecz właśnie wariant składania):
import com.example.contextflow.domain.ports.OrderIdGenerator;
import com.example.contextflow.infrastructure.id.DeterministicOrderIdGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertTrue;
// Podnosimy kontekst Spring tylko z wskazanej konfiguracji (bez uruchamiania całej aplikacji)
@SpringJUnitConfig(classes = com.example.contextflow.config.core.ContextFlowAppConfig.class)
// Jawnie utrwalamy profil, żeby nie zależeć od IDE/parametrów uruchomienia
@ActiveProfiles("test")
class TestProfileWiringTest {
// Wstrzykujemy port: interesuje nas nie konkretna implementacja po nazwie, lecz to, co wybrał kontener
@Autowired
OrderIdGenerator orderIdGenerator;
@Test
void usesDeterministicGeneratorInTestProfile() {
// Sprawdzamy właśnie „wariant składania": w profilu test powinien być deterministyczny generator
assertTrue(orderIdGenerator instanceof DeterministicOrderIdGenerator);
}
}
Jest tu jeden ważny niuans z „prawdziwego życia Spring", który warto trzymać w głowie już teraz. Jeśli na warstwie serwisowej jest włączone AOP (a w naszym projekcie jest: ServiceTimingAspect), to niektóre beany mogą okazać się proksowane, i weryfikacja w stylu bean.getClass() == SomeClass.class może niespodziewanie się złamać. Dlatego do „kto to w istocie jest" częściej pasuje instanceof lub weryfikacja po interfejsie, jak wyżej.
I jeszcze jeden niuans: @ActiveProfiles w teście — to nie „ficzer dla urody". To sposób na ochronę przed sytuacją, gdy test przypadkowo uruchamia się z domyślnym profilem. W efekcie zamiast NoOpNotificationSender dostaniesz konsolowego sendera, i test zacznie śmiecić wydrukiem. Nie złamie się logicznie, ale zamieni się w noisy-test, a głośne testy długo nie żyją: zaczyna się je ignorować.
3. @TestPropertySource: nadpisywanie właściwości
Profile rozwiązują kwestię „jakie beany w ogóle uczestniczą w składaniu". Ale często to nie wystarczy: nawet w profilu test chcesz punktowo podkręcić ustawienia. Na przykład chcesz, żeby raporty były pisane do build/test-reports, a nie do wspólnego build/reports, żeby testy nie mieszały się z twoimi ręcznymi uruchomieniami. Albo chcesz zmniejszyć jakiś limit, żeby test szybciej przechodził (w naszym projekcie limitów jest niewiele, ale w prawdziwych aplikacjach to klasyka).
Do tego służy @TestPropertySource. Dodaje ona property sources do Environment właśnie dla kontekstu testowego. Najwygodniejsze, zwłaszcza dla początkujących, — to inline overrides przez properties = .... Czytają się bezpośrednio w teście, nie wymagają osobnego pliku, i bardzo jawnie pokazują intencję.
Oto minimalny przykład: nie testujemy „generowania raportu", po prostu pokazujemy, że właściwość nadpisała się właśnie w środowisku testu:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertEquals;
// Podnosimy kontekst z „bojowej" konfiguracji, ale dodajemy testową warstwę właściwości
@SpringJUnitConfig(classes = com.example.contextflow.config.core.ContextFlowAppConfig.class)
// Inline override: czyta się bezpośrednio z testu i jawnie pokazuje intencję
@TestPropertySource(properties = "contextflow.report.output-dir=build/test-reports")
class PropertyOverrideTest {
// Bierzemy Environment jako najbardziej uczciwy sposób na sprawdzenie końcowych właściwości
@Autowired
Environment environment;
@Test
void overridesReportOutputDir() {
// Sprawdzamy nie zachowanie beana, lecz właśnie końcową wartość właściwości w środowisku
String dir = environment.getProperty("contextflow.report.output-dir");
assertEquals("build/test-reports", dir);
}
}
Zwróć uwagę na „poziom oczekiwań" w tym teście. Nie sprawdzamy, że katalog naprawdę jest stworzony i że coś do niego zapisano. To już weryfikacja zachowania scenariusza (i świadomie zostawimy ją następnemu wykładowi). Tu utrwalamy tylko fakt: w kontekście testowym wartość właściwości jest taka-to. To bardzo przydatne jako szybka diagnostyka: jeśli potem testy zaczęły pisać nie tam, szybko zrozumiesz, że na poziomie właściwości coś się zepsuło.
Oprócz inline overrides, @TestPropertySource potrafi podłączać plik testowych właściwości. Jest to wygodne, jeśli masz wiele testów i nie chcesz kopiować tego samego stringa po wszystkich klasach. Na przykład można stworzyć src/test/resources/contextflow-test-overrides.properties i podłączyć go:
import org.junit.jupiter.api.Test;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringJUnitConfig(classes = com.example.contextflow.config.core.ContextFlowAppConfig.class)
// Podłączamy osobny plik z testowymi nadpisaniami (leży w src/test/resources)
@TestPropertySource(locations = "/contextflow-test-overrides.properties")
class PropertyFileOverrideTest {
@Test
void contextStartsWithExtraTestProperties() {
// Smoke-weryfikacja: plik znaleziony i kontekst startuje z dodatkowymi właściwościami
assertTrue(true);
}
}
Ten test wygląda „pustym", i to normalne jako przykład edukacyjny: demonstruje mechanizm. W prawdziwym projekcie albo sprawdzisz konkretną właściwość, albo użyjesz tego pliku właściwości jako bazy dla testów scenariuszowych. Ale nawet „pusty" test bywa przydatny jako smoke na obecność pliku i poprawność ścieżek.
Priorytety właściwości
Właściwości w Spring — to jak warstwy odzieży: im bliżej ciała, tym trudniej zignorować. Ale dla tego wykładu przydatniejsze jest nie wkuwanie całego globalnego grafu precedencji Spring, lecz trzymanie w głowie trzech roboczych reguł.
- Zwykłe właściwości aplikacji, w tym wartości profile-specific, dają baseline.
- @TestPropertySource(locations = ...) nakłada powyżej testową warstwę opartą na pliku.
- @TestPropertySource(properties = ...) przykrywa wartości z file-based test source i jest wygodne dla punktowych nadpisań obok testu.
Dlatego inline overrides są takie wygodne: najbardziej lokalna i czytelna warstwa testowa żyje bezpośrednio w kodzie testu.
flowchart TD
A["Zwykłe właściwości aplikacji
(w tym profile-specific)"] --> B["@TestPropertySource(locations = ...)"]
B --> C["@TestPropertySource(properties = ...)"]
Właściwości systemowe i zmienne środowiskowe też mogą uczestniczyć w rozwiązywaniu właściwości, ale celowo nie robimy z nich codziennej dźwigni. Gdy tylko zarządzanie testem wyjeżdża do zewnętrznego środowiska, diagnostyka szybko staje się mętna: wartość niby „przyszła skądś", a obok testu tego już nie widać.
4. Test-specific @Configuration: testowe beany
Czasem profili i właściwości nadal brakuje. Przyczyna prosta: są zależności, które w „prawdziwej" aplikacji mają efekty uboczne, a w testach tylko przeszkadzają. Na przykład NotificationSender może pisać do konsoli (i będziesz widzieć śmieci w wydruku testów). Albo AuditWriter może pisać do pliku. Albo jakiś infrastrukturalny bean czyta zasoby, i w teście chcesz zastąpić go uproszczonym.
W świecie Boot często pokazuje się @MockBean. Ale my w kursie Spring Core, i naszym celem jest rozumienie kontenera, a nie chowanie wiringu za „magiczną adnotacją". Dlatego robimy uczciwie: dodajemy dodatkową testową konfigurację i rejestrujemy w niej stub lub no-op implementację.
Zazwyczaj wygląda to tak: w teście jest mała klasa @Configuration (często jako static inner class), która deklaruje testowe beany. Następnie test podnosi kontekst z dwóch konfiguracji: production ContextFlowAppConfig i testowego konfiga nadpisań.
Przykład: zastąpmy NotificationSender „ciszą" — no-op. A żeby kontener wybrał go przy autowiringu, oznaczymy jako @Primary.
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
class NoOpNotificationTestConfig {
@Bean
@Primary // W testach ten bean powinien „zwyciężyć" z wszelkimi implementacjami produkcyjnymi
NotificationSender notificationSender() {
// Implementacja no-op: usuwamy efekty uboczne (konsola/pliki/sieć) z testów
return message -> { /* intentionally no-op */ };
}
}
Wygląda śmiesznie, ale jest to użyteczna „pedagogiczna prostota". Masz port NotificationSender, i w testach dajesz mu implementację, która nic nie robi. Testy stają się cichsze, a co najważniejsze — bardziej przewidywalne: żadnych „przypadkowych" plików, konsoli, wywołań sieciowych i innych niespodzianek.
Podłącza się ta konfiguracja tak:
import com.example.contextflow.domain.ports.NotificationSender;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SpringJUnitConfig(classes = {
com.example.contextflow.config.core.ContextFlowAppConfig.class,
NoOpNotificationTestConfig.class // Domieszujemy testową konfigurację z podmianami
})
class TestSpecificConfigWiringTest {
// Wstrzykujemy port: chcemy się upewnić, że kontekst się złożył i bean jest znaleziony
@Autowired
NotificationSender notificationSender;
@Test
void testContextUsesNoOpNotificationSender() {
// Minimalna weryfikacja „kontekst startuje i zależność jest"
assertNotNull(notificationSender);
}
}
Ten przykład nie sprawdza „czy to naprawdę no-op", pokazuje zasadę: kontekst testowy można złożyć jako „bojowy plus nadkładka". W prawdziwym teście możesz dodatkowo sprawdzić typ lub zachowanie, ale najważniejszy jest sam mechanizm. I tak, tu używamy @Autowired w teście przez pole, i to jest normalne: klasa testowa — to nie kod produkcyjny, celem nie jest niezmienność, lecz wygodny dostęp do beanów kontekstu. Po prostu nie przenoś tego nawyku do serwisów aplikacji — one za to obrażają się i zaczynają żyć tajnym życiem.
5. Stub beans: podmiany dla sterowności
Stub — to nie straszne słowo. To nie „framework do mockowania", nie magia, nie alchemia. Stub — to po prostu prosta implementacja interfejsu, która zachowuje się przewidywalnie i często pomaga albo usunąć szum, albo utrwalić fakt wywołania. W naszym kursie świadomie stawiamy na takie proste podmiany, bo uczą myślenia architektonicznie: „mam port, a więc mogę podmienić implementację".
Niech mamy NotificationSender, i chcemy, żeby testowy sender niczego nie drukował, ale przy tym pozwalał zrozumieć „jaki sender w teście jest naprawdę używany". Zróbmy klasę stub, która zapamiętuje ostatnią wiadomość. To już trochę przypomina przygotowania do weryfikacji zachowania, ale w tym wykładzie użyjemy go właśnie jako narzędzie „kontrolowanego środowiska", a nie pełnoprawny test scenariuszowy.
import com.example.contextflow.domain.ports.NotificationSender;
class StubNotificationSender implements NotificationSender {
// Przechowujemy ostatnią wiadomość, żeby test mógł „zajrzeć do środka" i sprawdzić fakt wysłania
private String lastMessage;
@Override
public void send(String message) {
// Żadnych efektów ubocznych: tylko zapamiętujemy dane wejściowe
lastMessage = message;
}
String getLastMessage() {
// Wygodny getter dla testów
return lastMessage;
}
}
Teraz ten stub można zarejestrować przez test-specific config:
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
class StubNotificationConfig {
@Bean
@Primary // Ten stub powinien być głównym kandydatem dla NotificationSender w kontekście testowym
StubNotificationSender notificationSender() {
// Jeden bean wystarczy: można go wstrzykiwać i jako port, i jako konkretny stub
return new StubNotificationSender();
}
}
Tego jednego beana wystarczy. Kontener będzie mógł go oddać i jako NotificationSender, i jako StubNotificationSender, bo klasa konkretna implementuje potrzebny port. Podwójna rejestracja tylko by tu zamąciła obraz.
Jeśli teraz pomyślałeś: „zaczekaj, ale to już brzmi jak weryfikacja zachowania!" — masz rację. Dlatego w tym wykładzie ograniczymy się do tego, że stub pomaga uczynić środowisko kontrolowanym (wiemy, że nie będzie prawdziwych efektów ubocznych), a pełne weryfikacje scenariusza (stworzono zamówienie → uleciało zdarzenie → zadziałał listener) zostawimy następnemu wykładowi, gdzie będzie to głównym pytaniem edukacyjnym.
Główna dyscyplina dla stubów jest taka: powinny być głupie i przewidywalne. Jeśli napisałeś stub, który wewnątrz parsuje JSON, otwiera pliki i jeszcze wybiera implementację według profilu… gratulacje, właśnie napisałeś mini-aplikację wewnątrz testu. To zazwyczaj źle się kończy.
6. Składamy kontekst testowy ContextFlow
Gdy te trzy mechanizmy składają się razem, powstaje bardzo wygodny „testowy tryb składania" — w jednym miejscu utrwalasz: jaki profil, jakie właściwości i jakie zależności są podmienione. W efekcie test nie zależy od twojej maszyny i nie wymaga „właściwego nastroju" środowiska.
Złóżmy przykład „zarządzanego" kontekstu testowego, który robi trzy rzeczy jednocześnie: włącza profil test, nadpisuje ścieżkę wyjścia raportów i wyłącza powiadomienia przez stub/no-op.
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringJUnitConfig(classes = {
com.example.contextflow.config.core.ContextFlowAppConfig.class,
NoOpNotificationTestConfig.class // Podmiana, żeby testy były „ciche"
})
@ActiveProfiles("test") // Utrwalamy profil test: deterministyczność i minimum efektów ubocznych
@TestPropertySource(properties = "contextflow.report.output-dir=build/test-reports") // Piszemy raporty do osobnego katalogu
class ManagedEnvironmentSmokeTest {
@Test
void contextStartsInPredictableTestMode() {
// Smoke: szablon środowiska poprawnie się składa
assertTrue(true);
}
}
Znowu „pusty" test? Tak. I to normalne w formacie edukacyjnym: pokazuje „szablon" dla większości testów kontekstowych. Gdy zaczniesz pisać prawdziwe weryfikacje, będziesz dodawać @Autowired potrzebnych beanów i robić do nich assertions. Ale szablon środowiska pozostanie taki sam: profil + właściwości + testowe beany.
Jeśli chcesz uczynić ten test trochę bardziej sensownym, ale nadal nie wychodząc w pełny scenariusz, można dodać dwie miękkie weryfikacje: że profil jest aktywny i że nadpisanie właściwości zostało zastosowane. To weryfikacja środowiska, a nie logiki biznesowej:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringJUnitConfig(classes = com.example.contextflow.config.core.ContextFlowAppConfig.class)
@ActiveProfiles("test") // Chcemy testowy tryb składania
@TestPropertySource(properties = "contextflow.report.output-dir=build/test-reports") // I osobny output-dir dla raportów
class EnvironmentSanityTest {
// Sprawdzamy środowisko przez Environment: to „prawda" o profilach i właściwościach
@Autowired
Environment environment;
@Test
void profileAndPropertyAreApplied() {
// Profil jest naprawdę aktywny
assertTrue(environment.acceptsProfiles("test"));
// I właściwość naprawdę się nadpisała
assertEquals("build/test-reports",
environment.getProperty("contextflow.report.output-dir"));
}
}
Takie weryfikacje często stają się „siecią bezpieczeństwa". Jeśli za miesiąc zmienisz konfigurację i przypadkowo zepsujesz ładowanie property sources, ten test padnie szybko i zrozumiale: nie gdzieś głęboko w scenariuszu biznesowym, lecz bezpośrednio na poziomie „środowisko nie jest to".
7. Typowe błędy przy zarządzaniu środowiskiem testowym
Gdy zaczniesz aktywnie używać @ActiveProfiles, @TestPropertySource i stub beanów, prawie zawsze nadchodzi moment „dlaczego nie działa, przecież adnotację napisałem". To normalna faza rozwoju, mniej więcej jak „dlaczego git rebase wszystko zepsuł" — przez nią przechodzą wszyscy, którzy żyją w prawdziwym świecie. Ważne jest nie uczenie się magicznych zaklęć, lecz rozumienie przyczyn.
Błąd nr 1: testy uruchamiają się nie w profilu test, lecz „jak wyjdzie".
Jeśli polegasz na „domyślnym profilu" albo na tym, że IDE „jakoś ustawi spring.profiles.active", prawie na pewno dostaniesz nieprzewidywalność. Dziś test uruchomił się w dev, jutro — w ogóle bez profilu, a pojutrze na CI aktywował się inny zestaw. Najprostszy fix — jawnie stawiać @ActiveProfiles("test") tam, gdzie test potrzebuje właśnie testowego trybu składania.
Błąd nr 2: właściwości nadpisałeś, ale sprawdzasz nie to miejsce.
Nowicjusze czasem robią @TestPropertySource(...), a potem sprawdzają jakieś pole wewnątrz beana i dziwią się, że jest „stare". Często przyczyna jest banalna: albo bean jest tworzony nie z tej właściwości (inny klucz), albo wartość w beanie jest obliczana bardziej złożenie niż się wydaje. Do diagnostyki najpierw sprawdzaj Environment.getProperty(key) — to jest najbardziej uczciwe „źródło prawdy" dla wartości właściwości, a dopiero potem sprawdzaj zachowanie beana.
Błąd nr 3: nadpisałeś właściwość, ale nie rozumiesz, że przykrywa ją inna.
Jeśli w teście podłączyłeś plik właściwości przez @TestPropertySource(locations=...), a następnie gdzieś jeszcze dodałeś inline @TestPropertySource(properties=...), łatwo zapomnieć, kto kogo przykrywa. Staraj się trzymać nadpisania kompaktowymi i blisko testu, a jeśli nadpisań jest dużo — rób jeden „wspólny" plik dla pakietu testowego i w konkretnych testach dodawaj tylko punktowe zmiany inline.
Błąd nr 4: stub bean dodałeś, a kontener nadal wybiera „bojowy" bean.
Klasyk, gdy w kontekście pozostają dwie implementacje interfejsu, i autowiring wybiera nie tę. Najczęściej zapomniałeś @Primary na stubie, albo w kodzie produkcyjnym injection jest przez @Qualifier, który wskazuje na konkretną nazwę. Tu ważne jest pamiętanie: stub nie powinien być „kolejnym kandydatem", powinien być wybranym kandydatem. Osiągane jest to albo przez @Primary, albo przez uzgodniony @Qualifier, albo przez to, że podnosisz dla testu wąski kontekst, gdzie nie ma „bojowego" kandydata.
Błąd nr 5: testowe nadpisania zaczynają zamieniać się w osobną architekturę.
Gdy testowych konfiguracji staje się za dużo i zaczynają one duplikować połowę bojowego wiringu, wpadasz w pułapkę: testy przestają sprawdzać prawdziwą aplikację i zaczynają sprawdzać „testową wersję aplikacji". Dobra heurystyka jest taka: kontekst testowy powinien być maksymalnie podobny do produkcyjnego, ale z minimalnymi podmianami ze względu na deterministyczność i kontrolę efektów ubocznych. Jeśli przepisałeś połowę konfiguracji — testujesz już inny świat.
GO TO FULL VERSION