## Dlaczego Testcontainers to game-changer
Każdy developer Java zna ten problem: piszesz świetne testy jednostkowe, wszystko przechodzi, a aplikacja w produkcji… nie działa. Dlaczego? Bo mock’owałeś bazę danych, Redis’a, Elasticsearch i wszystko inne. Testy były szybkie, ale nie sprawdzały rzeczywistych integracji.
Testcontainers rozwiązuje ten problem fundamentalnie – pozwala uruchomić prawdziwe serwisy w kontenerach Docker i testować przeciwko nim. To oznacza koniec z H2 „udającą” PostgreSQL’a i początek testów które rzeczywiście coś weryfikują.
Co się nauczysz:
- Jak uruchamiać prawdziwe bazy danych w kontenerach podczas testów
- Integrację Testcontainers z JUnit 4 i Spring Boot
- Konfigurację różnych typów kontenerów (PostgreSQL, Redis, Elasticsearch)
- Optymalizację czasu wykonywania testów z kontenerami
- Best practices dla testów integracyjnych z Testcontainers
Wymagania wstępne:
- Znajomość Java 8+ i Spring Boot 2.x
- Podstawowa wiedza o Docker i kontenerach
- Doświadczenie z testami JUnit
- Zainstalowany Docker na lokalnej maszynie
## Pierwsze kroki z Testcontainers
### Dodanie zależności do projektu
Zacznijmy od dodania Testcontainers do naszego projektu Maven:
org.testcontainers junit-jupiter 1.12.0 test org.testcontainers postgresql 1.12.0 test
### Pierwszy test z bazą danych
Zobaczmy jak wygląda podstawowy test z PostgreSQL:
@TestMethodOrder(OrderAnnotation.class) public class UserRepositoryIntegrationTest { @Container static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:11.5") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); private DataSource dataSource; private UserRepository userRepository; @BeforeAll static void startContainer() { postgres.start(); } @BeforeEach void setUp() { // Konfiguracja DataSource z parametrów kontenera HikariConfig config = new HikariConfig(); config.setJdbcUrl(postgres.getJdbcUrl()); config.setUsername(postgres.getUsername()); config.setPassword(postgres.getPassword()); dataSource = new HikariDataSource(config); userRepository = new UserRepository(dataSource); // Inicjalizacja schematu initializeSchema(); } @Test @Order(1) void shouldSaveAndFindUser() { // Given User user = new User("john.doe@example.com", "John", "Doe"); // When Long userId = userRepository.save(user); OptionalfoundUser = userRepository.findById(userId); // Then assertThat(foundUser).isPresent(); assertThat(foundUser.get().getEmail()).isEqualTo("john.doe@example.com"); assertThat(foundUser.get().getFirstName()).isEqualTo("John"); } private void initializeSchema() { try (Connection connection = dataSource.getConnection()) { connection.createStatement().execute( "CREATE TABLE users (" + "id SERIAL PRIMARY KEY, " + "email VARCHAR(255) UNIQUE NOT NULL, " + "first_name VARCHAR(100), " + "last_name VARCHAR(100), " + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)" ); } catch (SQLException e) { throw new RuntimeException("Failed to initialize schema", e); } } }
## Integracja ze Spring Boot
Spring Boot 2.x doskonale współpracuje z Testcontainers. Oto jak skonfigurować test integracyjny:
@SpringBootTest @TestPropertySource(properties = { "spring.datasource.url=jdbc:tc:postgresql:11.5:///testdb", "spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver" }) public class OrderServiceIntegrationTest { @Autowired private OrderService orderService; @Autowired private OrderRepository orderRepository; @Test void shouldCreateOrderWithItems() { // Given CreateOrderRequest request = CreateOrderRequest.builder() .customerId(1L) .items(Arrays.asList( new OrderItem("laptop", 2, new BigDecimal("2999.99")), new OrderItem("mouse", 1, new BigDecimal("99.99")) )) .build(); // When Order createdOrder = orderService.createOrder(request); // Then assertThat(createdOrder.getId()).isNotNull(); assertThat(createdOrder.getCustomerId()).isEqualTo(1L); assertThat(createdOrder.getItems()).hasSize(2); assertThat(createdOrder.getTotalAmount()).isEqualTo(new BigDecimal("6099.97")); // Verify persistence OptionalsavedOrder = orderRepository.findById(createdOrder.getId()); assertThat(savedOrder).isPresent(); assertThat(savedOrder.get().getItems()).hasSize(2); } }
## Różne typy kontenerów
### Redis dla cache’owania
public class CacheServiceIntegrationTest { @Container static GenericContainer> redis = new GenericContainer<>("redis:5.0.5-alpine") .withExposedPorts(6379); private RedisTemplateredisTemplate; private CacheService cacheService; @BeforeEach void setUp() { // Konfiguracja Redis connection LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory( redis.getContainerIpAddress(), redis.getMappedPort(6379) ); connectionFactory.afterPropertiesSet(); redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); cacheService = new CacheService(redisTemplate); } @Test void shouldCacheAndRetrieveData() { // Given String key = "user:123"; User user = new User("test@example.com", "Test", "User"); // When cacheService.put(key, user); Optional cachedUser = cacheService.get(key, User.class); // Then assertThat(cachedUser).isPresent(); assertThat(cachedUser.get().getEmail()).isEqualTo("test@example.com"); } }
### Elasticsearch dla wyszukiwania
public class ProductSearchIntegrationTest { @Container static ElasticsearchContainer elasticsearch = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.3.0") .withEnv("discovery.type", "single-node"); private RestHighLevelClient client; private ProductSearchService searchService; @BeforeEach void setUp() { client = new RestHighLevelClient( RestClient.builder(HttpHost.create(elasticsearch.getHttpHostAddress())) ); searchService = new ProductSearchService(client); // Create index setupProductIndex(); } @Test void shouldSearchProductsByName() throws Exception { // Given indexProduct(new Product(1L, "MacBook Pro", "Apple laptop", new BigDecimal("2999.99"))); indexProduct(new Product(2L, "MacBook Air", "Lightweight Apple laptop", new BigDecimal("1999.99"))); // Wait for indexing Thread.sleep(1000); // When Listresults = searchService.searchByName("MacBook"); // Then assertThat(results).hasSize(2); assertThat(results).extracting(Product::getName) .containsExactlyInAnyOrder("MacBook Pro", "MacBook Air"); } }
## Optymalizacja wydajności testów
### Reużycie kontenerów
Największym wyzwaniem z Testcontainers jest czas uruchamiania kontenerów. Oto kilka strategii optymalizacji:
@TestMethodOrder(OrderAnnotation.class) public class OptimizedIntegrationTest { // Statyczny kontener współdzielony między testami private static PostgreSQLContainer> postgres; @BeforeAll static void startSharedContainer() { if (postgres == null) { postgres = new PostgreSQLContainer<>("postgres:11.5") .withDatabaseName("testdb") .withUsername("test") .withPassword("test") .withReuse(true); // Eksperymentalna funkcja reużycia postgres.start(); } } @BeforeEach void cleanDatabase() { // Czyść bazę przed każdym testem zamiast recreate kontenera try (Connection conn = DriverManager.getConnection( postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword())) { conn.createStatement().execute("TRUNCATE TABLE users, orders CASCADE"); } catch (SQLException e) { throw new RuntimeException(e); } } }
### Równoległe wykonywanie testów
org.apache.maven.plugins maven-surefire-plugin 2.22.2 classes 4 true integration
## Best practices i pułapki
### Zarządzanie zasobami
public class ResourceManagementTest { @Test void testWithAutoCleanup() { // Try-with-resources automatycznie zamyka kontener try (PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:11.5")) { postgres.start(); // Twoje testy tutaj String jdbcUrl = postgres.getJdbcUrl(); // ... } // Kontener automatycznie zostanie zatrzymany } }
### Konfiguracja logowania
## Troubleshooting częstych problemów
### Problem z połączeniem do Docker daemon
@Test void verifyDockerEnvironment() { // Sprawdź dostępność Docker przed testami DockerClientFactory.instance().client(); // Test czy można uruchomić kontener try (GenericContainer> container = new GenericContainer<>("hello-world:latest")) { container.start(); assertThat(container.isRunning()).isTrue(); } }
### Timeouty i opóźnienia
@Container static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:11.5") .withStartupTimeout(Duration.ofMinutes(2)) // Zwiększ timeout dla wolnych maszyn .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*", 2));
Nie, kontenery są wolniejsze w starcie, ale oferują znacznie lepszą wierność testów. Trade-off między szybkością a dokładnością – wybierz to co jest ważniejsze dla Twojego projektu.
Upewnij się że Docker jest dostępny w CI (Docker-in-Docker lub Docker socket mounting). Rozważ użycie lżejszych obrazów jak Alpine variants dla szybszego startu.
Tak! Możesz uruchomić wiele kontenerów jednocześnie i testować komunikację między serwisami. Użyj Docker Compose support w Testcontainers.
Embedded databases (H2, HSQLDB), mock’i, lub dedykowane test environments. Każde podejście ma swoje plusy i minusy w kontekście wierności vs szybkości.
Użyj .withLogConsumer() do śledzenia logów kontenera, sprawdź porty z .getMappedPort(), i rozważ pozostawienie kontenerów do manual inspection.
Tak, ale wymagają Docker Desktop for Windows. Performance może być gorszy niż na Linux/macOS ze względu na wirtualizację.
Używaj .withCopyFileToContainer() dla skryptów SQL, lub programowo przez connection do bazy. Rozważ też volume mounting dla większych datasets.
🚀 Zadanie dla Ciebie
Stwórz test integracyjny dla aplikacji e-commerce która używa PostgreSQL do przechowywania zamówień i Redis do cache’owania produktów. Test powinien:
- Uruchomić oba kontenery jednocześnie
- Stworzyć zamówienie z 3 produktami
- Sprawdzić czy dane zostały poprawnie zapisane w bazie
- Zweryfikować czy cache został zaktualizowany
Bonus: Dodaj container health check który czeka na gotowość obu serwisów.
## Przydatne zasoby
– [Testcontainers Documentation](https://www.testcontainers.org/) – oficjalna dokumentacja
– [Testcontainers GitHub](https://github.com/testcontainers/testcontainers-java) – kod źródłowy i przykłady
– [Spring Boot Testcontainers Guide](https://spring.io/blog/2019/07/17/spring-boot-2-2-0-m4-available-now) – oficjalny przewodnik Spring
– [Docker Hub](https://hub.docker.com/) – repozytorium obrazów Docker
– [Testcontainers Slack](https://testcontainers.slack.com/) – społeczność użytkowników
Testcontainers to przyszłość testów integracyjnych w Java. Zamiast udawać rzeczywiste środowisko, po prostu je uruchamiasz. Czy Twoje testy są gotowe na prawdziwą infrastrukturę?