Testy integracyjne z użyciem Docker Test Containers

Testy integracyjne z użyciem Docker Test Containers

Pisząc testy integracyjne zależy nam aby zasymulować działanie serwisów zewnętrznych w taki sposób aby nie różniły się znacząco od tych z którymi będziemy mieli do czynienia na produkcji. Postawmy sobie pytanie: W jaki sposób zasymulować działanie bazy danych celem zweryfikowania poprawności komunikacji naszej aplikacji? W rozbudowanym ekosystemie Javy najpopularniejszym jest zastosowanie rozwiązania bazy danych in-memory (H2). Wystarczy dołączyć bibliotekę H2 Database jako zależność do projektu i mamy dostęp do bazy danych przechowywanej w pamięci. Problem pojawia się wtedy kiedy musimy wykorzystać niestandardowe zapytania SQL. Przykładem jest klauzula COLLATE która nie jest to interpretowana przez H2. Z tego artykułu dowiesz się w jaki sposób skonfigurować testy integracyjne z użyciem Docker Test Containers na przykładzie prostej aplikacji wykorzystującej REST API.

W pierwszej kolejności skonfigurujmy plik docker-compose.yml dla obrazu PostgreSQL:

version: '3.8'
services:
  postgres_db:
    container_name: 'postgresdb'
    image: postgres:14.2-alpine
    restart: always
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=book_db
    ports:
      - '5432:5432'

Uruchomienie obrazu:

docker-compose up -d

Skonfigurujmy niezbędne zależności – plik pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>pl.javaleader</groupId>
	<artifactId>test-containers</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>test-containers</name>
	<description>Demo project for Spring Boot</description>
 
	<properties>
		<java.version>8</java.version>
		<testcontainers.version>1.16.2</testcontainers.version>
	</properties>
 
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.testcontainers</groupId>
			<artifactId>junit-jupiter</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.testcontainers</groupId>
			<artifactId>postgresql</artifactId>
			<version>1.16.3</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>javax.validation</groupId>
			<artifactId>validation-api</artifactId>
			<scope>compile</scope>
		</dependency>
	</dependencies>
 
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.testcontainers</groupId>
				<artifactId>testcontainers-bom</artifactId>
				<version>${testcontainers.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
 
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-resources-plugin</artifactId>
				<version>3.1.0</version>
			</plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>9</source>
                    <target>9</target>
                </configuration>
            </plugin>
        </plugins>
	</build>
 
	<repositories>
		<repository>
			<id>Central Maven repository</id>
			<name>Central Maven repository https</name>
			<url>https://repo.maven.apache.org/maven2</url>
		</repository>
	</repositories>
 
	<pluginRepositories>
		<pluginRepository>
			<id>central</id>
			<name>Central Repository</name>
			<url>https://repo.maven.apache.org/maven2</url>
			<layout>default</layout>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
			<releases>
				<updatePolicy>never</updatePolicy>
			</releases>
		</pluginRepository>
	</pluginRepositories>
</project>

Utwórzmy teraz model dla naszej aplikacji:

@Entity
@Getter
@Setter
public class Book {
 
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "id", nullable = false)
    private Long id;
 
    private String author;
 
    private String title;
 
    @Column(name= "publication_year")
    private int year;
 
}

Repozytorium:

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {}

Serwis:

@Service
@RequiredArgsConstructor
public class BookService {
 
    public final BookRepository repository;
 
    public Book saveBook(Book book) {
        return this.repository.save(book);
    }
 
    public List<Book> getAllBooks() {
        return this.repository.findAll();
    }
}

Kontroler:

@RestController
@RequiredArgsConstructor
public class BookController {
 
    private final BookService service;
 
    @ResponseStatus(HttpStatus.CREATED)
    @PostMapping("/create/book")
    public Book createNewBook(@Valid @RequestBody Book book) {
        return this.service.saveBook(book);
    }
 
    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/fetch/books")
    public List<Book> getAllBooks() {
        return this.service.getAllBooks();
    }
}

Jak widać jest to prosta aplikacja wystawiająca REST API. Mamy wystawiony endpoint do tworzenia nowej książki oraz endpoint służący do pobrania wszystkich zapisanych w bazie danych książek. Poniżej jeszcze plik konfiguracyjny aplikacji application.properties:

spring.datasource.url: jdbc:postgresql://localhost:5432/book_db
spring.datasource.username: postgres
spring.datasource.password: postgres
 
spring.jpa.hibernate.ddl-auto = update
spring.jpa.hibernate.properties.dialect: org.hibernate.dialect.PostgreSQL82Dialect

Jako, że z użyciem Dockera uruchomiliśmy obraz bazy danych PostgreSQL to w powyższym pliku wskazujemy namiary na skonteneryzowaną bazę danych. Od tej pory nasza aplikacja działa już prawidłowo, ale nie ma jeszcze napisanych testów integracyjnych. Utwórzmy zatem klasę – AbstractIntegrationTest która będzie odpowiedzialna za obraz bazy danych:

postgres:14.2-alpine

wykorzystywany w testach integracyjnych.

public abstract class AbstractIntegrationTest {
 
    private static final PostgreSQLContainer POSTGRES_SQL_CONTAINER;
 
    static {
        POSTGRES_SQL_CONTAINER = new PostgreSQLContainer<>(DockerImageName.parse("postgres:14.2-alpine"));
        POSTGRES_SQL_CONTAINER.start();
    }
 
    @DynamicPropertySource
    static void overrideTestProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", POSTGRES_SQL_CONTAINER::getJdbcUrl);
        registry.add("spring.datasource.username", POSTGRES_SQL_CONTAINER::getUsername);
        registry.add("spring.datasource.password", POSTGRES_SQL_CONTAINER::getPassword);
    }
}

Napiszmy pierwszy test który zwerfikuje poprawność dodania nowej książki poprzez API:

@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class BookPostgresSQLTest extends AbstractIntegrationTest {
 
    @Autowired
    private BookRepository bookRepository;
 
    @Autowired
    private MockMvc mockMvc;
 
    @BeforeEach
    void setUp() {
        bookRepository.deleteAll();
    }
 
    @Test
    @Order(1)
    void should_be_able_to_save_one_book() throws Exception {
        // Given
        final var book = new Book();
        book.setAuthor("author");
        book.setTitle("title");
        book.setYear(2000);
 
        // When & Then
        mockMvc.perform(post("/create/book")
                .content(new ObjectMapper().writeValueAsString(book))
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.author").value("author"))
                .andExpect(jsonPath("$.title").value("title"))
                .andExpect(jsonPath("$.year").value("2000"));
    }
 
}

Drugi z kolei test zweryfikuje poprawność pobrania wszystkich książek poprzez API (książki zostaną dodane do bazy bezpośrednio z użyciem serwisu bookRepository):

@Test
@Order(2)
void shouldGetAllBooks() throws Exception {
    // Given
    bookRepository.saveAll(List.of(new Book(), new Book(), new Book()));
 
    // When
    mockMvc.perform(get("/fetch/books")
                    .accept(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$").isArray());
 
    // Then
    assertThat(bookRepository.findAll()).hasSize(3);
}

[źródło] https://1kevinson.com/integration-testing-with-springboot-docker-and-tests-containers/

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.

Leave a comment

Your email address will not be published.


*