创建数据库集成测试
在本节中,我们将使用一个名为 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
测试类:
-
在
/test/
目录下的新包com.wordz.adapters.db
中创建一个新的测试类文件:Figure 1. Figure 14.1 – Integration testIDE 将为我们生成空的测试类。
-
在测试类上添加
@DBRider
和@DBUnit
注解@DBRider @DBUnit(caseSensitiveTableNames = true, caseInsensitiveStrategy = Orthography.LOWERCASE) public class WordRepositoryPostgresTest { }
@DBUnit
注解中的参数用于缓解 Postgres 和 DBRider 测试框架之间关于表和列名大小写敏感性的奇怪交互。 -
添加一个空的测试方法。我们想测试是否可以获取一个单词。添加一个空的测试方法:
@Test void fetchesWord() { }
-
运行测试,它会失败:
Figure 2. Figure 14.2 – DBRider cannot connect to the database -
按照 DBRider 文档,添加支持 DBRider 框架的代码。我们添加一个
connectionHolder
字段和一个javax.sql.DataSource
字段:@DBRider public class WordRepositoryPostgresTest { private DataSource dataSource; private final ConnectionHolder connectionHolder = () -> dataSource.getConnection(); }
dataSource
是 JDBC 标准方式,用于创建到 Postgres 数据库的连接。运行测试,它会失败并显示不同的错误消息:Figure 3. Figure 14.3 – dataSource is null -
我们通过添加一个
@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
)。 -
运行测试,它会失败:
Figure 4. Figure 14.4 – User does not exist错误的原因是我们的 Postgres 数据库中还没有
ciuser
用户。让我们创建一个。 -
打开
psql
终端并创建用户:create user ciuser with password 'cipassword';
-
再次运行测试
Figure 5. Figure 14.5 – Database not found失败的原因是
DBRider
框架试图将我们的新用户ciuser
连接到wordzdb
数据库,但该数据库不存在。 -
在
psql
终端中创建数据库create database wordzdb;
-
再次运行测试

fetchesWord()
测试现在通过了。我们注意到测试方法本身是空的,但这意味着我们已经完成了足够的数据库设置,可以继续测试驱动生产代码。我们很快会回到数据库设置,但现在我们将让测试驱动来指导我们。接下来的任务是为 fetchesWord()
测试添加缺失的 Arrange
、Act
和 Assert
代码。
驱动生产代码
我们的目标是测试驱动从数据库中获取单词的代码。我们希望这段代码位于一个实现 WordRepository
接口的类中,该接口我们在领域模型中定义。我们需要设计足够的数据库模式来支持这一点。通过从 Assert 步骤开始添加代码,我们可以快速驱动出一个实现。这是一种有用的技术——从断言开始编写测试,以便我们从期望的结果开始。然后我们可以向后工作,包括实现它所需的一切:
-
在
fetchesWord()
测试中添加 Assert 步骤@Test public void fetchesWord() { String actual = ""; assertThat(actual).isEqualTo("ARISE"); }
我们想检查是否可以从数据库中获取单词
ARISE
。这个测试失败了。我们需要创建一个类来包含必要的代码。 -
我们希望我们的新适配器类实现
WordRepository
接口,因此我们在测试的Arrange
步骤中驱动这个过程。@Test public void fetchesWord() { WordRepository repository = new WordRepositoryPostgres(); String actual = ""; assertThat(actual).isEqualTo("ARISE"); }
-
现在我们让 IDE 向导完成大部分创建新适配器类的工作。我们将它命名为
WordRepositoryPostgres
,这个名字既表示该类实现了WordRepository
接口,又表明它实现了对 Postgres 数据库的访问。我们使用 "New Class" 向导,并将其放在一个新包com.wordz.adapters.db
中。Figure 7. Figure 14.7 – New Class wizard这将生成一个空的类骨架:
package com.wordz.adapters.db; import com.wordz.domain.WordRepository; public class WordRepositoryPostgres implements WordRepository { }
-
IDE 会自动生成接口方法存根:
public class WordRepositoryPostgres implements WordRepository { @Override public String fetchWordByNumber(int number) { return null; } @Override public int highestWordNumber() { return 0; } }
-
回到我们的测试,我们可以添加 act 行,调用
fetchWordByNumber()
方法:@Test public void fetchesWord() { WordRepository repository = new WordRepositoryPostgres(); String actual = repository.fetchWordByNumber(27); assertThat(actual).isEqualTo("ARISE"); }
关于传递给
fetchWordByNumber()
方法的神秘常量 27 的解释。这是一个任意的数字,用于标识特定的单词。它的唯一硬性要求是,必须与稍后在 JSON 文件中看到的模拟测试数据中的单词编号一致。27 的实际值除了与模拟数据中的单词编号对齐外,没有其他意义。 -
为了让我们的类能够访问数据库,我们将
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 }
-
在我们的测试中,最后一步设置是将单词
ARISE
填充到数据库中。我们通过使用一个 JSON 文件来实现,DBRider
框架将在测试启动时将其应用到我们的数据库中。{ "word": [ { "word_number": 27, "word": "ARISE" } ] }
这里的
"word_number": 27
与测试代码中使用的值对应。 -
我们将文件命名为
wordTable.json
,并将其保存在测试目录的/resources/adapters/data
中:Figure 8. Figure 14.8 – Location of wordTable.json -
最后一步是将测试数据文件
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
接口的适配器类中的数据库访问。