如何使用Spring在unit testing中模拟远程REST API?

假设我在我的应用程序中创建了一个使用远程Web服务的简单客户端,该服务在某个URI /foo/bar/{baz}公开RESTful API。 现在我想对我的客户端进行unit testing,该客户端调用此Web服务。

理想情况下,在我的测试中,我想根据/foo/bar/123/foo/bar/42等特定请求模拟我从Web服务获得的响应。 我的客户端假设API实际上在某处运行,所以我需要一个本地“Web服务”来开始在http://localhost:9090/foo/bar运行我的测试。

我希望我的unit testing是自包含的,类似于使用Spring MVC Test框架测试Spring控制器。

一些简单客户端的伪代码,从远程API获取数字:

 // Initialization logic involving setting up mocking of remote API at // http://localhost:9090/foo/bar @Autowired NumberClient numberClient // calls the API at http://localhost:9090/foo/bar @Test public void getNumber42() { onRequest(mockAPI.get("/foo/bar/42")).thenRespond("{ \"number\" : 42 }"); assertEquals(42, numberClient.getNumber(42)); } // .. 

使用Spring有哪些替代方案?

如果使用Spring RestTemplate,则可以使用MockRestServiceServer。 可以在这里找到一个例子https://objectpartners.com/2013/01/09/rest-client-testing-with-mockrestserviceserver/

如果你想对你的客户端进行单元测试,那么你就会模拟掉那些正在进行REST API调用的服务,即使用mockito – 我假设你有一个为你调用这些API的服务,对吧?

另一方面,如果你想“模拟”其余的API,因为有某种服务器给你回复,这更符合集成测试,你可以尝试其中一个像restito那样的框架 , rest司机或betamax 。

最好的方法是使用WireMock 。 添加以下依赖项:

   com.github.tomakehurst wiremock 2.4.1   org.igniterealtime.smack smack-core 4.0.6  

定义并使用线缆,如下所示

 @Rule public WireMockRule wireMockRule = new WireMockRule(8089); String response ="Hello world"; StubMapping responseValid = stubFor(get(urlEqualTo(url)).withHeader("Content-Type", equalTo("application/json")) .willReturn(aResponse().withStatus(200) .withHeader("Content-Type", "application/json").withBody(response))); 

您正在寻找的是Spring MVC测试框架中对客户端REST测试的支持。

假设您的NumberClient使用Spring的RestTemplate ,前面提到的支持是要走的路!

希望这可以帮助,

山姆

您可以轻松地使用MockitoSpring Boot中模拟REST API。

在您的测试树中放置一个存根控制器:

 @RestController public class OtherApiHooks { @PostMapping("/v1/something/{myUUID}") public ResponseEntity handlePost(@PathVariable("myUUID") UUID myUUID ) { assert (false); // this function is meant to be mocked, not called return new ResponseEntity(HttpStatus.NOT_IMPLEMENTED); } } 

运行测试时,您的客户端需要在localhost上调用API。 这可以在src/test/resources/application.properties 。 如果测试使用的是RANDOM_PORT ,则您的受测客户端需要找到该值。 这有点棘手,但问题在于解决: Spring Boot – 如何获取正在运行的端口

配置您的测试类以使用WebEnvironment (正在运行的服务器),现在您的测试可以以标准方式使用Mockito,并根据需要返回ResponseEntity对象:

 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class TestsWithMockedRestDependencies { @MockBean private OtherApiHooks otherApiHooks; @Test public void test1() { Mockito.doReturn(new ResponseEntity(HttpStatus.ACCEPTED)) .when(otherApiHooks).handlePost(any()); clientFunctionUnderTest(UUID.randomUUID()); // calls REST API internally Mockito.verify(otherApiHooks).handlePost(eq(id)); } } 

您还可以使用此方法在上面创建的模拟环境中对整个微服务进行端到端测试。 一种方法是将TestRestTemplate注入到您的测试类中,并使用它来代替示例中的clientFunctionUnderTest来调用REST API。

 @Autowired private TestRestTemplate restTemplate; @LocalServerPort private int localPort; // you're gonna need this too 

这是如何工作的

因为OtherApiHooks是测试树中的@RestController ,所以Spring Boot会在运行SpringBootTest.WebEnvironment时自动建立指定的REST服务。

Mockito在这里用于模拟控制器类 – 而不是整个服务。 因此,在模拟命中之前,将会有一些由Spring Boot管理的服务器端处理。 这可能包括反序列化(和validation)示例中显示的路径UUID。

据我所知,这种方法对于IntelliJ和Maven的并行测试运行非常强大。

如果您正在使用rest并利用DTO模式 ,那么我建议您按照本教程进行操作 。

以下是如何使用Mockito模拟Controller类的基本示例:

Controller类:

 @RestController @RequestMapping("/users") public class UsersController { @Autowired private UserService userService; public Page getUsers(Pageable pageable) { Page page = userService.getAllUsers(pageable); List items = mapper.asUserCollectionItems(page.getContent()); return new PageImpl(items, pageable, page.getTotalElements()); } } 

配置bean:

 @Configuration public class UserConfig { @Bean public UsersController usersController() { return new UsersController(); } @Bean public UserService userService() { return Mockito.mock(UserService.class); } } 

UserCollectionItemDto是一个简单的POJO ,它表示API使用者发送给服务器的内容。 UserProfile是服务层中使用的主要对象(由UserService类提供)。 此行为还实现了DTO模式 。

最后,模拟预期的行为:

 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = AnnotationConfigContextLoader.class) @Import(UserConfig.class) public class UsersControllerTest { @Autowired private UsersController usersController; @Autowired private UserService userService; @Test public void getAllUsers() { initGetAllUsersRules(); PageRequest pageable = new PageRequest(0, 10); Page page = usersController.getUsers(pageable); assertTrue(page.getNumberOfElements() == 1); } private void initGetAllUsersRules() { Page page = initPage(); when(userService.getAllUsers(any(Pageable.class))).thenReturn(page); } private Page initPage() { PageRequest pageRequest = new PageRequest(0, 10); PageImpl page = new PageImpl<>(getUsersList(), pageRequest, 1); return page; } private List getUsersList() { UserProfile userProfile = new UserProfile(); List userProfiles = new ArrayList<>(); userProfiles.add(userProfile); return userProfiles; } } 

我们的想法是使用纯Controller bean并对其成员进行模型化。 在此示例中,我们模拟UserService.getUsers()对象以包含用户,然后validationController是否将返回正确数量的用户。

使用相同的逻辑,您可以测试服务和应用程序的其他级别。 此示例也使用Controller-Service-Repository模式 🙂