testcontainer

在我们的项目中,单元测试是保证我们代码质量非常重要的一环,但是我们的业务代码不可避免的需要依赖外部的系统或服务如DB,redis,其他外部服务等。如何保证我们的测试代码不受外部依赖的影响,能够稳定的运行成为了一件比较让人头疼的事情。

mock

通过mockito等框架,可以模拟外部依赖各类组件的返回值,通过隔离的方式稳定我们的单元测试。但是这种方式也会带来以下问题:

  1. 测试代码冗长难懂,因为要在测试中模拟各类返回值,一行业务代码往往需要很多测试代码来支撑
  2. 无法真实的执行SQL脚本
  3. 仅仅适用于单元测试,对于端到端的测试无能为力

testcontainer

testcontainer,人如其名,可以在启动测试时,创建我们所依赖的外部容器,在测试结束时自动销毁,通过容器的技术来达成测试环境的隔离。

目前testcontainer仅支持docker,使用testcontainer需要提前安装docker环境

简单的例子

假设我们有一个springboot的web项目,在这个项目中使用postgresql作为数据库
首先我们将testcontainer添加到pom.xml中

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

然后是我们的entity对象和Repository

@Entity
@Table(name = "customers")
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    public Customer() {}

    public Customer(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

public interface CustomerRepository extends JpaRepository<Customer, Long> {
}

最后是我们的controller

@RestController
public class CustomerController {

    private final CustomerRepository repo;

    CustomerController(CustomerRepository repo) {
        this.repo = repo;
    }

    @GetMapping("/api/customers")
    List<Customer> getAll() {
        return repo.findAll();
    }
}

由于是demo,我们通过src/resources/schema.sql来进行表的初始化,在正常项目中应该通过flyway等进行数据库的管理

create table if not exists customers (
    id bigserial not null,
    name varchar not null,
    email varchar not null,
    primary key (id),
    UNIQUE (email)
);

并在application.properties中添加如下配置

spring.sql.init.mode=always

最后是重头戏,如果通过testcontainer来完成我们的端到端测试

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.hasSize;

import com.example.testcontainer.domain.Customer;
import com.example.testcontainer.repo.CustomerRepository;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CustomerControllerTest {

    @LocalServerPort
    private Integer port;

    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            "postgres:14"
    );

    @BeforeAll
    static void beforeAll() {
        postgres.start();
    }

    @AfterAll
    static void afterAll() {
        postgres.stop();
    }

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    CustomerRepository customerRepository;

    @BeforeEach
    void setUp() {
        RestAssured.baseURI = "http://localhost:" + port;
        customerRepository.deleteAll();
    }

    @Test
    void shouldGetAllCustomers() {
        List<Customer> customers = List.of(
                new Customer(null, "John", "john@mail.com"),
                new Customer(null, "Dennis", "dennis@mail.com")
        );
        customerRepository.saveAll(customers);

        given()
                .contentType(ContentType.JSON)
                .when()
                .get("/api/customers")
                .then()
                .statusCode(200)
                .body(".", hasSize(2));
    }
}

