Testy weryfikują co robi kod (zachowanie widoczne z zewnątrz), a nie jak to robi (szczegóły implementacji).
- Nie testuj prywatnych metod — jeśli logika jest złożona, wyodrębnij ją do osobnej klasy
- Nie sprawdzaj, ile razy wywołano metodę wewnętrzną — sprawdzaj efekt końcowy
- Refactoring kodu nie powinien wymagać zmian w testach, o ile zachowanie pozostaje takie samo
// ZLE - testujemy szczegół implementacji
verify(reservationRepository, times(1)).save(any());
// DOBRZE - testujemy efekt / stan
assertThat(reservation.getStatus()).isEqualTo(ReservationStatus.CONFIRMED);Każdy test musi być podzielony na trzy sekcje oddzielone komentarzami:
@Test
void shouldConfirmReservation_whenPaymentSucceeds() {
// given
Reservation reservation = buildPendingReservation();
// when
reservationService.confirm(reservation.getId());
// then
assertThat(reservation.getStatus()).isEqualTo(ReservationStatus.CONFIRMED);
}- // given — przygotowanie stanu, danych wejściowych, stubów
- // when — wywołanie testowanej operacji (dokładnie jedna akcja)
- // then — weryfikacja rezultatu
Format: should[OczekiwaneZachowanie]_when[Warunek]
shouldReturnAvailableSeats_whenScreeningExists()
shouldThrowException_whenSeatIsAlreadyLocked()
shouldExpireReservation_whenConfirmationWindowExceeded()
shouldReturn403_whenUserIsNotAdmin()- Nazwa testu powinna być czytelna jak zdanie w języku angielskim
- Unikaj nazw takich jak
test1,testBooking,checkReservation
Jeden test weryfikuje jedną rzecz. Dopuszczalne jest wiele asercji, jeśli razem opisują jeden spójny stan (np. obiekt po operacji), ale niedopuszczalne jest łączenie niezwiązanych asercji.
// DOBRZE - wszystkie asercje dotyczą stanu jednego obiektu po operacji
assertThat(reservation.getStatus()).isEqualTo(CONFIRMED);
assertThat(reservation.getConfirmedAt()).isNotNull();
// ZLE - dwie niepowiązane weryfikacje w jednym teście
assertThat(reservation.getStatus()).isEqualTo(CONFIRMED);
assertThat(emailService).isNotNull(); // niezwiązane z główną asercją- Testy logiki biznesowej: reguły rezerwacji, wyliczanie cen, walidacja blokad miejsc, expiry
- Testy klas domenowych:
ReservationService,SeatLockPolicy, obliczenia wReservation
- Używaj Mockito do zastępowania zależności (repozytoria, zewnętrzne serwisy)
- Nie mockuj prostych obiektów domenowych (value objects, encje, klasy bez zależności) — twórz je bezpośrednio
- Klasa testowa:
[NazwaKlasy]Test, np.ReservationServiceTest
@ExtendWith(MockitoExtension.class)
class ReservationServiceTest {
@Mock
private ReservationRepository reservationRepository;
@Mock
private SeatRepository seatRepository;
@InjectMocks
private ReservationService reservationService;
@Test
void shouldThrowException_whenSeatIsAlreadyLocked() {
// given
Seat seat = buildLockedSeat();
given(seatRepository.findById(seat.getId())).willReturn(Optional.of(seat));
// when
ThrowableAssert.ThrowingCallable action = () -> reservationService.lockSeat(seat.getId(), USER_ID);
// then
assertThatThrownBy(action)
.isInstanceOf(SeatAlreadyLockedException.class);
}
}// ZLE - mockowanie prostego obiektu domenowego
Seat seat = mock(Seat.class);
when(seat.isLocked()).thenReturn(true);
// DOBRZE - tworzenie obiektu bezpośrednio lub przez builder
Seat seat = Seat.builder()
.id(1L)
.status(SeatStatus.LOCKED)
.lockedUntil(LocalDateTime.now().plusMinutes(5))
.build();- Testy pełnego flow HTTP (od request do response)
- Weryfikacja integracji: kontroler → serwis → repozytorium → baza danych (H2)
- Testy reguł bezpieczeństwa (brak headera
X-User-Id, brak roli ADMIN)
- Używaj
@SpringBootTest+MockMvc— pełny kontekst aplikacji, baza H2 in-memory - Klasa testowa:
[NazwaKontrolera]IntegrationTest, np.ReservationControllerIntegrationTest - Każdy test powinien być niezależny — dane przygotowuj w
@BeforeEachlub wewnątrz testu
@SpringBootTest
@AutoConfigureMockMvc
class ReservationControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ScreeningRepository screeningRepository;
@Test
void shouldReturn400_whenXUserIdHeaderIsMissing() throws Exception {
// given
long screeningId = existingScreeningId();
// when
ResultActions result = mockMvc.perform(
post("/screenings/{id}/reservations", screeningId)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"seatIds\": [1]}")
);
// then
result.andExpect(status().isBadRequest());
}
@Test
void shouldReturn403_whenUserIsNotAdmin() throws Exception {
// given
long screeningId = existingScreeningId();
// when
ResultActions result = mockMvc.perform(
post("/screenings")
.header("X-User-Id", CUSTOMER_USER_ID)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{ "movieId": 1, "hallId": 1, "startsAt": "2025-06-01T18:00:00" }
""")
);
// then
result.andExpect(status().isForbidden());
}
}| Zasada | Opis |
|---|---|
| Zachowanie, nie implementacja | Weryfikuj efekty, nie wywołania metod wewnętrznych |
// given / // when / // then |
Obowiązkowy podział każdego testu |
| Nazewnictwo | should[Zachowanie]_when[Warunek] |
| Jeden assert koncepcyjnie | Jeden test = jedna weryfikowana właściwość systemu |
| Mockito w unit testach | Mockuj zależności (repo, serwisy), nie obiekty domenowe |
@SpringBootTest + MockMvc |
Wymagane dla testów integracyjnych HTTP |
| Brak mocków dla logiki domenowej | Proste klasy domenowe twórz bezpośrednio przez konstruktor/builder |