后端自动化测试

后端自动化测试的逻辑与前端自动化测试很像:都是引入一些工具,对于每一个功能模块,用代码模拟真实的运行场景,然后通过脚本一键运行所有测试。

引入工具

首先,在 pom.xml 中间声明这个依赖包(如果创建项目时使用的是 Spring Initializer,这个应该默认是有的)。

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

这个依赖其实是很多工具的打包版本,其中包含各位熟悉的 JUnit

模拟完整请求

在这里,我们测试完整的后端逻辑:接收请求 -> 调用 Controller -> 调用 Service -> 调用 Mapper -> 查询数据库 -> 返回。为了这样测试,我们只需要模拟真实场景,给自己发送请求就行了。

我们需要新建一个测试类,并为它打上标签,像下面这样

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ProductTest {
    //...
}

@SpringBootTest 会告诉 SpringBoot 这是一个测试用的类,这样可以在一键运行的时候找到。

而括号里的 webEnvironment 部分,可以理解为让 SpringBoot 跑一个模拟的服务:手动测试的时候,是要先让服务跑起来,然后发请求;这样写之后,你不需要运行这个项目,只需要运行这个测试文件就可以了。RANDOM_PORT 意思是随便指定一个端口运行这个模拟的服务。

之后,我们使用 RestTemplate 这个东西(一个发送 HTTP 请求的工具),向自己的接口发送请求,然后用常规的 JUnit 语句测试返回值是否符合预期。

举个例子,假定我们的接口是这样:

@PostMapping("/product")
public CommonResult<Product> addProduct(@RequestBody Product product) {
    // 插入一个 product 到数据库里
}

为了测试这个接口,我们的测试类可以这样写:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ProductTest {

    @Resource
    TestRestTemplate restTemplate; // 用来向模拟服务发送 HTTP 请求

    @Resource
    ObjectMapper mapper;    // 用来把 JSON String 转成 Java Object

    @Test
    void testPostProduct() throws Exception {
        // 创建一个 Product
        Product product = new Product();
        product.setId(BigInteger.valueOf(777));
        product.setName("AirPods");
        // ... 以及 set 一堆东西...

        // 发送 POST 请求,然后以 String 形式从模拟的服务拿回一个 response
        // 注意:这里的 String.class 告诉 restTemplate,返回值是 String
        // 但是,无法直接告诉它,返回值是 `CommonResult<Product>`,因为牵扯到 generic type
        String response = restTemplate.postForObject("/product", product, String.class);

        // 把 response 从 String 弄成一个有结构的 class
        CommonResult<Product> result = this.mapper.readValue(
            response,
            new TypeReference<CommonResult<Product>>() {} 
            // 这个 TypeReference 用来告诉 mapper,要把 JSON 转成啥类型
            // TypeReference 是一个 AbstractClass,所以初始化它的时候后面要加一个 {}
        );

        // 用正常的 JUnit 的功能去测试即可
        Assertions.assertEquals("AirPods", result.getData().getName());

    }
}

这里对 /product 发送了一个 POST 请求,模拟了插入一个 Product 的请求。

我们也可以用RestTemplate 下的 putdeleteexchangegetForObject 等方法,发送不同请求。具体的使用方法可以查阅这个 GuideRestTemplate官方文档

模拟单个组件

我们可以把 MapperServiceController 单独拿来测试。我们会用到 MockMvc 这个工具(其实是 mockito 这个工具的延申)。它的官方教程在这里。

比如,我们现在只测试 Controller,而不去管 Service 层有没有在正确地干活,我们需要先 Mock一个 Service 层,假定它是好的,在这基础上,测试 controller 的功能。 举个例子:

@SpringBootTest
@AutoConfigureMockMvc
public class ProductTest2 {

    @MockBean
    private ProductService productService; // 这是我们 mock 的 service

    @Resource
    private MockMvc mockMvc; // 这是我们要用的工具

    @Test
    public void testProductController() throws Exception {

        // 创建一个 product
        Product p = new Product();
        p.setId(BigInteger.valueOf(777));
        p.setName("Apple");
        // 指定怎么个 mock 法
        // 意思是:别管 productService 到底是怎么实现的(没写都没关系)
        // 假定:只要我给了任何 argument,它都会 return 一个 p (之前定义的 product)
        given(this.productService.getProductById(ArgumentMatchers.any())).willReturn(p);

        // 然后,去向 controller 发请求,然后 expect 一堆结果(就像 assert 那样)
        // jsonPath 里面的东西是用来获取 json 的 attribute 的
        mockMvc.perform(get("/product/777"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.message").value("操作成功"))
                .andExpect(jsonPath("$.data.id").value(777))
                .andExpect(jsonPath("$.data.name").value("Apple"))
                .andReturn();
    }
}

如果想要测试 ServiceMapper,其实逻辑非常像,而且由于不牵扯到发送请求,所以更加简单。

以测试 Service 的代码为例:

@SpringBootTest
@AutoConfigureMockMvc
public class ProductServiceTest {
    @MockBean
    private ProductMapper productMapper;
    @Autowired
    private ProductService productService;
    @Test
    public void testProductService(){
        Product p = new Product();
        p.setxxx() //... 设置 p 的属性

        // 假定 Mapper 是 ok 的,不管查询什么 id,都会返回 p
        given(productMapper.findById(any())).willReturn(p);

        // 在 Mapper ok 的情况下,使用 productService
        Product resultProduct = productService.findById(100);

        // 然后在这里使用 assert 语句判断 resultProduct 是否符合要求。
        // 如果没有问题,说明 Service 这层没问题。
    }
}

测试 Mapper 的话,就不需要使用 mock 去假定什么东西了。