static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
“postgres:14”
);`

这里创建了一个postgres的容器,然后通过 @DynamicPropertySourcespring.datasource的各项参数赋值,剩下的就由Spring的auto-configure完成各类bean的自动装配,JPA的装配和注入。

除了通过 @DynamicPropertySource外,spring-boot-testcontainers提供了一些方法可以更加简化这个流程
首先添加依赖

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-testcontainers</artifactId>
            <scope>test</scope>
        </dependency>
@Testcontainers
@SpringBootTest
public class MyIntegrationServiceConnectionTests {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(
            "postgres:14"
    );

    @Autowired
    CustomerRepository customerRepository;

    @Test
    void shouldGetAllCustomers() {
        List<Customer> customers = List.of(
                new Customer(null, "John", "john@mail.com"),
                new Customer(null, "Dennis", "dennis@mail.com")
        );
        customerRepository.saveAll(customers);

        assertTrue(customerRepository.findByName("John").isPresent());
        assertEquals(2,customerRepository.findAll().size());
    }
}

@Testcontainers注解会帮助我们在测试开始前启动容器,在测试结束后停止容器,因此我们可以省略@BeforeAll和@AfterAll

@ServiceConnection取代了 @DynamicPropertySource中代码的功能,帮助我们完成bean的创建

除此之外,我们也可以通过@bean的方式来创建容器

@Testcontainers
@SpringBootTest
public class MyIntegrationBeanConfiguratonTests {


    @Autowired
    CustomerRepository customerRepository;

    @Test
    void shouldGetAllCustomers() {
        List<Customer> customers = List.of(
                new Customer(null, "John", "john@mail.com"),
                new Customer(null, "Dennis", "dennis@mail.com")
        );
        customerRepository.saveAll(customers);

        assertTrue(customerRepository.findByName("John").isPresent());
        assertEquals(2,customerRepository.findAll().size());
    }

    @TestConfiguration(proxyBeanMethods = false)
    public  static class MyPostgreConfiguration {

        @Bean
        @ServiceConnection
        public PostgreSQLContainer<?> postgreSQLContainer() {
            return new PostgreSQLContainer<>("postgres:14");
        }

    }
}

连接web服务

PostgreSQLContainer是一个专门用于连接PostgreSQL的类,除此之外testcontainer还提供了redis、mysql、es、kafka等常用中间件的容器类。如果我们想要去连接一个内部的web服务,那该怎么做?

首先,我们要确保该服务已经容器化,可以直接通过docker来启动,其次,testcontainer提供了一个公共的GenericContainer来处理这类场景,

假设我们有一个服务镜像demo/demo:latest,该服务暴露了一个8080端口,我们通过feignclient来访问

@FeignClient(name = "customFeign",url = "${remote.custom.url}")
public interface ExternalCustomClient {

    @GetMapping("custom/{id}")
    public CustomInfo getCustom(@PathVariable("id") String id);
}

@Testcontainers
@SpringBootTest
public class ExternalCustomClientTest {

    @Container
    static GenericContainer<?> container = new GenericContainer<>(
            "demo/demo:latest"
    ).withExposedPorts(8080);


    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        Integer firstMappedPort = container.getMappedPort(8080);
        String ipAddress = container.getHost();
        System.out.println(ipAddress);
        System.out.println(firstMappedPort);
        registry.add("remote.custom.url",() -> "http://"+ipAddress+":"+firstMappedPort);
    }

    @Autowired
    ExternalCustomClient customClient;



    @Test
    void shouldGetAllCustomers() {
        String id ="111";

        CustomInfo customInfo=customClient.getCustom(id);
        Assertions.assertEquals(id, customInfo.getCustomNo());
    }


}

由于使用了GenericContainer,Springboot不知道该如何去连接容器,因此不能使用@ServiceConnection注解,还是回到@DynamicPropertySource的方式。

@Container
static GenericContainer<?> container = new GenericContainer<>(
“zhangxiaotian/caizi:latest”
).withExposedPorts(8080);

在容器内部监听了一个8080端口,这个需要和外部服务的端口一致,对容器外部暴露的端口,我们可以通过

Integer firstMappedPort = container.getMappedPort(8080);
String ipAddress = container.getHost();

来获取完整的ip和端口

完整的代码示例可以通过以下仓库获取
https://gitee.com/xiiiao/testcontainer-demo

  • 25
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
vs2010 中添加 ActiveX Control Test Container工具(转载) vs2010中的TSTCON( ActiveX Control Test Container )工具非自动安装,而是作为一个例程提供。所以应找到该例程,并编译: 如vs2010安装在默认路径则 1, 进入:C:\Program Files\Microsoft Visual Studio 10.0\Samples\1033,可找到VC2010Samples.zip文件, 2, 将其解决到同一个文件夹,解压后将出现一个C++子文件夹。 3, TstCon例程在:C:\Program Files\Microsoft Visual Studio 10.0\Samples\1033\C++\MFC\ole\TstCon,双击其中的.sln文件启动工程,直接编译, 4, 编译后在Debug子文件夹中将出现一个TstCon.exe文件,将其复制到:C:\Program Files\Microsoft Visual Studio 10.0\Common7\Tools文件夹。 5, 在vs2010中的Tools菜单点击“External Tools...”,出现如下对话框,点击“Add”添加一项工具。 6, 新工具项的"Title"为:ActiveX Control Test Container,"Command"为:C:\Program Files\Microsoft Visual Studio 10.0\Common7\Tools\TstCon.exe。 7, 确认退出对话框,Tools菜单将出现“ActiveX Control Test Container”菜单项。完毕! 同理在2015安装目录下找到并编译: 但是编译的时候需要区分win32和x64的不同平台生成的工具 分别添加 ActiveX Control Test Container 32和 ActiveX Control Test Container 64的工具,因为不同工具显示的是不同平台注册的com组件,比如你注册一个32位的com组件,此时只能用ActiveX Control Test Container 32测试,而在ActiveX Control Test Container 64中是找不到这个com组件的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值