如何为调用 API 的方法编写 JUnit 测试?

Posted

技术标签:

【中文标题】如何为调用 API 的方法编写 JUnit 测试?【英文标题】:How to write JUnit test for a method that calls API? 【发布时间】:2019-11-01 12:03:24 【问题描述】:

我必须为调用 API 然后处理响应的类编写测试。该类有两个公共函数和一个私有函数。第一个公共方法获取 ID 列表。第二个公共方法在每个 ID 的循环中调用,以获取与 ID 关联的详细信息。私有方法在第二个公共方法内部调用,因为基于 id 获取详细信息的调用是异步进行的。

我是 JUnits 的新手,虽然我知道我不应该测试 API 调用,只测试我的函数,但我仍然不明白单元测试应该断言什么。

以下是我的功能:

public List<Integer> fetchVehicleIds(String datasetId) throws ApiException 

    VehiclesApi vehiclesApi = new VehiclesApi();

    List<Integer> vehicleIds;
    vehicleIds = vehiclesApi.vehiclesGetIds(datasetId).getVehicleIds();

    return vehicleIds;


 public List<VehicleResponse> fetchVehicleDetails(String datasetId, List<Integer> vehicleIds) throws InterruptedException, ApiException 

    CountDownLatch latch = new CountDownLatch(vehicleIds.size());
    List<VehicleResponse> vehiclesList = new ArrayList<>();

    for (Integer vehicleId: vehicleIds) 
        populateEachVehicleDetail(datasetId, vehicleId, vehiclesList, latch);
    

    latch.await();

    return vehiclesList;


private void populateEachVehicleDetail(String datasetId, Integer vehicleId, List<VehicleResponse> vehiclesList, CountDownLatch latch) throws ApiException 

    ApiCallback<VehicleResponse> vehicleResponseApiCallback = new ApiCallback<VehicleResponse>() 
        @Override
        synchronized public void onSuccess(VehicleResponse result, int statusCode, Map<String, List<String>> responseHeaders) 
            vehiclesList.add(result);
            latch.countDown();
        
    ;

    VehiclesApi vehiclesApi = new VehiclesApi();
    vehiclesApi.vehiclesGetVehicleAsync(datasetId,vehicleId,vehicleResponseApiCallback);


根据我到目前为止所做的研究,我认为我必须使用 mockito 来模拟 API 调用?我仍然不清楚如何对功能进行单元测试。

【问题讨论】:

基本上,您希望通过将其与您控制的 Mock 交互的所有内容与实际 API 隔离开来。因此,创建一个 Mock VehiclesAPI,注入它(通过一个不在这里的设置器)并使用您自己制作的模拟响应处理对它的所有调用。 【参考方案1】:

这两个语句确实是您要在单元测试中隔离的内容:

private void populateEachVehicleDetail(String datasetId, Integer vehicleId, List<VehicleResponse> vehiclesList, CountDownLatch latch) throws ApiException 
....
    VehiclesApi vehiclesApi = new VehiclesApi();
    vehiclesApi.vehiclesGetVehicleAsync(datasetId,vehicleId,vehicleResponseApiCallback);
...

1) 让你的依赖可模拟

但是你只能模拟你可以从类的客户端设置的东西。 这里的 API 是一个局部变量。因此,您应该更改您的类以公开依赖项,例如在构造函数中。 通过这种方式,您可以轻松地模拟它。

2) 让你的模拟不返回结果,而是调用回调。

在同步调用上下文中,您希望模拟返回的结果。 在带有回调的异步调用上下文中,情况有所不同。实际上,回调不会返回给调用者,但会调用回调以提供调用的结果。因此,您想要的是模拟 API 使用代表单元测试数据集的模拟参数调用 onSuccess() 回调:

@Override
synchronized public void onSuccess(VehicleResponse result, int statusCode, Map<String, List<String>> responseHeaders) 
    vehiclesList.add(result);
    latch.countDown();

在您的单元测试中,您应该以这种方式模拟每个预期调用的回调:

@Mock
VehiclesApi vehiclesApiMock;
// ...

// when the api method is invoked with the expected dataSetId and vehicleId
Mockito.when(vehiclesApiMock.vehiclesGetVehicleAsync(Mockito.eq(datasetId), Mockito.eq(vehicleId),
                                                 Mockito.any(ApiCallback.class)))
       // I want to invoke the callback with the mocked data
       .then(invocationOnMock -> 
           ApiCallback<VehicleResponse> callback = invocationOnMock.getArgument(2);
           callback.onSuccess(mockedVehicleResponse, mockedStatusCode,
                              mockedResponseHeaders);
           return null; // it is a void method. So no value to return in T then(...).
       );

我认为ApiCallback 缺少演员表,但您应该有整体想法。

【讨论】:

【参考方案2】:

你是对的:既然你想测试你的单元(即呈现的代码),你应该模拟 API(主要是:vehicleApi 实例)。

目前,没有办法在您的代码中注入 VehicleApi 的模拟实例(嗯,有,但它会涉及使用反射......我们不要走这条路)。您可以应用Inversion of Control 使您的代码可测试:与其在对象中构造VehicleApi,不如编写一个期望VehicleApi-instance 的构造函数:

public class YourClass 
    private final VehicleApi vehicleApi;

    public YourClass(final VehicleApi vehicleApi) 
        this.vehicleApi = vehicleApi;
    

    [...]

你赢了什么?好了,现在您可以将模拟对象注入到您的测试单元中:

@RunWith(MockitoJRunner.class)
public class YourClassTest 

    private final VehicleApi vehicleApiMock = mock(VehicleApi.class);
    private final YourClass underTest = new YourClass(vehicleApiMock);

    @Test
    void someTest() 
        // GIVEN
        [wire up your mock if necessary]

        // WHEN
        [write the test-call]

        // THEN
        [verify that the unit under test is in the expected state]
    

此示例假定 JUnit5 作为测试框架,Mockito 作为模拟框架,但也有其他选项。

测试写在Gherkin language: - GIVEN 块描述了先决条件,即被测单元和外部(模拟)系统所在的位置 - WHEN 块执行应该被测试的动作 - THEN 块验证被测单元是否处于预期状态。

【讨论】:

以上是关于如何为调用 API 的方法编写 JUnit 测试?的主要内容,如果未能解决你的问题,请参考以下文章

如何为 Spring 托管 bean 编写 Junit 测试用例?

如何为配置类编写@Bean的junit案例

如何为 DirectoryStream 编写 junit 测试

如何为以下异常处理代码编写junit测试用例?

如何为调用外部 API 的 Laravel Artisan 命令编写 PHPUnit 测试,而无需物理调用该 API?

使用嵌入式 tomcat 服务器进行 JUnit 测试,如何为 http 和 https 连接器指定自动端口?