基于MockServer测试访问其他服务接口的代码

本文已同步至个人微信公众号【不能止步】,链接为基于MockServer测试访问其他服务接口的代码,欢迎订阅。

在实际项目中,存在一些业务场景,为了完成该业务,需要在某个服务中编排多个服务,这里便产生了服务间的调用依赖。于是在一个服务中需要编写访问其他服务接口的代码。在基于TDD的编程实践下,需要对所有的代码进行测试覆盖(可以根据实际情况进行裁剪)。为了测试访问其他服务接口的代码,需要考虑几个问题:

  • 测试哪些内容
  • 选择怎样的测试方式

本文试图回答这两个问题并给出一个测试示例。

1. 测试哪些内容

一般在实现访问其他服务接口的代码时,我们要做这几个事情:

  1. 配置访问的url,包括host、端口、链接超时、读超时等
  2. 将业务模型或参数转换成请求参数
  3. 将响应转换成DTO或业务模型
  4. 处理依赖服务的异常,包括服务无法连接、参数异常、内部错误等

根据我们所选的测试类型不同,可以通过测试保证代码实现不同部分的正确性。比如,如果我们选择单元测试,则通常只能保证2、3、4这几项实现的正确性;如果我们选择集成测试,可以保证整个实现的正确性。(这里我们认为开发者已经清楚被访问接口的URL、参数和响应)

通常在测试访问其他服务接口的代码时,我们着重保证2、3、4几项实现的正确性。

2. 选择怎样的测试方式

针对访问其他服务接口的代码的测试,难点是如何实现依赖的其他服务的接口,这点我们有三种选择:

  1. 基于真实的服务
  2. 采用Mock技术提前启动一个依赖服务的进程并Mock依赖的接口
  3. 采用运行时Mock

后两种都属于Mock,只是Mock的方式不一样。

这几种方式的优缺点如下:

方法 优点 缺点 代表技术
方法1 能测试所有实现的正确性 测试效率低,如果真实服务的稳定性较差,测试会经常性失败,从而影响开发效率
方法2 测试代码里不用写很多mock服务的代码 需要维护Mock服务的启动脚本、接口和响应映射;测试代码与Mock代码离的较远,不利于分析测试失败原因;测试相对效率相对较低 WireMock, MockServer
方法3 速度快,稳定性强,测试代码与Mock代码在一起,定位测试失败原因效率高 并不能测试所有实现的正确性 WireMock, MockServer

综合以上分析,在测试访问其他服务接口的代码时,通常以运行时Mock为主,可以辅以与真实服务的集成测试来覆盖所有的代码实现的正确性。

3. 基于MockServer测试访问其他服务接口的代码

接下来我们就通过代码来说明如何在运行时Mock服务接口来测试实现代码。代码的技术栈包括:

  • Spring Boot 2.7.0。
  • Lombok
  • Junit 5
  • MockServer

3.1 待测的访问其他服务接口的代码

待测的代码包括一个待访问服务的链接配置和实现的访问代码。

  • 待访问服务的链接配置
@Getter
public class HttpClientProperties {

    private final String host;

    private final int port;

    private final Integer connectTimeoutSeconds;

    private final Integer readWriteTimeoutSeconds;

    private final Integer retryCount;

    private final Integer retryDelaySeconds;

    public HttpClientProperties(String host,
                                Integer port,
                                @DefaultValue("10") Integer connectTimeoutSeconds,
                                @DefaultValue("30") Integer readWriteTimeoutSeconds,
                                @DefaultValue("0") Integer retryCount,
                                @DefaultValue("0") Integer retryDelaySeconds) {
        this.host = host;
        this.port = port;
        this.connectTimeoutSeconds = connectTimeoutSeconds;
        this.readWriteTimeoutSeconds = readWriteTimeoutSeconds;
        this.retryCount = retryCount;
        this.retryDelaySeconds = retryDelaySeconds;
    }
}
  • 访问服务的实现代码
