描述
一直以来集成测试所产生的脏数据
都是一个令人头疼的问题。而Testcontainers
正好可以被用来解决这一问题。Testcontainers
的原理时在运行测试时使用Docker
容器中的数据库,测试结束后容器就会被被清除,这使得每次运行测试时数据库都处在一个干净
的状态。
Testcontainers
支持Mysql
、Postgres
、MongoDB
等常见数据库,并且可以很好的和JUnit 4
、Jupiter/JUnit 5
、Spock
等测试框架协作。本文以Spring data jpa
+ Mysql
为例。编写订单
和订单项
场景下的增删改查集成测试。
Testcontainers使用示例
新建项目
新建一个web项目并选择相关依赖。这里使用Spring Initializr
初始化项目。
添加Testcontainers
依赖
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.10.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.10.7</version>
<scope>test</scope>
</dependency>
创建实体
以订单
和订单项
的场景为例,创建一个订单实体和一个订单项实体。使用@OneToMany
注解描述订单
和订单项
的关系为一对多
。
@Data
@Entity
@NoArgsConstructor
public class OrderInfo {
public OrderInfo(Set<OrderItem> orderItems){
orderItems.forEach(this::addOrderItem);
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 总价
*/
private BigDecimal totalPrice = BigDecimal.ZERO;
/**
* 所有订单项
*/
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "order_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Set<OrderItem> orderItems = new HashSet<>();
/**
* 新增订单项
*/
public OrderInfo addOrderItem(OrderItem orderItem){
this.orderItems.add(orderItem);
BigDecimal orderItemTotalPrice = orderItem.getUnitPrice().multiply(BigDecimal.valueOf(orderItem.getQuantity()));
this.totalPrice = this.totalPrice.add(orderItemTotalPrice);
return this;
}
}
@Data
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 商品名
*/
private String tradeName;
/**
* 数量
*/
private Integer quantity;
/**
* 单价
*/
private BigDecimal unitPrice;
}
创建实体的Repository
public interface OrderRepo extends PagingAndSortingRepository<OrderInfo, Long> {
}
编写service
编写简单的增删改查功能
public interface OrderService {
OrderInfo getOrderById(Long id);
OrderInfo addOrder(OrderInfo orderEntity);
void delOrder(Long id);
OrderInfo addOrderItem(Long orderId, OrderItem orderItem);
}
@Service
@AllArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderRepo orderRepo;
@Override
public OrderInfo getOrderById(Long id) {
return orderRepo.findById(id).orElse(null);
}
@Override
public OrderInfo addOrder(OrderInfo orderEntity) {
return orderRepo.save(orderEntity);
}
@Override
public void delOrder(Long id) {
orderRepo.deleteById(id);
}
@Override
public OrderInfo addOrderItem(Long orderId, OrderItem orderItem) {
OrderInfo orderInfo = orderRepo.findById(orderId).orElseThrow(() -> new RuntimeException("订单不存在"));
orderInfo.addOrderItem(orderItem);
return orderRepo.save(orderInfo);
}
}
编写测试代码
编写配置文件
在test
目录下的resources
目录下创建application.yml
文件,并写入一下配置。
spring:
jpa:
properties:
hibernate:
dialect: com.example.integrationtestdemo.config.MysqlConfig # 创建表时使用自定义的编码和存储引擎
generate-ddl: true # 构建ddl语句
show-sql: true # 打印sql
hibernate:
ddl-auto: create # 自动创建数据库表
配置jpa
生成数据库表的编码
和存储引擎
public class MysqlConfig extends MySQL57Dialect {
@Override
public String getTableTypeString() {
// 配置使用`InnoDB`存储引擎和utf8mb4编码
return " ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
}
}
创建一个测试基类
创建测试基类
,在这个基类
中启动mysql容器
供测试使用并将相关数据源配置注入spring
环境中。之后在进行测试时只需要继承这个基类
即可
@SpringBootTest
@ContextConfiguration(initializers = IntegrationTestBase.DockerMySQLDataSourceInitializer.class) // 注入数据源
public class IntegrationTestBase {
public static final MySQLContainer<?> mysql;
static {
// 配置使用mysql:5.7.30镜像,并创建使用jdbc数据库
mysql = new MySQLContainer<>("mysql:5.7.30").withDatabaseName("jdbc");
mysql.start();
}
public static class DockerMySQLDataSourceInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// 注入数据源
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
applicationContext,
"spring.datasource.url=" + mysql.getJdbcUrl() + "?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai",
"spring.datasource.username=" + mysql.getUsername(),
"spring.datasource.password=" + mysql.getPassword(),
"spring.datasource.driver-class-name=" + mysql.getDriverClassName()
);
}
}
}
继承这个基类实现集成测试
通过集成上面编写的基类即可进行集成测试。为了使用测试的方法在一个环境中运行可以在测试类上添加注解@TestInstance(TestInstance.Lifecycle.PER_CLASS)
。
// 使测试类中所有方法在一个测试环境中运行
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class OrderInfoServiceTest extends IntegrationTestBase {
@Autowired
private OrderService orderService;
// 记录保存的对象,以便查询时进行比较
private OrderInfo orderInfo;
@Test
@Order(1)
void addOrder() {
OrderItem bookOrderItem = OrderItem.builder()
.tradeName("书籍")
.quantity(3)
.unitPrice(BigDecimal.valueOf(10))
.build();
OrderItem notebookOrderItem = OrderItem.builder()
.tradeName("笔记本")
.quantity(5)
.unitPrice(BigDecimal.valueOf(20))
.build();
Set<OrderItem> bookOrderItems = Set.of(bookOrderItem, notebookOrderItem);
OrderInfo orderInfo = new OrderInfo(bookOrderItems);
this.orderInfo = orderService.addOrder(orderInfo);
assert orderInfo.getId() != null : "新增失败";
}
@Test
@Order(2)
void getOrderById() {
OrderInfo orderInfo = orderService.getOrderById(this.orderInfo.getId());
assert orderInfo != null : "通过id未找到数据";
assert this.orderInfo.getTotalPrice().compareTo(orderInfo.getTotalPrice()) == 0 : "数据不一致";
assert this.orderInfo.getOrderItems().size() == orderInfo.getOrderItems().size() : "数据不一致";
}
@Test
@Order(3)
void addOrderItem() {
OrderItem pictureAlbumOrderItem = OrderItem.builder()
.tradeName("画册")
.quantity(3)
.unitPrice(BigDecimal.valueOf(50))
.build();
this.orderInfo = orderService.addOrderItem(this.orderInfo.getId(), pictureAlbumOrderItem);
OrderInfo orderInfo = orderService.getOrderById(this.orderInfo.getId());
assert this.orderInfo.getTotalPrice().compareTo(orderInfo.getTotalPrice()) == 0 : "数据不一致";
assert this.orderInfo.getOrderItems().size() == orderInfo.getOrderItems().size() : "数据不一致";
}
@Test
@Order(4)
void delOrder() {
orderService.delOrder(this.orderInfo.getId());
OrderInfo orderInfo = orderService.getOrderById(this.orderInfo.getId());
assert orderInfo == null : "删除订单失败";
}
}
项目结构如下:
测试结果
运行测试时会在本地Dokcer环境中启动Docker容器
在运行日志中也可以看到启动的容器信息
运行结果如下: