改了条库存 SQL,测试全绿。换到 MySQL 跑一下,才发现冲突时的更新走歪了。写 SQL 的时候,这种差别光靠 mock 不容易看出来。我们可以让测试自己起一个临时 MySQL,往同一个商品里加两次库存,再问数据库最后剩多少。
下面用 Spring Boot 3.5.x、JDK 17 或 21、Maven 和 MySQL 8.4。手边有个最小的 Spring Boot JDBC 项目就行,测试机需要能访问 Docker 并拉取镜像。
先让测试有自己的库
在项目的 pom.xml 里补齐这几项。已经有的依赖留一份即可。版本交给 Spring Boot 3.5 的 parent 或 BOM,这里用的是它管理的 Testcontainers 1.x 坐标。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
库存表只留两个字段。把下面这段存到 src/test/resources/schema-testcontainers.sql。已有 Flyway 或 Liquibase 的项目,直接用自己的迁移脚本建表更合适。这份小表用于独立的示例项目。
CREATE TABLE book_stock (
sku VARCHAR(64) PRIMARY KEY,
quantity INT NOT NULL
) ENGINE=InnoDB;
同一个 SKU 写两次,最后查一下
把测试类放在启动类所在的包或子包下。包名按你的项目填写。@JdbcTest 会加载 JDBC 相关的测试配置,@ServiceConnection 把容器的连接信息交给 Spring Boot。映射端口由容器分配,所以不必在测试里填本机的 3306。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.jdbc.core.JdbcTemplate;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
@JdbcTest(properties = {
"spring.sql.init.mode=always",
"spring.sql.init.schema-locations=classpath:schema-testcontainers.sql"
})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BookStockTest {
@Container
@ServiceConnection
static final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4");
@Autowired
JdbcTemplate jdbc;
@Test
void addsQuantityWhenSkuAlreadyExists() {
String sql = """
INSERT INTO book_stock (sku, quantity) VALUES (?, ?) AS incoming
ON DUPLICATE KEY UPDATE quantity = book_stock.quantity + incoming.quantity
""";
jdbc.update(sql, "book-001", 2);
jdbc.update(sql, "book-001", 3);
Integer quantity = jdbc.queryForObject(
"SELECT quantity FROM book_stock WHERE sku = ?",
Integer.class, "book-001"
);
assertThat(quantity).isEqualTo(5);
}
}
先跑这一条,别急着把整个测试目录改一遍。
mvn -Dtest=BookStockTest test
第一次运行可能在拉镜像。测试真正过关的标准是查出来等于 5。想确认断言有用,可以暂时把 SQL 改成 quantity = incoming.quantity,再跑一次。第二次写入会覆盖成 3,这时断言应该失败。改回累加后再跑一遍。
什么时候值得这么测
数据库版本、排序规则、唯一键和事务隔离级别,最好跟你的实际运行环境对齐。比如线上还是 MySQL 8.0,先用相同的小版本跑关键 SQL,别拿一个更新的镜像把兼容问题盖过去。这里的 mysql:8.4 是方便试用的系列标签,CI 需要稳定复现时可固定镜像版本。
跑不起来就看失败发生在哪一段。Docker 没连上或镜像拉不下来,先处理测试环境。库起来了却提示找不到表,再看初始化脚本的位置。SQL 跑完但结果不对,回到断言和实际的表约束。普通的金额计算、字符串处理照旧用小而快的单元测试。把这类容器测试留给确实要问 MySQL 的问题。
项目还没接 JDBC,可以先走一遍Spring Boot 连接 MySQL。
参考资料
Spring Boot 的 SQL,直接用 MySQL 测一遍
https://java.li/archives/spring-boot-mysql-testcontainers
评论