本文已同步至个人微信公众号【不能止步】,链接为基于MockServer测试访问其他服务接口的代码,欢迎订阅。
在实际项目中,存在一些业务场景,为了完成该业务,需要在某个服务中编排多个服务,这里便产生了服务间的调用依赖。于是在一个服务中需要编写访问其他服务接口的代码。在基于TDD的编程实践下,需要对所有的代码进行测试覆盖(可以根据实际情况进行裁剪)。为了测试访问其他服务接口的代码,需要考虑几个问题:
本文试图回答这两个问题并给出一个测试示例。
一般在实现访问其他服务接口的代码时,我们要做这几个事情:
根据我们所选的测试类型不同,可以通过测试保证代码实现不同部分的正确性。比如,如果我们选择单元测试,则通常只能保证2、3、4这几项实现的正确性;如果我们选择集成测试,可以保证整个实现的正确性。(这里我们认为开发者已经清楚被访问接口的URL、参数和响应)
通常在测试访问其他服务接口的代码时,我们着重保证2、3、4几项实现的正确性。
针对访问其他服务接口的代码的测试,难点是如何实现依赖的其他服务的接口,这点我们有三种选择:
后两种都属于Mock,只是Mock的方式不一样。
这几种方式的优缺点如下:
方法 | 优点 | 缺点 | 代表技术 |
---|---|---|---|
方法1 | 能测试所有实现的正确性 | 测试效率低,如果真实服务的稳定性较差,测试会经常性失败,从而影响开发效率 | |
方法2 | 测试代码里不用写很多mock服务的代码 | 需要维护Mock服务的启动脚本、接口和响应映射;测试代码与Mock代码离的较远,不利于分析测试失败原因;测试相对效率相对较低 | WireMock, MockServer |
方法3 | 速度快,稳定性强,测试代码与Mock代码在一起,定位测试失败原因效率高 | 并不能测试所有实现的正确性 | WireMock, MockServer |
综合以上分析,在测试访问其他服务接口的代码时,通常以运行时Mock为主,可以辅以与真实服务的集成测试来覆盖所有的代码实现的正确性。
接下来我们就通过代码来说明如何在运行时Mock服务接口来测试实现代码。代码的技术栈包括:
待测的代码包括一个待访问服务的链接配置和实现的访问代码。
@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);
}
}
<dependency>
<groupId>org.mock-servergroupId>
<artifactId>mockserver-junit-jupiterartifactId>
<version>5.14.0version>
<scope>testscope>
dependency>
测试代码包括两部分:测试基类和测试实现类。
@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());
}
}
WebClientBaseTest
中通过@BeforeAll
和@AfterAll
来设置测试类运行前先启动MockServer,测试类运行结束后关闭MockServer。因为MockServer的启停是异步的,如果在每个测试用例执行前后(如用@BeforEach
和@AfterEach
注解)都启动和停止MockServer,可能会出现由于当前MockServer未关闭完成而导致下一个测试用例启动MockServer时绑定端口失败,从而导致测试失败。WebClientBaseTest
中通过@BeforeEach
注解在每个测试用例运行前清除之前Mock接口的信息,避免各个测试用例Mock接口之间相互干扰。