如何用jOOQ集成测试存储程序

185 阅读3分钟

当你在数据库中编写存储过程和函数时,你想确保其正确性,就像你的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");

这里描述了如何用JUnit配置测试容器的另一种选择

并使用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实例。