如何对 Retrofit api 调用进行单元测试?

Posted

技术标签:

【中文标题】如何对 Retrofit api 调用进行单元测试?【英文标题】:How to unit test Retrofit api calls? 【发布时间】:2015-11-29 05:19:59 【问题描述】:

我正在尝试为每个可能的代码块集成单元测试用例。 但是我在为 通过改造进行的 api 调用添加测试用例时遇到了问题。

JUnit 编译器从不执行回调函数中的代码

还有另一个选项可以使所有 api 调用 同步 以用于测试目的,但对于我的应用程序中的所有情况而言,这并不是可能的。

我该如何解决这个问题?我必须通过任何方式在 api 调用中添加测试用例。

【问题讨论】:

您能否先分享一个示例,说明您最初是如何为 api 调用编写测试用例的? 【参考方案1】:

我使用 Mockito、Robolectric 和 Hamcrest 库测试了我的 Retrofit 回调。

首先,在你的模块的 build.gradle 中设置 lib 栈:

dependencies 
    testCompile 'org.robolectric:robolectric:3.0'
    testCompile "org.mockito:mockito-core:1.10.19"
    androidTestCompile 'org.hamcrest:hamcrest-library:1.1'

在 jour 项目的全局 build.gradle 中添加以下行到 buildscript 依赖项:

classpath 'org.robolectric:robolectric-gradle-plugin:1.0.1'

然后在 Android Studio 中进入“Build Variants”菜单(要快速找到它,按 Ctrl+Shift+A 并搜索它),并将“Test Artifact”选项切换为“Unit Tests”。 Android Studio 会将您的测试文件夹切换到“com.your.package (test)”(而不是 androidTest)。

好的。设置完成,是时候写一些测试了!

假设您有一些改造 api 调用来检索需要放入 RecyclerView 等适配器的对象列表。我们想测试适配器是否在成功调用时填充了正确的项目。 为此,我们需要切换您的 Retrofit 接口实现,您可以使用该接口通过模拟进行调用,并利用 Mockito ArgumentCaptor 类进行一些虚假响应。

@Config(constants = BuildConfig.class, sdk = 21,
    manifest = "app/src/main/AndroidManifest.xml")
@RunWith(RobolectricGradleTestRunner.class)
public class RetrofitCallTest 

    private MainActivity mainActivity;

    @Mock
    private RetrofitApi mockRetrofitApiImpl;

    @Captor
    private ArgumentCaptor<Callback<List<YourObject>>> callbackArgumentCaptor;

    @Before
    public void setUp()             
        MockitoAnnotations.initMocks(this);

        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        mainActivity = controller.get();

        // Then we need to swap the retrofit api impl. with a mock one
        // I usually store my Retrofit api impl as a static singleton in class RestClient, hence:
        RestClient.setApi(mockRetrofitApiImpl);

        controller.create();
    

    @Test
    public void shouldFillAdapter() throws Exception 
        Mockito.verify(mockRetrofitApiImpl)
            .getYourObject(callbackArgumentCaptor.capture());

        int objectsQuantity = 10;
        List<YourObject> list = new ArrayList<YourObject>();
        for(int i = 0; i < objectsQuantity; ++i) 
            list.add(new YourObject());
        

        callbackArgumentCaptor.getValue().success(list, null);

        YourAdapter yourAdapter = mainActivity.getAdapter(); // Obtain adapter
        // Simple test check if adapter has as many items as put into response
        assertThat(yourAdapter.getItemCount(), equalTo(objectsQuantity));
    

通过右键单击测试类并点击运行来继续测试。

就是这样。我强烈建议使用 Robolectric(带有 robolectric gradle 插件)和 Mockito,这些库使测试 android 应用程序变得更加容易。 我从下面的blog post 学到了这个方法。另请参阅this answer。

更新:如果您正在使用 Retrofit 和 RxJava,请查看 my other answer on that。

【讨论】:

我阅读了同一篇博文,发现其中的解释很有帮助。对我来说,一个难点是这个例子。我尝试阅读示例,然后在我的项目中以类似的方式实施测试。我了解到,仅从测试代码 sn-p 并不清楚的是,在博客文章中描述的测试中,有一个 Activity 在 onCreate() 方法中进行网络调用。需要进行此调用,以便在测试中可以运行对 Mockito.verify() 的调用。如果您不在应用程序代码中进行实际的“网络”调用,测试代码将不会按原样运行。 这不是问题的答案 @JemshitIskenderov 怎么来的?愿意指出它的问题吗? 1) 问题是 jUnit,你介绍了 Roboelectric,hamcrest 2) 这是关于异步回调没有被执行,你从来没有回答为什么会发生,问题是什么...... 3) 你刚刚展示你在你的项目中做了什么,所以它不是这个问题的答案 @JemshitIskenderov 你一定错过了“任何关于另一种方法的建议也会有所帮助。”问题的一部分。这就是这个答案 - 一种建议的工作方法。【参考方案2】:

