创建数据库集成测试

在本节中,我们将使用一个名为 DBRider 的测试框架创建数据库集成测试的骨架。我们将使用此测试来驱动数据库表和数据库用户的创建。我们将致力于实现 WordRepository 接口,该接口将访问存储在 Postgres 数据库中的单词。

之前,我们为 Wordz 应用程序创建了一个领域模型,并使用六边形架构来指导我们。我们的领域模型没有直接访问数据库,而是使用了一个抽象,在六边形架构中称为端口。其中一个端口是 WordRepository 接口,它表示用于猜测的存储单词。

在六边形架构中,端口必须由适配器实现。WordRepository 接口的适配器将是一个实现该接口的类,其中包含访问真实数据库所需的所有代码。

为了测试驱动此适配器代码,我们将编写一个集成测试,使用一个支持数据库测试的库。这个库称为 DBRider,是项目 gradle.build 文件中列出的依赖项之一:

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
    testImplementation 'org.assertj:assertj-core:3.22.0'
    testImplementation 'org.mockito:mockito-core:4.8.0'
    testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0'
    testImplementation 'com.github.database-rider:rider-core:1.33.0'
    testImplementation 'com.github.database-rider:rider-junit5:1.33.0'
    implementation 'org.postgresql:postgresql:42.5.0'
}

DBRider 有一个名为 rider-junit5 的配套库,它与 JUnit5 集成。有了这个新的测试工具,我们可以开始编写测试。首先需要设置测试,以便它使用 DBRider 连接到我们的 Postgres 数据库。

使用DBRider创建数据库测试

在测试驱动任何应用程序代码之前,我们需要一个连接到本地运行的 Postgres 数据库的测试。我们按照通常的方式开始,编写一个 JUnit5 测试类:

  1. /test/ 目录下的新包 com.wordz.adapters.db 中创建一个新的测试类文件:

    image 2025 01 12 20 40 31 119
    Figure 1. Figure 14.1 – Integration test

    IDE 将为我们生成空的测试类。

  2. 在测试类上添加 @DBRider@DBUnit 注解

    @DBRider
    @DBUnit(caseSensitiveTableNames = true,
        caseInsensitiveStrategy = Orthography.LOWERCASE)
    public class WordRepositoryPostgresTest {
    }

    @DBUnit 注解中的参数用于缓解 PostgresDBRider 测试框架之间关于表和列名大小写敏感性的奇怪交互。

  3. 添加一个空的测试方法。我们想测试是否可以获取一个单词。添加一个空的测试方法:

    @Test
    void fetchesWord() {
    }
  4. 运行测试,它会失败:

    image 2025 01 12 20 46 19 391
    Figure 2. Figure 14.2 – DBRider cannot connect to the database
  5. 按照 DBRider 文档,添加支持 DBRider 框架的代码。我们添加一个 connectionHolder 字段和一个 javax.sql.DataSource 字段:

    @DBRider
    public class WordRepositoryPostgresTest {
        private DataSource dataSource;
    
        private final ConnectionHolder connectionHolder = () -> dataSource.getConnection();
    }

    dataSource 是 JDBC 标准方式,用于创建到 Postgres 数据库的连接。运行测试,它会失败并显示不同的错误消息:

    image 2025 01 12 20 47 54 778
    Figure 3. Figure 14.3 – dataSource is null
  6. 我们通过添加一个 @BeforeEach 方法来设置 dataSource

    @BeforeEach
    void setupConnection() {
        var ds = new PGSimpleDataSource();
        ds.setServerNames(new String[]{"localhost"});
        ds.setDatabaseName("wordzdb");
        ds.setCurrentSchema("public");
        ds.setUser("ciuser");
        ds.setPassword("cipassword");
    
        this.dataSource = ds;
    }

    这指定我们希望使用用户名为 ciuser、密码为 cipassword 的用户连接到名为 wordzdb 的数据库,该数据库运行在本地主机的默认 Postgres 端口(5432)。

  7. 运行测试,它会失败:

    image 2025 01 12 20 50 09 404
    Figure 4. Figure 14.4 – User does not exist

    错误的原因是我们的 Postgres 数据库中还没有 ciuser 用户。让我们创建一个。

  8. 打开 psql 终端并创建用户:

    create user ciuser with password 'cipassword';
  9. 再次运行测试

    image 2025 01 12 20 51 40 552
    Figure 5. Figure 14.5 – Database not found

    失败的原因是 DBRider 框架试图将我们的新用户 ciuser 连接到 wordzdb 数据库,但该数据库不存在。

  10. psql 终端中创建数据库

    create database wordzdb;
  11. 再次运行测试

image 2025 01 12 20 53 41 009
Figure 6. Figure 14.6 – Test passes

