JUnit 5数据驱动测试入门指南:高效可扩展的测试方法

本文详细介绍了如何使用JUnit 5的@ParameterizedTest注解实现数据驱动测试,包括@EnumSource和@MethodSource的使用方法,通过实际代码示例展示如何提高测试覆盖率和效率。

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,增强测试覆盖率的可能性是广阔且可定制的。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计