如果您使用 .execute() 而不是 .enqueue() 它会使执行同步,因此测试可以正常运行而无需导入 3 个不同的库和添加任何代码或修改构建变体。

喜欢:

public class LoginAPITest 

    @Test
    public void login_Success() 

        APIEndpoints apiEndpoints = RetrofitHelper.getTesterInstance().create(APIEndpoints.class);

        Call<AuthResponse> call = apiEndpoints.postLogin();

        try 
            //Magic is here at .execute() instead of .enqueue()
            Response<AuthResponse> response = call.execute();
            AuthResponse authResponse = response.body();

            assertTrue(response.isSuccessful() && authResponse.getBearer().startsWith("TestBearer"));

         catch (IOException e) 
            e.printStackTrace();
        

    


【讨论】:

虽然这是一种有效的方法,但问题指出 还有另一种选择可以使所有 api 调用同步以进行测试,但对于我的应用程序中的所有情况而言,这并非不可能。 mybad mybad mybad 这个解决方案给了我一个非常有用的线索。谢谢 什么是 RetrofitHelper?是自定义类还是依赖类 还想看RetrofitHelper【参考方案3】:

JUnit 框架从不执行回调函数中的代码,因为执行的主线程在检索到响应之前终止。可以使用CountDownLatch,如下图:

@Test
public void testApiResponse() 
    CountDownLatch latch = new CountDownLatch(1);
    mApiHelper.loadDataFromBackend(new Callback() 
        @Override
        public void onResponse(Call call, Response response) 
            System.out.println("Success");
            latch.countDown();
        

        @Override
        public void onFailure(Call call, Throwable t) 
            System.out.println("Failure");
            latch.countDown();
        
    );

    try 
        latch.await();
     catch (InterruptedException e) 
        e.printStackTrace();
     

这个test sample 也可能有帮助。

我的建议是不要在 android 应用程序中测试 API 响应。为此有很多外部tools。

【讨论】:

【参考方案4】:

Junit 不会等待异步任务完成。您可以使用 CountDownLatch(不需要外部库的优雅解决方案)来阻塞线程,直到您收到来自服务器的响应或超时。

您可以使用CountDownLatch。 由于调用了 countDown() 方法,await 方法一直阻塞,直到当前计数达到零,之后所有等待的线程都被释放,任何后续的 await 调用立即返回。

//Step 1: Do your background job 
 latch.countDown(); //Step 2 : On completion ; notify the count down latch that your async task is done
 
 latch.await(); // Step 3: keep waiting

或者您可以在 await 调用中指定超时

  try 
      latch.await(2000, TimeUnit.MILLISECONDS);
     catch (InterruptedException e) 
        e.printStackTrace();
    

示例测试用例

void testBackgroundJob() 

        Latch latch = new CountDownLatch(1);

        //Do your async job
        Service.doSomething(new Callback() 

            @Override
            public void onResponse()
                ACTUAL_RESULT = SUCCESS;
                latch.countDown(); // notify the count down latch
                // assertEquals(..
            

        );

        //Wait for api response async
        try 
            latch.await();
         catch (InterruptedException e) 
            e.printStackTrace();
        
        assertEquals(expectedResult, ACTUAL_RESULT);

    

【讨论】:

【参考方案5】:

如果已经用 rx 和 restful 封装了 retrofit2.0

open class BaseEntity<E> : Serializable 
    /*result code*/
    var status: Int = 0
    /**data */
    var content: E? = null

和服务器 api 请求类似

@GET(api/url)
fun getData():Observable<BaseEntity<Bean>>

你的服务只回调一个同步请求 Observable

val it = service.getData().blockingSingle()
assertTrue(it.status == SUCCESS_CODE)

【讨论】:

【参考方案6】:

作为@Islam Salah said:

JUnit 框架从不执行 CallBack 函数中的代码,因为执行的主线程在检索到响应之前终止。

您可以使用awaitility 解决问题。查看 *** 上的 this 答案。

【讨论】:

以上是关于如何对 Retrofit api 调用进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

如何对 AngularJS $promise.then 进行单元测试

如何对自定义 AJAX 事件进行单元测试

使用 Retrofit Android 对多个 API 进行常见成功/失败/错误处理的良好设计

如何在目标 c 中对目标的项目级 API 进行单元测试?

如何使用 API 平台对 GraphQL 文件上传进行单元测试?

Android:带有改造和单元测试的网络调用