fetchesWord() 测试现在通过了。我们注意到测试方法本身是空的,但这意味着我们已经完成了足够的数据库设置,可以继续测试驱动生产代码。我们很快会回到数据库设置,但现在我们将让测试驱动来指导我们。接下来的任务是为 fetchesWord() 测试添加缺失的 ArrangeActAssert 代码。

驱动生产代码

我们的目标是测试驱动从数据库中获取单词的代码。我们希望这段代码位于一个实现 WordRepository 接口的类中,该接口我们在领域模型中定义。我们需要设计足够的数据库模式来支持这一点。通过从 Assert 步骤开始添加代码,我们可以快速驱动出一个实现。这是一种有用的技术——从断言开始编写测试,以便我们从期望的结果开始。然后我们可以向后工作,包括实现它所需的一切:

  1. fetchesWord() 测试中添加 Assert 步骤

    @Test
    public void fetchesWord() {
       String actual = "";
       assertThat(actual).isEqualTo("ARISE");
    }

    我们想检查是否可以从数据库中获取单词 ARISE。这个测试失败了。我们需要创建一个类来包含必要的代码。

  2. 我们希望我们的新适配器类实现 WordRepository 接口,因此我们在测试的 Arrange 步骤中驱动这个过程。

    @Test
    public void fetchesWord() {
       WordRepository repository
           = new WordRepositoryPostgres();
       String actual = "";
       assertThat(actual).isEqualTo("ARISE");
    }
  3. 现在我们让 IDE 向导完成大部分创建新适配器类的工作。我们将它命名为 WordRepositoryPostgres,这个名字既表示该类实现了 WordRepository 接口,又表明它实现了对 Postgres 数据库的访问。我们使用 "New Class" 向导,并将其放在一个新包 com.wordz.adapters.db 中。

    image 2025 01 12 20 58 20 946
    Figure 7. Figure 14.7 – New Class wizard

    这将生成一个空的类骨架:

       package com.wordz.adapters.db;
       import com.wordz.domain.WordRepository;
    
       public class WordRepositoryPostgres implements WordRepository {
       }
  4. IDE 会自动生成接口方法存根:

    public class WordRepositoryPostgres implements WordRepository {
       @Override
       public String fetchWordByNumber(int number) {
           return null;
       }
    
       @Override
       public int highestWordNumber() {
           return 0;
       }
    }
  5. 回到我们的测试,我们可以添加 act 行,调用 fetchWordByNumber() 方法:

    @Test
    public void fetchesWord() {
       WordRepository repository
           = new WordRepositoryPostgres();
    
       String actual = repository.fetchWordByNumber(27);
    
       assertThat(actual).isEqualTo("ARISE");
    }

    关于传递给 fetchWordByNumber() 方法的神秘常量 27 的解释。这是一个任意的数字,用于标识特定的单词。它的唯一硬性要求是,必须与稍后在 JSON 文件中看到的模拟测试数据中的单词编号一致。27 的实际值除了与模拟数据中的单词编号对齐外,没有其他意义。

  6. 为了让我们的类能够访问数据库,我们将 dataSource 传递给 WordRepositoryPostgres 构造函数:

    @Test
    public void fetchesWord() {
       WordRepository repository
           = new WordRepositoryPostgres(dataSource);
    
       String actual = repository.fetchWordByNumber(27);
    
       assertThat(actual).isEqualTo("ARISE");
    }

    这驱动出构造函数的更改:

    public WordRepositoryPostgres(DataSource dataSource) {
       // Not implemented
    }
  7. 在我们的测试中,最后一步设置是将单词 ARISE 填充到数据库中。我们通过使用一个 JSON 文件来实现,DBRider 框架将在测试启动时将其应用到我们的数据库中。

    {
       "word": [
           {
               "word_number": 27,
               "word": "ARISE"
           }
       ]
    }

    这里的 "word_number": 27 与测试代码中使用的值对应。

  8. 我们将文件命名为 wordTable.json,并将其保存在测试目录的 /resources/adapters/data 中:

    image 2025 01 12 21 04 52 876
    Figure 8. Figure 14.8 – Location of wordTable.json
  9. 最后一步是将测试数据文件 wordTable.json 链接到 fetchesWord() 测试方法。我们使用 DBRider@DataSet 注解来实现这一点:

    @Test
    @DataSet("adapters/data/wordTable.json")
    public void fetchesWord() {
       WordRepository repository
           = new WordRepositoryPostgres(dataSource);
    
       String actual = repository.fetchWordByNumber(27);
    
       assertThat(actual).isEqualTo("ARISE");
    }

现在测试失败了,我们可以通过编写数据库访问代码使其通过。在下一节中,我们将使用流行的 JDBI 库来实现 WordRepository 接口的适配器类中的数据库访问。