Dlaczego znajomość różnic między testami jest kluczowa?
Pisanie testów bez zrozumienia ich typów to jak używanie młotka do wszystkiego – czasem zadziała, ale często będzie nieefektywne. Unit testy to jak sprawdzanie czy każda część samochodu działa osobno, Integration testy to testowanie czy silnik współpracuje z przekładnią. Oba są potrzebne, ale do różnych celów.
Co się nauczysz:
- Różnice między unit a integration testami
- Kiedy używać każdego typu testów
- Praktyczne przykłady z JUnit 4, Mockito i Spring Boot
- Test doubles: mocks, stubs, fakes
- Test Pyramid i testing strategy
Unit Tests – testowanie w izolacji
Charakterystyka unit testów
Aspekt | Unit Tests | Zalety | Wady |
---|---|---|---|
Scope | Pojedyncza klasa/metoda | Szybka identyfikacja błędów | Nie sprawdza integracji |
Dependencies | Wszystkie zmockowane | Deterministyczne | Mogą przegapić real-world issues |
Speed | Bardzo szybkie (<1ms) | Szybki feedback | – |
Maintenance | Łatwe w utrzymaniu | Proste do debug | Dużo mock setup |
Przykład unit testu z Mockito
// UserService.java - Klasa do testowania public class UserService { private final UserRepository userRepository; private final EmailService emailService; public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; this.emailService = emailService; } public User createUser(String email, String firstName, String lastName) { // Validation if (email == null || !email.contains("@")) { throw new IllegalArgumentException("Invalid email"); } // Business logic User user = new User(email, firstName, lastName); user.setActive(true); user.setCreatedAt(LocalDateTime.now()); // Save user User savedUser = userRepository.save(user); // Send welcome email emailService.sendWelcomeEmail(savedUser.getEmail(), savedUser.getFirstName()); return savedUser; } }
// UserServiceTest.java - Unit test z mockami import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.Mockito.*; import static org.junit.Assert.*; @RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserRepository userRepository; @Mock private EmailService emailService; private UserService userService; @Before public void setUp() { userService = new UserService(userRepository, emailService); } @Test public void shouldCreateUserWithValidData() { // Given String email = "john@example.com"; String firstName = "John"; String lastName = "Doe"; User expectedUser = new User(email, firstName, lastName); expectedUser.setId(1L); expectedUser.setActive(true); when(userRepository.save(any(User.class))).thenReturn(expectedUser); // When User result = userService.createUser(email, firstName, lastName); // Then assertNotNull(result); assertEquals(email, result.getEmail()); assertEquals(firstName, result.getFirstName()); assertTrue(result.isActive()); // Verify interactions verify(userRepository).save(any(User.class)); verify(emailService).sendWelcomeEmail(email, firstName); } @Test(expected = IllegalArgumentException.class) public void shouldThrowExceptionForInvalidEmail() { // When & Then userService.createUser("invalid-email", "John", "Doe"); } @Test public void shouldNotSendEmailWhenRepositoryFails() { // Given when(userRepository.save(any(User.class))) .thenThrow(new RuntimeException("Database error")); // When & Then try { userService.createUser("john@example.com", "John", "Doe"); fail("Expected exception"); } catch (RuntimeException e) { verify(emailService, never()).sendWelcomeEmail(anyString(), anyString()); } } }
Integration Tests – testowanie współpracy
Charakterystyka integration testów
Aspekt | Integration Tests | Zalety | Wady |
---|---|---|---|
Scope | Wiele komponentów razem | Realistyczne scenariusze | Trudniejsze debug |
Dependencies | Prawdziwe lub test doubles | Sprawdza real integration | Więcej setup |
Speed | Wolniejsze (sekundy) | Większe confidence | Feedback delay |
Maintenance | Trudniejsze w utrzymaniu | Catch więcej bugów | Flaky tests |
Spring Boot Integration Test
// UserControllerIntegrationTest.java import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.TestPropertySource; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(locations = "classpath:application-integrationtest.properties") public class UserControllerIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Autowired private UserRepository userRepository; @Test public void shouldCreateUserThroughAPI() { // Given String baseUrl = "http://localhost:" + port + "/api/users"; CreateUserRequest request = new CreateUserRequest(); request.setEmail("integration@example.com"); request.setFirstName("Integration"); request.setLastName("Test"); // When ResponseEntityresponse = restTemplate.postForEntity( baseUrl, request, User.class); // Then assertEquals(HttpStatus.CREATED, response.getStatusCode()); assertNotNull(response.getBody()); assertEquals("integration@example.com", response.getBody().getEmail()); // Verify in database Optional savedUser = userRepository.findByEmail("integration@example.com"); assertTrue(savedUser.isPresent()); assertTrue(savedUser.get().isActive()); } @Test public void shouldReturnBadRequestForInvalidEmail() { // Given String baseUrl = "http://localhost:" + port + "/api/users"; CreateUserRequest request = new CreateUserRequest(); request.setEmail("invalid-email"); request.setFirstName("John"); request.setLastName("Doe"); // When ResponseEntity response = restTemplate.postForEntity( baseUrl, request, String.class); // Then assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } }
Database Integration Test z H2
// UserRepositoryIntegrationTest.java @RunWith(SpringRunner.class) @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class UserRepositoryIntegrationTest { @Autowired private TestEntityManager entityManager; @Autowired private UserRepository userRepository; @Test public void shouldFindUserByEmail() { // Given User user = new User("test@example.com", "Test", "User"); entityManager.persistAndFlush(user); // When Optionalfound = userRepository.findByEmail("test@example.com"); // Then assertTrue(found.isPresent()); assertEquals("Test", found.get().getFirstName()); } @Test public void shouldReturnActiveUsersOnly() { // Given User activeUser = new User("active@example.com", "Active", "User"); activeUser.setActive(true); User inactiveUser = new User("inactive@example.com", "Inactive", "User"); inactiveUser.setActive(false); entityManager.persistAndFlush(activeUser); entityManager.persistAndFlush(inactiveUser); // When List activeUsers = userRepository.findByActiveTrue(); // Then assertEquals(1, activeUsers.size()); assertEquals("active@example.com", activeUsers.get(0).getEmail()); } }
Test Doubles – kiedy używać czego
Rodzaje test doubles
Type | Opis | Kiedy używać | Przykład |
---|---|---|---|
Mock | Sprawdza interactions | Gdy ważne są side effects | verify(emailService).send() |
Stub | Zwraca pre-defined responses | Gdy potrzebujesz specific return values | when(repo.find()).thenReturn(user) |
Fake | Working implementation (simplified) | Complex dependencies | In-memory database |
Spy | Partial mock (real object + some mocked methods) | Legacy code, selective mocking | spy(realService) |
Przykłady różnych test doubles
public class TestDoublesExample { @Test public void exampleWithMock() { // Mock - sprawdza czy metoda została wywołana EmailService emailMock = mock(EmailService.class); UserService userService = new UserService(userRepository, emailMock); userService.createUser("test@example.com", "John", "Doe"); verify(emailMock).sendWelcomeEmail("test@example.com", "John"); } @Test public void exampleWithStub() { // Stub - zwraca pre-defined values UserRepository repositoryStub = mock(UserRepository.class); User stubUser = new User("test@example.com", "John", "Doe"); when(repositoryStub.save(any(User.class))).thenReturn(stubUser); UserService userService = new UserService(repositoryStub, emailService); User result = userService.createUser("test@example.com", "John", "Doe"); assertEquals("John", result.getFirstName()); } @Test public void exampleWithFake() { // Fake - in-memory implementation UserRepository fakeRepository = new InMemoryUserRepository(); UserService userService = new UserService(fakeRepository, emailService); User result = userService.createUser("test@example.com", "John", "Doe"); // Fake ma working implementation assertEquals(1, fakeRepository.count()); } @Test public void exampleWithSpy() { // Spy - real object z niektórymi mocked methods UserService realService = new UserService(userRepository, emailService); UserService spyService = spy(realService); // Mock tylko specific method doReturn(true).when(spyService).isEmailUnique(anyString()); // Reszta methods użyje real implementation User result = spyService.createUser("test@example.com", "John", "Doe"); } }
Testing Strategy – Test Pyramid
Optymalne proporcje testów
/\ / \ / UI \ /Tests \ 10% - End-to-End Tests /________\ (Selenium, Cypress) / \ / Integration \ 20% - Integration Tests / Tests \ (Spring Boot, TestContainers) /________________\ / \ / Unit Tests \ 70% - Unit Tests /______________________\ (JUnit, Mockito)
Testing strategy guidelines
- Unit Tests (70%):
- Business logic
- Edge cases
- Error handling
- Algorithms
- Integration Tests (20%):
- Database operations
- REST API endpoints
- External service integration
- Configuration
- End-to-End Tests (10%):
- Critical user workflows
- Cross-system integration
- UI functionality
Best Practices
Unit Test best practices
// GOOD - Clear test name, single responsibility @Test public void shouldCalculateDiscountFor_PremiumCustomer_WhenOrderAbove100() { // Given Customer premiumCustomer = new Customer(CustomerType.PREMIUM); Order order = new Order(150.0); // When double discount = discountService.calculateDiscount(premiumCustomer, order); // Then assertEquals(15.0, discount, 0.01); } // BAD - Unclear name, testing multiple things @Test public void testDiscount() { Customer customer = new Customer(CustomerType.PREMIUM); Order order = new Order(150.0); double discount = discountService.calculateDiscount(customer, order); assertEquals(15.0, discount, 0.01); assertTrue(customer.isPremium()); // Testing different thing }
Integration Test best practices
# application-integrationtest.properties # Use different database for tests spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.jpa.hibernate.ddl-auto=create-drop # Disable external services external.email.service.enabled=false external.payment.service.mock=true # Faster test execution logging.level.org.springframework=WARN
Unit testy dla business logic, algorithms, edge cases. Integration testy dla database operations, REST endpoints, external integrations. Jeśli mockujesz więcej niż testujesz, prawdopodobnie potrzebujesz integration test.
Mockuj external dependencies (database, web services, file system), ale nie mockuj value objects ani simple collaborators. Over-mocking prowadzi do testów które testują mocks zamiast real behavior.
Użyj deterministic test data, avoid shared state, use test containers zamiast external services, add proper waits dla async operations, cleanup after each test.
Lepiej ich nie mieszać. Unit testy powinny być szybkie i niezależne, integration testy wymagają więcej setup. Separate test classes pozwalają na różne configuration i parallel execution.
Focus na test coverage of critical paths, nie na 100% line coverage. 80% coverage z dobrze napisanymi testami jest lepsze niż 100% z testami które testują getters/setters.
🚀 Zadanie dla Ciebie
Napisz kompletny test suite dla Order Management System:
- Unit testy: OrderService.calculateTotal() z różnymi discount rules
- Unit testy: OrderValidator.validate() z edge cases
- Integration test: OrderRepository database operations
- Integration test: REST API /orders endpoint
- Test strategy: Określ które parts wymagają unit vs integration testing
Use JUnit 4, Mockito, Spring Boot Test. Zmierz test execution time i sprawdź jak proporcje wpływają na feedback speed.
Przydatne zasoby:
Jakich proporcji unit vs integration testów używasz w swoich projektach? Które podejście sprawdza się najlepiej w twoim teamie?