在Espresso测试下,在活动中进行的无效或覆盖API调用

时间:2018-04-17 18:00:29

标签: android retrofit2 okhttp3 android-espresso

我有一个Activity,它在onCreate()生命周期方法中执行API调用。如果此调用失败(在调试或测试环境中通常会失败),ViewStub会膨胀,充当错误屏幕。这个ViewStub涵盖了Activity的其余部分。

使用Espresso执行UI测试时会出现问题。我希望能够取消或控制此请求的结果,以便我可以为它编写可预测的测试。

以下是Activity中的API代码:

@Override                                                                                             
protected void onCreate(@Nullable Bundle savedInstanceState) {                                        

    // ...                                                  

    // Perform call                                                                                   
    viewModel.loadStuff()                                                                           
        .subscribeOn(Schedulers.io())                                                                 
        .observeOn(AndroidSchedulers.mainThread())                                                    
        .doOnSubscribe(disposable -> progressBar.setVisibility(View.VISIBLE))                         
        .doOnComplete(() -> progressBar.setVisibility(View.GONE))                                     
        .subscribe(response -> {                                              
            // Success
            // ...                                                                                         
        }, throwable -> {
            // Fail                                                                
            throwable.printStackTrace();                                                          
            errorStub.inflate();                                                                                          
        });
}

我写的一个Espresso测试是在几秒钟后测试此Activity中按钮的启用状态。但是,由于上述API调用在测试时失败,该按钮被ViewStub覆盖,测试失败。

我尝试通过编写自定义成功响应脚本来实现OkHTTP3的MockWebServer(注意应用程序使用Retrofit,而不是直接使用OkHTTP)。但是,似乎要使用MockWebServer,您必须使用它返回的自定义URL,并让您的应用程序代码使用该URL进行调用。这意味着修改应用程序代码以适应测试代码,这是没有意义的。

我也听说过自定义Dagger设置来解决这个问题。

问题:

  1. 如何设置我的测试,以便Activity的{​​{1}}方法中执行的API调用可以取消或控制?
  2. 如果onCreate 正确用于此案例,那么如何在不修改应用程序代码的情况下使用它呢?
  3. 赞赏与此问题相关的一般性评论和提示。

2 个答案:

答案 0 :(得分:2)

  

如何设置测试以便在API中执行API调用   Activity的onCreate方法可以无效或控制吗?

您的viewModel.loadStuff()似乎与某些预先定义的网址硬连线。为了使其可测试,我将在viewmodel创建期间将url作为参数传递或将其传递给loadStuff方法。我将演示第二种选择。
接下来的问题是我们如何在测试期间更改网址?一种方法是将URL存储在Application实例中并覆盖测试值。

<强> 1。创建包含url值的应用程序类。

class MyApplication extends Application {
    private static final String DEFAULT_URL = "http://yourdomain.com/api";
    private String url;

    @Override
    void onCreate() {
        super.onCreate();

        url = DEFAULT_URL;
    }

    public String getUrl() {
        return url;
    }

    @VisibleForTesting
    public void setUrl(String newUrl) {
        url = newUrl
    }
}

<强> 2。解析活动中的网址值

@Override                                                                                             
protected void onCreate(@Nullable Bundle savedInstanceState) {                                        
    // resolve url value
    String url = ((MyApplication) getApplication()).getUrl();                                         

    // Perform call                                                                                   
    viewModel.loadStuff(url)                                                                           
        .subscribeOn(Schedulers.io())                                                                 
        .observeOn(AndroidSchedulers.mainThread())                                                    
        .doOnSubscribe(disposable -> progressBar.setVisibility(View.VISIBLE))                         
        .doOnComplete(() -> progressBar.setVisibility(View.GONE))                                     
        .subscribe(response -> {                                              
            // Success
            // ...                                                                                         
        }, throwable -> {
            // Fail                                                                
            throwable.printStackTrace();                                                          
            errorStub.inflate();                                                                                          
        });
}

第3。在测试中,您可以覆盖URL以使用MockWebServer的URL。

// Espresso Test
public class ActivityTest {

  @Before
    public void setUp() throws Exception {
        MockWebServer webServer = new MockWebServer();
        HttpUrl url = webServer.url("/");

        // set the url to point to mock webserver in localhost
        MyApplication app = (MyApplication) InstrumentationRegistry.getTargetContext().getApplicationContext();
        app.setUrl(url.toString());
    }

    ....
}

您还可以使用共享首选项来存储服务器URL并在测试期间覆盖该值,但想法是相同的: 您的视图模型不应该硬连线到某个URL,而是将URL注入视图模型。 为了解决这样的依赖性,Dagger是个不错的选择。

答案 1 :(得分:0)

我建议使用MockWebServer(还有Wiremock但设置稍微复杂一点,所以让我们坚持使用 MockWebServer )并模拟 Dagger 模块。

<强> 1。添加模拟应用程序和模块

首先,我们需要使api模块独立于 baseUrl 。我们将有 BaseUrlProvider 两个实现(仅用于ui测试的普通和模拟)。

@Module
class ApiModule {

    @Provides
    fun provideRetrofit(baseUrlProvider: BaseUrlProvider): Retrofit {
        return Retrofit.Builder()
                .baseUrl(baseUrlProvider.baseUrl())
                .builc()
    }

    @Provides
    fun provideMyApi(retrofit: Retrofit): MyApi {
        return retrofit.create(MyApi::class.java)
    }
}

我们需要提供 BaseUrlProvider 的模拟实现。 Mock baseUrl 可以存储在测试应用程序类中。

@Module
abstract class MockBaseUrlModule {

    @Binds
    abstract fun bindBaseUrlProvider(baseUrlProvider: MockBaseUrlProvider): BaseUrlProvider
}

class MockBaseUrlProvider(private val context: Context) : BaseUrlProvider {

    override fun baseUrl: String {
        return (context as MockApplication).baseUrl
    }   
}

class MockApplication : MyApplication() {

    var baseUrl: String = ""

    override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
        return DaggerMockAppComponent.builder().build()
    }
}

自定义运行器负责创建正在测试的应用程序实例。

class MockRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader, className: String, context: Context): Application {
        return super.newApplication(cl, MockApplication::class.java.name, context)
    }
}

android {       
    defaultConfig {        
        testInstrumentationRunner "com.foo.MockRunner"
    }
}

<强> 2。添加规则

我们需要启动模拟服务器并在应用程序中保存模拟 baseUrl

class MockWebServerRule : ExternalResource() {

    val server: MockWebServer = MockWebServer()

    override fun before() {
        server.start()
        val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as MockApplication
        app.baseUrl = server.url("/").toString()
    }

    override fun after() {
        server.shutdown()
    }  
}

第3。添加测试

请求在活动 onCreate 中生成,因此需要在模拟响应后手动启动活动。

class MyActivityTest {

    @Rule
    @JvmField
    val activityRule = ActivityTestRule(MyActivity::class.java, false, null)

    @Rule
    @JvmField
    val mockWebServerRule = MockWebServerRule()

    @Test
    fun testFailure() {
        mockWebServerRule.server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST))
        activityRule.launchActivity(null) 
        //assert view          
    }
}