如何使用 Testcontainers 和 PostgreSQL 编写集成测试
测试容器是测试应用程序的最有效工具之一。
扫码关注《Java学研大本营》,加入读者群,分享更多精彩
测试容器是测试应用程序的最有效工具之一。因为您可以在 prod 环境中如此接近地运行测试。您不需要模拟存储库层。
它有各种各样的容器替代品,例如数据库(SQL 和 NoSQL 选项)、消息传递、MockServer、AWS Localstack等。
这篇文章将解释如何使用 PostgreSQL 测试容器编写集成测试。
我们需要在我们的计算机上安装一个 Docker 引擎来进行测试。因此,您可以查看兼容性并下载所需的版本。
(https://www.testcontainers.org/supported_docker_environment/)
需要的依赖
可以看到相关的依赖,分别是Testcontainers和PostgreSQL。可以从 repo 中看到整个依赖项。
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</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>
实体
我们的 User 实体类如下。我们将在测试中从 dockerized PostgreSQL 数据库中获取这些数据。
package com.example.demo.model;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "app_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "status")
private int status;
}
初始脚本
此初始脚本放置在测试的存储库文件夹下。该脚本将在 dockerized PostgreSQL 实例中使用。我们将基本上创建一个表并插入两条记录。
CREATE TABLE app_user
(
id bigserial NOT NULL PRIMARY KEY,
name text NOT NULL,
status integer NOT NULL
);
insert into app_user (name, status)
values ('Turan', 1);
insert into app_user (name, status)
values ('Ulus', 1);
我们的测试类
如果在第一个测试中将数据插入到表中,我们将获取数据。在下面的测试中,我们将对用户的状态属性进行简单的计数检查。
在这两个测试中,我们启动了一个实际的 Web 应用程序实例。因此,这两个测试都通过 API 运行。
我们将在测试中使用的一些注释:
@Testcontainers:这个注解自动处理容器的生命周期。它负责启动和关闭我们测试中的每个容器。
@Container:标记要由 Testcontainers 扩展管理的容器。
@DynamicPropertySource:此注解用于分配外部属性。
package com.example.demo.controller;
import com.example.demo.model.User;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class UserControllerIntegrationTest {
@LocalServerPort private int port;
@Autowired protected TestRestTemplate testRestTemplate;
@Container
static final PostgreSQLContainer<?> postgreSQLContainer =
new PostgreSQLContainer<>("postgres:11.1")
.withUsername("sa")
.withPassword("password")
.withInitScript("init_script.sql")
.withDatabaseName("integration-tests-db");
@DynamicPropertySource
static void postgresqlProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
}
@Test
void testGetUserById_successful() {
ResponseEntity<User> response =
this.testRestTemplate.getForEntity("http://localhost:" + port + "/users/1", User.class);
Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
Assertions.assertEquals("Turan", response.getBody().getName());
}
@Test
void testCountByStatus_successful() {
ResponseEntity<Long> response =
this.testRestTemplate.getForEntity(
"http://localhost:" + port + "/users/count-by-status/1", Long.class);
Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
Assertions.assertEquals(2, response.getBody());
}
}package com.example.demo.controller;
import com.example.demo.model.User;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class UserControllerIntegrationTest {
@LocalServerPort private int port;
@Autowired protected TestRestTemplate testRestTemplate;
@Container
static final PostgreSQLContainer<?> postgreSQLContainer =
new PostgreSQLContainer<>("postgres:11.1")
.withUsername("sa")
.withPassword("password")
.withInitScript("init_script.sql")
.withDatabaseName("integration-tests-db");
@DynamicPropertySource
static void postgresqlProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
}
@Test
void testGetUserById_successful() {
ResponseEntity<User> response =
this.testRestTemplate.getForEntity("http://localhost:" + port + "/users/1", User.class);
Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
Assertions.assertEquals("Turan", response.getBody().getName());
}
@Test
void testCountByStatus_successful() {
ResponseEntity<Long> response =
this.testRestTemplate.getForEntity(
"http://localhost:" + port + "/users/count-by-status/1", Long.class);
Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
Assertions.assertEquals(2, response.getBody());
}
}
结论
您可以看到 Testcontainers 使创建准确的数据库变得简单。这些测试在 Docker 引擎基础设施和真实的 Web 应用程序(本例中为 Tomcat)上运行。因此,这可能是 CI/CP 管道的开销。应该针对需要的点运行类似于以前的测试。
参考文章: https://medium.com/@turanulus/how-to-write-an-integration-test-with-testcontainers-and-postgresql-67425e124753
推荐书单
1.《精通PostgreSQL 11(第2版)》
购买链接:https://item.jd.com/12859900.html
本书详细阐述了与PostgreSQL 11相关的基本解决方案,主要包括PostgreSQL概述、理解事务和锁定、使用索引、处理高级SQL、日志文件和系统统计信息、优化查询性能、编写存储过程、管理PostgreSQL的安全性、处理备份和恢复、理解备份与复制、选取有用的扩展、检修PostgreSQL、迁移到PostgreSQL等内容。此外,本书还提供了相应的示例、代码,以帮助读者进一步理解相关方案的实现过程。
本书适合作为高等院校计算机及相关专业的教材和教学参考书,也可作为相关开发人员的自学教材和参考手册。
本书帮助读者用新的PostgreSQL版本为企业应用构建动态数据库方案,能让数据库分析师轻松地设计物理和技术方面的系统架构。
2.《由浅入深PostgreSQL》
购买链接:https://item.jd.com/47420584201.html
本书从一位PostgreSQL 专家在多年咨询、技术支持工作中的切身体会出发,深入介绍了开源数据库管理系统PostgreSQL 9.6 版本中的主要特性,其内容涵盖了作为一个PostgreSQL 数据库从业人员经常会接触到的主题:事务和锁定、索引的使用、高级SQL 处理、日志文件和统计信息、查询优化、存储过程、安全性、备份与恢复、复制、各类扩展、故障排查、系统迁移。
作者通过亲身经历和直观的例子,详细介绍了PostgreSQL 主要特性的工作原理、常用配置以及常见的误区,是一本实用性很强的PostgreSQL 进阶指南,能帮助有一定PostgreSQL 知识的读者深入了解PostgreSQL 中更多更全面的高级特性。
本书适合数据库管理人员和开发人员了解和学习PostgreSQL。通过阅读本书,读者可以对PostgreSQL有一个全面透彻的了解。
3.《名师讲坛:Java微服务架构实战(SpringBoot+SpringCloud+Docker+RabbitMQ)》
购买链接:https://item.jd.com/12793864.html
Java微服务架构是当下流行的软件架构设计方案,可以快速地进行代码编写与开发,维护起来也非常方便。利用微架构技术,可以轻松地实现高可用、分布式、高性能的项目结构开发,同时也更加安全。
《名师讲坛:Java微服务架构实战(SpringBoot+SpringCloud+Docker+RabbitMQ)》一共15章,核心内容为SpringBoot、SpringCloud、Docker、RabbitMQ消息组件。其中,SpringBoot 是SpringMVC技术的延伸,使用它进行程序开发会更简单,服务整合也会更容易。SpringCloud是当前微架构的核心技术方案,属于SpringBoot的技术延伸,它可以整合云服务,基于RabbitMQ和GITHUB进行微服务管理。除此以外,该书还重点分析了OAuth统一认证服务的应用。
《名师讲坛:Java微服务架构实战(SpringBoot+SpringCloud+Docker+RabbitMQ)》适用于从事Java开发且有架构与项目重构需求的读者,也适用于相关技术爱好者,同时也可作为应用型高等院校及培训机构的学习教材。
精彩回顾
扫码关注《Java学研大本营》,加入读者群,分享更多精彩