在浓缩咖啡测试下取消或覆盖在活动中进行的 API 调用



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

这在使用浓缩咖啡执行 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. 如何设置我的测试,以便在ActivityonCreate方法中执行的API调用可以无效或控制?
  2. 如果在这种情况下使用MockWebServer是正确的,如何在不修改应用程序代码的情况下使用它?

欢迎与此问题相关的一般性评论和提示。

如何设置我的测试,以便在 活动的 onCreate 方法可以无效还是受控?

似乎您的viewModel.loadStuff()已硬连接到某个预先挑衅的 URL。为了使其可测试,我会在视图模型创建期间将 url 作为参数传递或将其传递给loadStuff方法。我将演示第二个选项。
下一个问题是我们如何在测试期间更改 url?一种方法是将 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 以使用模拟网络服务器的 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 是一个不错的选择。

我建议使用MockWebServer(也有Wiremock,但设置有点复杂,所以让我们坚持使用MockWebServer)和mockDagger模块。

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的模拟实现。模拟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. 添加测试

请求是在活动中发出的,因此需要在模拟响应后手动启动活动。

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          
}
}

最新更新