模拟外部 API 调用是集成或端到端测试中的常见做法,因为它允许开发人员将他们的代码与外部隔离。如果我们使用付费 API 并希望避免在测试时进行调用以节省资金,这也会有所帮助。
有两种方法可以模拟外部 API
- 使用 Mockito
- 使用 WireMock
在集成测试和端到端测试中,我更喜欢使用 WireMock,因为使用 WireMock 我们也可以测试 http 交互,而 mockito 将模拟整个 http 调用方法。
我们用于说明如何使用 WireMock 的场景有两个微服务,分别称为订单服务和库存服务。订单服务中的下单端点将对库存服务的库存端点进行 http 调用,以检查产品是否有库存,如果库存充足,则创建订单。
订单服务 — 控制器
@RestController
@RequestMapping("/api/order")
@RequiredArgsConstructor
public class OrderController { private final OrderService orderService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public String placeOrder ( @RequestBody OrderRequest orderRequest) { orderService.placeOrder(orderRequest); return "订单下达成功" ; }
}
OrderService — 服务
@RequiredArgsConstructor
@Service
public class OrderService { private final OrderRepository orderRepository; private final InventoryClient inventoryClient; public void placeOrder (OrderRequest orderRequest) { var isProductInStock = inventoryClient.isInStock(orderRequest.skuCode(), orderRequest.quantity()); if (!isProductInStock) {throw new RuntimeException ( "产品缺货 " +orderRequest.skuCode()+ " " ); } Order order = new Order (); order.setOrderNumber(UUID.randomUUID().toString()); order.setPrice(orderRequest.price()); order.setSkuCode(orderRequest.skuCode()); order.setQuantity(orderRequest.quantity()); orderRepository.save(order); }
}
库存服务——控制器
@RestController
@RequestMapping("/api/inventory")
@RequiredArgsConstructor
public class InventoryController { private final InventoryService inventoryService; @GetMapping @ResponseStatus(HttpStatus.OK) public boolean isInStock ( @RequestParam String skuCode,@RequestParam Integer quantile) { return inventoryService.isInStock(skuCode,quantity); }
}
Order 微服务中用于对 Inventory 微服务进行 rest api 调用的 feign 客户端接口如下(但我们也可以使用 rest 模板或新的 rest 客户端来代替 Feign 进行此 http 调用)
@FeignClient(value = "inventory-service", url = "${inventory.url}")
public interface InventoryClient {@RequestMapping(method = RequestMethod.GET, value = "/api/inventory")boolean isInStock(@RequestParam String skuCode, @RequestParam Integer quantity);
}
设置和使用 WireMock 的步骤如下;
- 将 WireMock 依赖项添加到 pom.xml
在依赖项下添加以下内容
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-contract-stub-runner</artifactId><scope>test</scope>
</dependency>
最后在 pom.xml 的属性部分下添加 <spring-cloud.version>2023.0.1</spring-cloud.version> ,如下所示
<properties><java.version>21</java.version><spring-cloud.version>2023.0.1</spring-cloud.version>
</properties>
如果你使用 gradle,请将以下代码添加到你的 build.gradle 文件中
ext {set('springCloudVersion', "2023.0.1")set('springCloudVersion', "2023.0.1")
}dependencies {implementation 'org.springframework.boot:spring-boot-starter'testImplementation 'org.springframework.boot:spring-boot-starter-test'testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
}dependencyManagement {imports {mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"}
}
2)使用@AutoConfigureWireMock注释spring boot测试
下一步是使用 @AutoConfigureWireMock 注释测试类
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWireMock(port = 0)
class OrderServiceApplicationTests {}
端口 0 表示我们要求 spring boot 使用随机端口,以避免端口冲突
3)创建一个存根客户端类,并在其中创建一个用于api调用的存根方法
我们需要为存根方法创建一个单独的类,并在其中定义用于 API 调用方法的存根。我将创建一个存根方法来调用库存服务中的以下端点 /api/inventory。
public class InventoryClientStub {public static void stubInventoryCall(String skuCode, Integer quantity) {System.out.println("Stubbing inventory call for skuCode: " + skuCode + " and quantity: " + quantity);stubFor(get(urlEqualTo("/api/inventory?skuCode=" + skuCode + "&quantity=" + quantity)).willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json").withBody("true")));}
}
每当 WireMock 收到与请求参数“/api/inventory?skuCode=” + skuCode + “&quantity=” + amount 匹配的 URL 时,WireMock 将返回状态代码为 200 且主体为 JSON 值的响应
4)在测试目录中创建一个资源文件夹,并在资源目录中创建“application.properties”文件
然后在 test/resources/application.properties 中添加带有 Wiremock 动态端口的 url
5)最后将存根添加到测试方法中
将 InventoryClientStub.stubInventoryCall(“iphone_15”, 1) 存根添加到测试方法
@Test
void shouldPlaceOrder () { String requestBody = """ { "skuCode":"iphone_15", "price": 1000, "quantity": 1 } """ ; InventoryClientStub.stubInventoryCall( "iphone_15" , 1 ); var responseBodyString = RestAssured.given() .contentType( "application/json" ) .body(requestBody) .when() .post( "/api/order" ) .then() .statusCode( 201 ) .extract() .body().asString(); assertThat(responseBodyString, Matchers.is( "订单下单成功" ));
}
就是这样。运行测试时,你将从 WireMock 获得以下日志