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/
Leave a comment