使用 Testcontainers 测试 Redis

1、概览

Testcontainers 是一个用于创建临时 Docker 容器进行单元测试的 Java 库。当我们想要避免使用实际服务器进行测试时,它非常有用。

本文将会带你了解如何在 Spring Boot 中使用 Testcontainers 测试 Redis。

2、项目设置

使用任何测试容器的首要前提是在运行测试的机器上安装 Docker。

安装好 Docker 后,就可以开始设置 Spring Boot 应用了。

在此应用中,我们将设置一个 Redis Hash、一个 Repository 和一个使用 Repository 与 Redis 交互的 Service。

2.1、依赖

添加所需的 spring-boot-starter-testspring-boot-starter-data-redis 依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

接着,还要添加 Testcontainers 依赖:

<dependency> 
    <groupId>org.testcontainers</groupId> 
    <artifactId>testcontainers</artifactId> 
    <version>1.17.2</version> 
    <scope>test</scope> 
</dependency>

2.2、配置

application.properties 文件中添加 Redis 连接的详细信息:

spring.redis.host=127.0.0.1
spring.redis.port=6379

3、应用设置

我们要创建一个小型应用,向 Redis 数据库读写 Product(产品)。

3.1、Entity

创建 Product 类。

@RedisHash("product")
public class Product implements Serializable {
    private String id;
    private String name;
    private double price;

    // 构造函数、get、set 方法省略
}

@RedisHash 注解用于告诉 Spring Data Redis 该类应存储在 Redis Hash 中。

以 Hash 形式保存适用于不包含嵌套对象的实体。

3.2、Repository

接下来,为 Product Hash 定义一个 Repository:

@Repository
public interface ProductRepository extends CrudRepository<Product, String> {
}

CrudRepository 接口已经实现了 save、update、delete 和 findXxx 方法。

3.3、Service

最后,创建一个使用 ProductRepository 执行读写操作的 Service:

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Product getProduct(String id) {
        return productRepository.findById(id).orElse(null);
    }

    // 其他方法
}

然后,Controller 或其他 Service 就可以使用该 Service 对 Product 执行 CRUD 操作。

在实际应用中,这些方法可能包含更复杂的逻辑,但在本教程中,只关注 Redis 交互。

4、测试

现在,为 ProductService 编写测试,以测试 CRUD 操作。

4.1、测试 Service

ProductService 写一个集成测试:

@Test
void givenProductCreated_whenGettingProductById_thenProductExistsAndHasSameProperties() {
    Product product = new Product("1", "Test Product", 10.0);
    productService.createProduct(product);
    Product productFromDb = productService.getProduct("1");
    assertEquals("1", productFromDb.getId());
    assertEquals("Test Product", productFromDb.getName());
    assertEquals(10.0, productFromDb.getPrice());
}

测试假设在 properties 中指定的 URL 上运行了一个 Redis 数据库。如果没有运行 Redis 实例或服务器无法连接,测试就会出错。

4.2、使用 Testcontainers 运行一个 Redis 容器

我们可以在运行时启动一个 Redis 测试容器来进行测试,这需要改动一些代码。

创建和运行测试容器的代码:

static {
    GenericContainer<?> redis = 
      new GenericContainer<>(DockerImageName.parse("redis:5.0.3-alpine")).withExposedPorts(6379);
    redis.start();
}

如上:

  • redis:5.0.3-alpine 镜像中创建了一个新容器。
  • 默认情况下,Redis 实例在 6379 端口上运行。要暴露这个端口,可以使用 withExposedPorts() 方法。它会暴露该端口,并将其映射到主机上的一个随机端口。
  • start() 方法将启动容器并等待其准备就绪。
  • 将该代码添加到静态代码块中,以便在注入依赖和运行测试之前运行该代码。

4.3、更改连接信息

此时,我们已经运行了 Redis 容器,但还没有更改应用使用的连接信息。

只需使用系统属性(System Properties)覆盖 application.properties 文件中的连接详细信息即可:

static {
    GenericContainer<?> redis = 
      new GenericContainer<>(DockerImageName.parse("redis:5.0.3-alpine")).withExposedPorts(6379);
    redis.start();
    System.setProperty("spring.redis.host", redis.getHost());
    System.setProperty("spring.redis.port", redis.getMappedPort(6379).toString());
}

spring.redis.host 属性设置为容器的 IP 地址。

获取 6379 的映射端口,然后设置 spring.redis.port 属性。

现在,当测试运行时,将连接到容器上运行的 Redis 数据库。

4.4、Redis 容器的其他配置

另外,还可以通过 @Testcontainers 注解使用 Jupiter 集成来管理 Redis 容器的生命周期。使用该集成时,可以使用 @Container 注解来标记容器,以便进行生命周期管理。

接下来,我们使用这种方法为测试配置 Redis 容器。

首先,在项目的 pom.xml 文件中添加 junit-jupitertestcontainers-redis-junit-jupiter 依赖项:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.17.6</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>com.redis.testcontainers</groupId>
    <artifactId>testcontainers-redis-junit-jupiter</artifactId>
    <version>1.4.6</version>
    <scope>test</scope>
</dependency>

接下来,使用 @Container 注解来定义 REDIS_CONTAINER 静态字段:

@Container
private static final RedisContainer REDIS_CONTAINER = 
  new RedisContainer(DockerImageName.parse("redis:5.0.3-alpine")).withExposedPorts(6379);

注意,定义为静态字段的容器将在测试方法之间共享,并且只会启动一次。

此外,使用 @DynamicPropertySource 注解配置 registerRedisProperties() 方法,为应用配置连接属性:

@DynamicPropertySource
private static void registerRedisProperties(DynamicPropertyRegistry registry) {
    registry.add("spring.redis.host", REDIS_CONTAINER::getHost);
    registry.add("spring.redis.port", () -> REDIS_CONTAINER.getMappedPort(6379).toString());
}

最后,验证配置是否按预期运行:

@Test
void givenRedisContainerConfiguredWithDynamicProperties_whenCheckingRunningStatus_thenStatusIsRunning() {
    assertTrue(REDIS_CONTAINER.isRunning());
}

一切OK!我们可以看到,Redis 容器可用于测试方法。而且,无需更改其他测试方法,可以跟以前一样使用它们。


参考:https://www.baeldung.com/spring-boot-redis-testcontainers