```public class ProductClient {
    private final RestTemplate restTemplate;
    private final HttpClientProperties httpClientProperties;

    public ProductClient(HttpClientProperties httpClientProperties) {
        this.restTemplate = new RestTemplate();
        this.httpClientProperties = httpClientProperties;
    }

    public ResponseEntity<Product> getProduct(String id) {
        DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory();
        URI uri = uriBuilderFactory.builder()
                .scheme("http")
                .host(httpClientProperties.getHost())
                .port(httpClientProperties.getPort())
                .path("/api/products/{productId}")
                .build(id);
        return restTemplate.getForEntity(uri, Product.class);
    }
}

3.2 引入MockServer依赖

<dependency>
     <groupId>org.mock-servergroupId>
     <artifactId>mockserver-junit-jupiterartifactId>
     <version>5.14.0version>
     <scope>testscope>
dependency>

3.3 实现测试代码

测试代码包括两部分:测试基类和测试实现类。

  • 测试基类
@ActiveProfiles("test")
@ExtendWith(MockServerExtension.class)
@MockServerSettings(ports = {9090})
public abstract class WebClientBaseTest {
    protected static ClientAndServer clientAndServer;

    @BeforeAll
    public static void initMockClientService(ClientAndServer clientAndServer) {
        WebClientBaseTest.clientAndServer = clientAndServer;
    }

    @BeforeEach
    public void resetMockClientService() {
        clientAndServer.reset();
    }

    @AfterAll
    public static void stopMockClientService(ClientAndServer clientAndServer) {
        clientAndServer.stop(true);
    }
  • 测试实现类
@MockServerSettings(ports = 9090)
class ProductClientTest extends WebClientBaseTest {

    private final HttpClientProperties httpClientProperties = new HttpClientProperties(
            "localhost",
            9090,
            10,
            30,
            0,
            0);

    private final ObjectMapper objectMapper = new ObjectMapper();

    private final ProductClient productClient = new ProductClient(httpClientProperties);

    @Test
    public void shouldReturnProductSuccessfully() throws JsonProcessingException {
        String productId = "1";
        Product product = new Product();
        product.setId("1");
        product.setName("test");
        clientAndServer.when(HttpRequest.request()
                        .withMethod("GET")
                        .withPath("/api/products/{productId}")
                        .withPathParameter("productId", productId))
                .respond(HttpResponse.response()
                        .withStatusCode(200)
                        .withBody(objectMapper.writeValueAsString(product), MediaType.APPLICATION_JSON));

        ResponseEntity<Product> response = productClient.getProduct(productId);

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals("1", response.getBody().getId());
        assertEquals("test", response.getBody().getName());
    }

    @Test
    public void shouldReturn404GivenProductIdNotExist() throws JsonProcessingException {
        String productId = "1";
        clientAndServer.when(HttpRequest.request()
                        .withMethod("GET")
                        .withPath("/api/products/{productId}")
                        .withPathParameter("productId", productId))
                .respond(HttpResponse.response()
                        .withStatusCode(HttpStatus.NOT_FOUND.value()));

        ResponseEntity<Product> response = productClient.getProduct(productId);

        assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
    }
}

3.4 测试注意事项

  • WebClientBaseTest中通过@BeforeAll@AfterAll来设置测试类运行前先启动MockServer,测试类运行结束后关闭MockServer。因为MockServer的启停是异步的,如果在每个测试用例执行前后(如用@BeforEach@AfterEach注解)都启动和停止MockServer,可能会出现由于当前MockServer未关闭完成而导致下一个测试用例启动MockServer时绑定端口失败,从而导致测试失败。
  • WebClientBaseTest中通过@BeforeEach注解在每个测试用例运行前清除之前Mock接口的信息,避免各个测试用例Mock接口之间相互干扰。

你可能感兴趣的:(日常开发问题收录,测试,单元测试,MockServer)