Testy jednostkowe to metoda weryfikacji oprogramowania, w której sprawdza się poprawność działania poszczególnych modułów lub funkcji kodu w izolacji od reszty systemu. Według Martina Fowlera, 2026, testy jednostkowe są fundamentem CI/CD i refaktoryzacji, zapewniając szybkie informacje zwrotne o działaniu kodu. Testy modułowe pomagają wykrywać błędy na wczesnych etapach rozwoju, zmniejszając koszt ich naprawy dziesięciokrotnie.
Najważniejsze
Testy jednostkowe to proces sprawdzania poszczególnych jednostek (unit) kodu źródłowego — funkcji, metod, klas — w izolacji od reszty programu. Każdy test uruchamia konkretny scenariusz użycia modułu i sprawdza, czy wynik odpowiada oczekiwanemu. Testy jednostkowe pisze się w tym samym języku programowania co kod główny i wykonuje automatycznie w środowisku programistycznym lub w pipeline’ie CI/CD. W przeciwieństwie do testów integracyjnych, testy jednostkowe nie współdziałają z rzeczywistymi bazami danych, systemem plików ani usługami sieciowymi.
Głównym celem jest szybkie informacje zwrotne o poprawności kodu po zmianach. Jeśli programista refaktoryzuje metodę, zestaw testów jednostkowych potwierdza, że działanie nie zostało zepsute. Według Google Testing Blog (2025), projekty z pokryciem testami jednostkowymi powyżej 60% mają 2,5 razy mniej incydentów produkcyjnych. Dodatkowe zalety: dokumentacja kodu (testy pokazują, jak używać API), uproszczenie refaktoryzacji (można zmieniać implementację, zachowując działanie) i szybka diagnostyka regresji.
Nie każdy automatyczny test to test jednostkowy. Kryteria: testowany jest jeden moduł (klasa lub funkcja), zewnętrzne zależności zastąpiono makami lub stubami, test wykonuje się w milisekundach, nie wymaga uruchomienia serwera ani bazy danych. Test odwołujący się do rzeczywistej bazy danych to test integracyjny. Test otwierający przeglądarkę to test E2E. Zrozumienie granic między typami testów jest ważne dla prawidłowego rozkładu wysiłków w piramidzie testowania.
Wysokiej jakości testy jednostkowe przestrzegają zasad FIRST sformułowanych przez Roberta C. Martina. Każdy test powinien być Fast (szybki — milisekundy), Isolated (izolowany — nie zależy od innych testów), Repeatable (powtarzalny — taki sam wynik na każdej maszynie), Self-validating (samosprawdzający się — wynik „passed” lub „failed”, bez ręcznej weryfikacji) i Timely (terminowy — napisany przed lub jednocześnie z kodem). Naruszenie którejkolwiek zasady zmniejsza wartość testu.
Standardowy wzór do pisania testów jednostkowych. Arrange — przygotowanie danych i zależności: tworzenie obiektów, konfiguracja maków, ustawienie parametrów wejściowych. Act — wykonanie testowanej akcji: wywołanie metody lub funkcji. Assert — weryfikacja wyniku: porównanie rzeczywistej wartości z oczekiwaną. Podział na trzy bloki czyni test czytelnym i zrozumiałym. Jeśli blok Assert wymaga złożonej logiki — test prawdopodobnie sprawdza zbyt wiele naraz.
// Przykład testu jednostkowego według wzorca AAA w Kotlinie z JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — przygotowujemy testowany obiekt
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — wykonujemy akcję
val result = calculator.add(2, 3)
// ASSERT — sprawdzamy wynik
Assertions.assertEquals(5, result)
}
}
Nazwa testu powinna opisywać, co jest sprawdzane i jaki jest oczekiwany rezultat. Format: [methodName]_[scenario]_[expectedResult]. Przykład: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Dobra nazwa testu zastępuje komentarz i przy niepowodzeniu od razu wskazuje, która funkcjonalność jest naruszona. Unikaj nazw takich jak test1, checkSomething czy verify — nie niosą one informacji i utrudniają diagnostykę.
Do izolacji testowanego modułu od zewnętrznych zależności używa się dublerów testowych (test doubles). Główne typy: maki (mocks) — sprawdzają, czy określona metoda została wywołana z odpowiednimi parametrami; stuby (stubs) — zwracają zadane wartości przy wywołaniu metody; fake’i (fakes) — uproszczona implementacja rzeczywistego komponentu (np. InMemoryUserRepository zamiast UserRepository działającego z bazą danych). Wybór zależy od tego, co trzeba sprawdzić: stan (stub) czy interakcję (mak).
| Dubler | Co sprawdza | Przykład |
|---|---|---|
| Mak | Wywołanie metody z poprawnymi parametrami | userRepository.save(user) został wywołany dokładnie 1 raz |
| Stub | Zwracana wartość | repository.findById(1) zwraca User(id=1, name=„Test”) |
| Fake | Logika przez uproszczoną implementację | InMemoryMapUserRepository z HashMap zamiast bazy danych |
| Szpieg | Cięściowe makiowanie rzeczywistego obiektu | spy(repo).when(findById).thenReturn(user) |
Mockito to najpopularniejszy framework do makiowania w Javie i Kotlinie. Pozwala tworzyć maki przez mock(), konfigurować zwracanie wartości przez when().thenReturn() i sprawdzać wywołania przez verify(). Nowoczesne wersje Mockito (5.x) obsługują statyczne maki (mockStatic) i uproszczoną składnię przez BDDMockito (given-willReturn). Ważna zasada: nie makuj tego, co nie jest twoje — nie twórz maków dla obiektów wartościowych i standardowych bibliotek.
// Przykład testu jednostkowego z Mockito w Kotlinie
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) — metodyka, w której test pisze się przed implementacją kodu. Cykl „Red-Green-Refactor”: napisz test, który nie przechodzi (Red), napisz minimalny kod do przejścia testu (Green), popraw kod bez zmiany działania (Refactor). TDD gwarantuje, że cały kod jest pokryty testami (coverage = 100% dla napisanej funkcjonalności) i że kod jest testowalny — jeśli kod jest trudny do przetestowania, oznacza to, że architektura wymaga poprawy.
Według badań IBM (2006-2026, badanie podłużne), zespoły stosujące TDD popełniają o 40-80% mniej defektów w produkcji w porównaniu z zespołami piszącymi testy po kodzie. TDD poprawia również architekturę: programista musi myśleć o projekcie API przed implementacją, co prowadzi do słabej zależności (loose coupling) i wysokiej spójności (high cohesion). Dodatkowy efekt — dokumentacja żywym kodem: testy służą jako specyfikacja zachowania modułu, która jest zawsze aktualna.
TDD nie zawsze jest optymalne. Komponenty UI trudno testować w izolacji — dla nich skuteczniejsze są testy snapshot lub wizualne testy regresji (Percy, Chromatic). Prototypowanie i badania (spike solutions) nie wymagają testów. Legacy kod bez testów trudno pokryć przez TDD — tutaj najpierw potrzebne są characterization tests (testy utrwalające obecne zachowanie przed refaktoryzacją). W tych przypadkach TDD nie jest całkowicie odrzucane, ale adaptowane — pisze się testy dla zmienianej funkcjonalności, a nie dla całego legacy kodu.
Rozwój mobilny ma swoją specyfikę: logika biznesowa często jest wymieszana z kodem UI (Activity, ViewController, ViewModel), co utrudnia testy jednostkowe. Najlepszą praktyką są cienkie widoki, grube ViewModele: przenoś całą logikę z komponentów UI do osobnych klas (UseCase, Repository, ViewModel), które łatwo testować bez emulatora. Dla Android i iOS istnieją natywne frameworki testów jednostkowych działające na JVM/Native bez uruchamiania urządzenia.
Testy jednostkowe Androida wykonują się na lokalnej JVM bez emulatora, co zapewnia szybkość wykonania — typowy test zajmuje poniżej 100ms. JUnit 5 to główny runner. Do testów ViewModel używaj kotlinx-coroutines-test do testowania korutyn i Turbine do testowania StateFlow. Robolectric pozwala testować komponenty zależne od Androida (Context, Resources) bez emulatora, ładując shadow-klasy. Do testów Compose używaj Compose UI Test — ale to już testy UI, nie jednostkowe.
Testy jednostkowe iOS pisze się w Swift z XCTest (wbudowany w Xcode). Quick + Nimble — frameworki BDD do bardziej czytelnych testów (describe/context/it). Do makiowania używaj Cuckoo (generacja maków) lub SwiftyMocky. Swift obsługuje protokoły i wstrzykiwanie zależności, co ułatwia wymianę zależności. Kluczowe: testy jednostkowe iOS wykonują się na symulatorze macOS, a nie na rzeczywistym urządzeniu. Testy wymagające funkcji sprzętowych (aparat, Bluetooth) to testy integracyjne.
Testy jednostkowe Fluttera używają pakietu flutter_test i wykonują się na Dart VM bez emulatora. Do makiowania — pakiet mockito z generatorem kodu (build_runner). Testy widgetów (w tym samym pakiecie) testują poszczególne widgety, ale wymagają renderowania i działają wolniej — używaj ich tylko do sprawdzania logiki UI. Czysta logika Dart (modele, repozytoria, bloby) jest testowana jak zwykłe testy Dart bez importowania flutter_test.
// Przykład testu jednostkowego na Flutterze z mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
Efektywne testy jednostkowe wymagają dyscypliny. Główna zasada: testuj zachowanie, a nie implementację. Test nie powinien wiedzieć, jak moduł jest zaimplementowany wewnętrznie (jakie prywatne metody są wywoływane, w jakiej kolejności). Jeśli test jest związany z implementacją, psuje się przy każdej refaktoryzacji i traci wartość. Test sprawdza kontrakt: przy wejściu X powinno być wyjście Y. Wyjątek — testy dla algorytmów o krytycznej wydajności, gdzie ważna jest kolejność wywołań.
100% pokrycia to nieosiągalny i niepotrzebny cel. Według Google Testing Blog (2025), optymalny poziom pokrycia dla testów jednostkowych to 70-80% linii kodu. 100% pokrycia często osiąga się kosztem testowania getterów, setterów i konstruktorów, co nie przynosi wartości. Skupiaj się na krytycznej logice biznesowej: złożone obliczenia, walidacja, obsługa błędów, przypadki brzegowe. Używaj JaCoCo (Java), Coverage.py (Python), Istanbul (JS) do pomiaru i ustaw próg w CI — upadek builda przy pokryciu poniżej 60%.
Testy jednostkowe to pierwszy etap każdego pipeline’a CI/CD. Wykonują się przy każdym pushu do repozytorium, przed buildem i deployem. Średni czas wykonania testów jednostkowych w projekcie nie powinien przekraczać 5 minut — jeśli jest dłuższy, testy przestają być „szybkie” i programiści przestają je uruchamiać lokalnie. Dziel testy na szybkie (jednostkowe) i wolne (integracyjne) i uruchamiaj je w różnych etapach pipeline’a. Używaj równoległego wykonania i fail-fast dla przyspieszenia.
Często zadawane pytania
Test jednostkowy sprawdza jeden moduł w izolacji, zastępując zewnętrzne zależności makami. Test integracyjny sprawdza współdziałanie kilku rzeczywistych komponentów (bazy danych, API, system plików). Testy jednostkowe wykonują się w milisekundach, integracyjne — w sekundach. W piramidzie testowania testy jednostkowe zajmują 70%.
Wybór zależy od platformy: JUnit 5 dla Javy/Kotlina, XCTest dla iOS/Swift, pytest dla Pythona, Jest/Vitest dla JavaScript/TypeScript, flutter_test dla Fluttera. Do makiowania używaj Mockito (Java), Cuckoo (iOS), unittest.mock (Python) lub vitest.mock (JS). Wszystkie nowoczesne frameworki obsługują testy parametryzowane, wbudowane asercje i równoległe uruchamianie.
Fast — test wykonuje się w milisekundach. Isolated — nie zależy od innych testów i systemów zewnętrznych. Repeatable — daje taki sam wynik na każdej maszynie. Self-validating — sprawdza wynik automatycznie. Timely — napisany przed lub synchronicznie z kodem. Naruszenie choćby jednej zasady obniża efektywność testowania.
Tak, obowiązkowo. ViewModel zawiera logikę biznesową — obsługę zdarzeń, transformację danych, zarządzanie stanem. Na Androidzie używaj kotlinx-coroutines-test dla korutyn i Turbine do testowania StateFlow. Na iOS testuj Combine Publishers lub async/await w ViewModel. Testy ViewModel to czyste testy jednostkowe działające na JVM/macOS bez emulatora.
Zapytania sieciowe w testach jednostkowych nie są wykonywane — zastępuje się je makami klienta HTTP. Na Androidzie używaj MockWebServer (OkHttp) — uruchamia on lokalny serwer HTTP, co jest preferowane nad makami, ponieważ odtwarza rzeczywistą interakcje sieciową. MockWebServer zapewnia izolację bez utraty realizmu. Dla iOS — OHHTTPStubs lub URLProtocol do przechwytywania i podmiany odpowiedzi.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również