JUnit 5数据驱动测试入门指南:高效可扩展的测试方法
使用JUnit 5的@ParameterizedTest与@EnumSource和@MethodSource,通过多数据输入运行测试,提高测试覆盖率和应用健壮性。
在软件发展历史中,我们可以看到软件复杂性的增加,表现为更多的规则和条件。对于严重依赖数据库的现代应用,测试应用如何与其数据交互变得同等重要。数据驱动测试在这里扮演关键角色。
数据驱动测试通过支持多数据集测试来提高软件质量,这意味着同一测试使用不同的数据输入多次运行。自动化这些测试还能确保测试套件的可扩展性和可重复性,减少人为错误,提高生产力,节省时间,并确保同样的错误不会发生两次。
现代应用通常依赖数据库存储和操作关键数据;数据确实是任何现代应用的灵魂。因此,必须验证这些操作在一系列场景中是否正确运行。编写传统的单元测试通常不足,因为它们没有考虑真实世界应用遇到的数据可变性。这就是数据驱动测试的闪光点。
数据驱动测试是一种策略,其中同一测试使用不同的输入数据集多次运行。不是为每个数据变体编写单独的测试用例,而是使用一个测试方法并提供其他数据集进行测试。探索数据驱动测试不仅减少了测试代码的冗余,还通过确保系统在所有类型数据下按预期行为来提高测试覆盖率。
数据驱动测试流程
在本文中,我们将使用Java和Jupiter探索这一能力。
实时会话:使用Jakarta NoSQL和Jakarta Data实现数据驱动测试
在本节中,我们将通过一个使用Java SE、Jakarta NoSQL和Jakarta Data的实时示例来演示数据驱动测试。对于我们的示例,我们将构建一个简单的酒店管理系统,跟踪房间状态并与Oracle NoSQL作为数据库集成。
先决条件
在深入代码之前,确保Oracle NoSQL在云上或本地使用Docker运行。您可以通过运行以下命令快速启动Oracle NoSQL:
1
|
docker run -d --name oracle-instance -p 8080:8080 ghcr.io/oracle/nosql:latest-ce
|
一旦数据库启动并运行,我们就可以开始构建项目。
您还可以在GitHub上找到完整项目:Data-Driven Test with Oracle NoSQL
步骤1:结构实体
我们首先定义Room实体,它代表我们系统中的酒店房间。该实体使用@Entity注解映射到数据库,每个字段对应数据库中的一列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Entity
public class Room {
@Id
private String id;
@Column
private int number;
@Column
private RoomType type;
@Column
private RoomStatus status;
@Column
private CleanStatus cleanStatus;
@Column
private boolean smokingAllowed;
@Column
private boolean underMaintenance;
}
|
步骤2:房间仓库
接下来,我们创建RoomRepository接口,它使用Jakarta Data和NoSQL注解来定义各种房间相关操作的查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Repository
public interface RoomRepository {
@Query("WHERE type = 'VIP_SUITE' AND status = 'AVAILABLE' AND underMaintenance = false")
List<Room> findVipRoomsReadyForGuests();
@Query("WHERE type <> 'VIP_SUITE' AND status = 'AVAILABLE' AND cleanStatus = 'CLEAN'")
List<Room> findAvailableStandardRooms();
@Query("WHERE cleanStatus <> 'CLEAN' AND status <> 'OUT_OF_SERVICE'")
List<Room> findRoomsNeedingCleaning();
@Query("WHERE smokingAllowed = true AND status = 'AVAILABLE'")
List<Room> findAvailableSmokingRooms();
@Save
void save(List<Room> rooms);
@Save
Room newRoom(Room room);
void deleteBy();
@Query("WHERE type = :type")
List<Room> findByType(@Param("type") String type);
}
|
在这个仓库中,我们定义了多个查询来根据不同条件检索房间,例如查找可用房间、需要清洁的房间或允许吸烟的房间。我们还包括保存、删除和按类型查询房间的方法。
为了测试我们的仓库,我们希望确保使用测试容器而不是生产环境。为此,我们设置了一个DatabaseContainer单例,启动Oracle NoSQL容器用于测试目的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public enum DatabaseContainer {
INSTANCE;
private final GenericContainer<?> container = new GenericContainer<>(
DockerImageName.parse("ghcr.io/oracle/nosql:latest-ce"))
.withExposedPorts(8080);
{
container.start();
}
public DatabaseManager get(String database) {
DatabaseManagerFactory factory = managerFactory();
return factory.apply(database);
}
public DatabaseManagerFactory managerFactory() {
var configuration = DatabaseConfiguration.getConfiguration();
Settings settings = Settings.builder()
.put(OracleNoSQLConfigurations.HOST, host())
.build();
return configuration.apply(settings);
}
public String host() {
return "http://" + container.getHost() + ":" + container.getFirstMappedPort();
}
}
|
这个容器确保我们使用Oracle NoSQL数据库,它在Docker容器内运行,从而模拟类似生产的环境,同时为测试目的保持完全隔离。
步骤4:注入DatabaseManager
我们需要将DatabaseManager注入到我们的CDI上下文中。为此,我们创建一个ManagerSupplier类,确保DatabaseManager对我们的应用可用:
1
2
3
4
5
6
7
8
9
10
11
12
|
@ApplicationScoped
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
public class ManagerSupplier implements Supplier<DatabaseManager> {
@Produces
@Database(DatabaseType.DOCUMENT)
@Default
public DatabaseManager get() {
return DatabaseContainer.INSTANCE.get("hotel");
}
}
|
步骤5:使用JUnit 5的@ParameterizedTest编写数据驱动测试
在这一步,我们重点介绍如何使用JUnit 5的@ParameterizedTest注解编写数据驱动测试,并特别深入探讨RoomServiceTest中使用的类型。我们将探索@EnumSource和@MethodSource注解,所有这些都有助于使用不同的输入数据集多次运行同一测试方法。
让我们详细看看RoomServiceTest类中使用的类型:
1
2
3
4
5
6
|
@ParameterizedTest(name = "should find rooms by type {0}")
@EnumSource(RoomType.class)
void shouldFindRoomByType(RoomType type) {
List<Room> rooms = this.repository.findByType(type.name());
SoftAssertions.assertSoftly(softly -> softly.assertThat(rooms).allMatch(room -> room.getType().equals(type)));
}
|
@EnumSource(RoomType.class)注解用于自动提供RoomType枚举中的每个枚举常量给测试方法。在这种情况下,RoomType枚举包含像VIP_SUITE、STANDARD、SUITE等值。
这个注解导致测试方法为RoomType枚举中的每个值运行一次。每次测试运行时,type参数被分配一个枚举值,测试检查仓库返回的所有房间是否与提供的RoomType匹配。
当您想为枚举的所有可能值运行相同的测试逻辑时,这特别有用。它确保您的代码在枚举类型的所有变体上一致工作,最小化冗余测试用例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@ParameterizedTest
@MethodSource("room")
void shouldSaveRoom(Room room) {
Room updateRoom = this.repository.newRoom(room);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(updateRoom).isNotNull();
softly.assertThat(updateRoom.getId()).isNotNull();
softly.assertThat(updateRoom.getNumber()).isEqualTo(room.getNumber());
softly.assertThat(updateRoom.getType()).isEqualTo(room.getType());
softly.assertThat(updateRoom.getStatus()).isEqualTo(room.getStatus());
softly.assertThat(updateRoom.getCleanStatus()).isEqualTo(room.getCleanStatus());
softly.assertThat(updateRoom.isSmokingAllowed()).isEqualTo(room.isSmokingAllowed());
});
}
|
@MethodSource(“room”)注解指定测试方法应该使用room()方法提供的数据运行。该方法返回一个包含不同Room对象的参数流。
room()方法使用Faker生成随机房间数据,并为roomNumber、type、status等房间属性分配随机值。这些随机生成的房间一次一个传递给测试方法。
测试检查仓库中保存的房间是否与原始房间的属性匹配,确保保存操作按预期工作。
当您需要提供复杂或自定义测试数据时,@MethodSource是一个很好的选择。在这种情况下,我们使用随机数据生成来模拟不同的房间配置,确保我们的代码可以处理广泛的输入而无需冗余。
结论
在本文中,我们探讨了数据驱动测试的重要性以及如何使用JUnit 5(Jupiter)有效实现它。我们演示了如何利用参数化测试使用不同的输入多次运行同一测试,使我们的测试过程更高效、全面和可扩展。通过使用像@EnumSource、@MethodSource和@ArgumentsSource这样的注解,我们可以轻松地将多组数据传递给我们的测试方法,确保我们的应用在广泛的输入条件下按预期工作。
我们重点介绍了@EnumSource迭代枚举常量和@MethodSource为我们的测试生成自定义数据。这些工具,以及JUnit 5丰富的参数化测试源,如@ValueSource、@CsvSource和@ArgumentsSource,给我们灵活性来设计覆盖更广泛数据变体的测试。
通过整合这些技术,我们确保我们的仓库方法(和其他组件)是健壮的、适应性强的,并使用多样化的真实世界数据进行了彻底测试。这种方法显著提高了软件质量,减少了测试代码重复,并加速了测试过程。
数据驱动测试不仅仅是自动化测试;它通过考虑软件可能面临的各种真实世界条件,使这些测试更有意义。它是构建弹性应用的有价值策略,借助JUnit 5,增强测试覆盖率的可能性是广阔且可定制的。