如何使用 Spring 在单元测试中模拟远程 REST API?

Posted

技术标签:

【中文标题】如何使用 Spring 在单元测试中模拟远程 REST API?【英文标题】:How to mock remote REST API in unit test with Spring? 【发布时间】:2014-10-23 05:52:06 【问题描述】:

假设我在我的应用程序中创建了一个简单的客户端,它使用一个远程 Web 服务,该服务在某个 URI /foo/bar/baz 处公开一个 RESTful API。现在我希望对调用此 Web 服务的客户端进行单元测试。

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

我希望我的单元测试是独立的,类似于使用 Spring MVC 测试框架测试 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 的替代方案?

【问题讨论】:

你利用 DTO 模式吗? @Manu 你的意思是如果我将 JSON 编组到我的客户端中的域对象或从 JSON 编组到域对象? 更准确地说,如果您编组/解组为代表网络中域实体的中间对象 @Manu 不,我没有中间对象,只有域对象和 JSON 文本。 那么我建议开始使用 DTO 模式,并按照与测试 MVC 控制器相同的方式进行操作。 【参考方案1】:

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

    <dependency>
        <groupId>com.github.tomakehurst</groupId>
        <artifactId>wiremock</artifactId>
        <version>2.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-core</artifactId>
        <version>4.0.6</version>
    </dependency>

如下所示定义和使用wiremock

@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)));

【讨论】:

aResponse() 来自哪里? @powder366 WireMock.aResponse() 感谢您的回答,我一直在努力模拟远程 API 终于有了解决方案。【参考方案2】:

如果你使用 Spring RestTemplate,你可以使用 MockRestServiceServer。一个例子可以在这里找到REST Client Testing With MockRestServiceServer

【讨论】:

如果不使用Spring RestTemplate,可以查看WireMock。但是,这将在后台启动服务器。 This 一个比较新。 可以肯定:我猜这只有在被测试的客户端使用相同的RestTemplate实例时才有效?在我的例子中,测试代码在内部实例化了它自己的RestTemplate,我无权访问它。所以我不能用MockRestServiceServer【参考方案3】:

如果您想unit 测试您的客户端,那么您将模拟出进行 REST API 调用的服务,即使用mockito - 我假设您确实有一个服务是为您进行这些 API 调用,对吗?

另一方面,如果您想“模拟”其他 API,因为有某种服务器会为您提供响应,这将更符合集成测试,您可以尝试其中的众多框架比如restito、rest-driver 或betamax。

【讨论】:

【参考方案4】:

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

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

@RestController
public class OtherApiHooks 

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

您的客户端在运行测试时需要调用 localhost 上的 API。这可以在src/test/resources/application.properties 中配置。如果测试使用RANDOM_PORT,您的被测客户端将需要找到该值。这有点棘手,但这里解决了这个问题:Spring Boot - How to get the running port

将您的测试类配置为使用 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<Void>(HttpStatus.ACCEPTED))
      .when(otherApiHooks).handlePost(any());
    clientFunctionUnderTest(UUID.randomUUID()); // calls REST API internally
    Mockito.verify(otherApiHooks).handlePost(eq(id));
  


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

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

这是如何工作的

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

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

据我所知,这种方法对于使用 IntelliJ 和 Maven 的并行测试运行是稳健的。

【讨论】:

警告:请注意,assert 可能会受到一些评论者的反对。 这简直太棒了【参考方案5】:

您正在寻找的是 Spring MVC 测试框架中对Client-side REST Tests 的支持。

假设您的 NumberClient 使用 Spring 的 RestTemplate,那么上述支持就是要走的路!

希望这会有所帮助,

山姆

【讨论】:

【参考方案6】:

这是一个关于如何使用Mockito 模拟 Controller 类的基本示例:

控制器类:

@RestController
@RequestMapping("/users")
public class UsersController 

    @Autowired
    private UserService userService;

    public Page<UserCollectionItemDto> getUsers(Pageable pageable) 
        Page<UserProfile> page = userService.getAllUsers(pageable);
        List<UserCollectionItemDto> items = mapper.asUserCollectionItems(page.getContent());
        return new PageImpl<UserCollectionItemDto>(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 pattern。

最后,模拟预期的行为:

@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<UserDto> page = usersController.getUsers(pageable);
        assertTrue(page.getNumberOfElements() == 1);
    

    private void initGetAllUsersRules() 
        Page<UserProfile> page = initPage();
        when(userService.getAllUsers(any(Pageable.class))).thenReturn(page);
    

    private Page<UserProfile> initPage() 
        PageRequest pageRequest = new PageRequest(0, 10);
        PageImpl<UserProfile> page = new PageImpl<>(getUsersList(), pageRequest, 1);
        return page;
    

    private List<UserProfile> getUsersList() 
        UserProfile userProfile = new UserProfile();
        List<UserProfile> userProfiles = new ArrayList<>();
        userProfiles.add(userProfile);
        return userProfiles;
    

这个想法是使用纯控制器 bean 并模拟它的成员。在此示例中,我们模拟了 UserService.getUsers() 对象以包含用户,然后验证控制器是否会返回正确数量的用户。

使用相同的逻辑,您可以测试服务和应用程序的其他级别。这个例子也使用了Controller-Service-Repository Pattern :)

【讨论】:

以上是关于如何使用 Spring 在单元测试中模拟远程 REST API?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring Security Bean 中模拟自定义 UserService 详细信息以进行单元测试?

您如何在 C# 中模拟文件系统以进行单元测试?

如何在 Spring-boot 中不模拟服务类的情况下为 REST 控制器端点编写单元测试

如何模拟在 AngularJS Jasmine 单元测试中返回承诺的服务?

如何使用 Mockito 模拟由 Spring 应用程序上下文创建的对象以进行单元测试覆盖?

如何在 Spring Boot REST API 中为 db insert 方法编写模拟单元测试?