当你在数据库中编写存储过程和函数时,你想确保其正确性,就像你的Java代码一样。在Java中,这是用单元测试来完成的,通常是用JUnit。例如,如果你在Java中拥有以下代码:
public static int add(int a, int b) {
return a + b;
}
那么,你可能会写一个这样的测试:
@Test
public void testAdd() {
assertEquals(3, add(1, 2));
}
但在编写存储过程时,我们如何做到这一点呢?虽然存在一些针对某些数据库产品的单元测试库(例如用于Oracle的utPLSQL),但它们可能存在以下限制:
- 它们可能不像JUnit那样与你的Maven/Gradle构建紧密结合。
- 你的IDE可能不支持额外的视图,如Eclipse/IntelliJ的视图
- 你的数据库产品可能根本就没有这样的工具
- 你可能需要支持多个数据库产品,并且需要维护所有这些产品的测试,最好是用Java编写的。
- 你的程序集成测试可能与一些Java代码交互,所以你想用Java写测试。
我们想重新使用我们的Java测试基础设施,而不是通过JDBC直接绑定到程序上,但没有这样的麻烦。
使用jOOQ与testcontainers
Testcontainers是在Docker中进行数据库集成测试的一个越来越流行的框架。你可以快速启动一个数据库实例并部署你的数据库模式,包括你的存储过程、函数、包、用户定义类型等。例如,你可能决定将上述方法移入你的数据库,使用PostgreSQL:
CREATE OR REPLACE FUNCTION add(a integer, b integer)
RETURNS integer AS
$$
BEGIN
RETURN a + b;
END;
$$
LANGUAGE PLPGSQL;
现在,你可以用JDBC调用这个函数,就像这样:
try (CallableStatement s = connection.prepareCall(
"{ ? = call add(?, ?) }"
)) {
s.registerOutParameter(1, Types.INTEGER);
s.setInt(2, 1);
s.setInt(3, 2);
s.executeUpdate();
System.out.println(s.getInt(1));
}
上面的打印结果:
3
但这是大量的手动管道。每当你重构你的程序时,你的测试就会在运行时失败,而不是在编译时。而且你还得繁琐地更新上面的测试代码。
所以,为什么不直接使用jOOQ的代码生成器为你生成一个Routines 类,其中包含一个add() 方法。那个方法,你可以像这样调用。
System.out.println(Routines.add(configuration, 1, 2));
其中configuration 是包裹你的JDBCConnection 的一个jOOQ类型。你现在可以像这样设置你的JUnit测试,例如,使用JUnit的ClassRule 。
@ClassRule
public static PostgreSQLContainer<?> db =
new PostgreSQLContainer<>("postgres:14")
.withUsername("postgres")
.withDatabaseName("postgres")
.withPassword("test");
并使用db.getJdbcUrl() ,用jOOQ连接到PostgreSQL testcontainers数据库。然后你的最终测试看起来就像这样。
@Test
public void testAdd() {
try (CloseableDSLContext ctx = DSL.using(
db.getJdbcUrl(), "postgres", "test"
)) {
assertEquals(3, Routines.add(ctx.configuration(), 1, 2));
}
}
很明显,你会把DSLContext 的逻辑从单个测试中移到@Before 块中。与你所习惯的没有什么不同,对吗?请注意,即使是代码生成,我们也建议使用测试容器,作为你构建的一部分。如果你愿意的话,你可以在你的集成测试中重新使用代码生成器的testcontainers实例。