使用MockWebServer升级UI测试
今年春天,我终于花了一周时间深入研究Espresso的测试。我以前用这个工具自动化过应用程序,但在Espresso测试中有一个问题我一直在努力解决:网络嘲笑。
通过亚当麦克尼尔公司

OkCupid的开发人员每年有三次机会188bet金宝搏官网将日常工作抛到一边,享受“Hack Week”,这是一个让我们在更自由的探索和学习中追求自己想要解决的问题的完整工作周。
这里有很多导游,包括官方导游咖啡文档但如果你想编写可靠的UI测试,你需要模拟你的网络层。如果不这样做,您的测试可能不一致。让我们考虑在OkCupid上测试DoubleTake(这是你向左/向右滑动188bet金宝搏官网用户卡片进行投票的主屏幕)。除了嘲笑网络,我不知道:
- 哪个用户将首先出现在卡片堆栈中。
- 如果只显示用户卡(而不是广告或特别公告)。
- 如果喜欢一个用户会导致一个相互匹配的结果,即使我的测试没有期望它。
对于OkCupi188bet金宝搏官网d应用程序来说,模仿网络层似乎是一项特别具有挑战性的任务。我们现在不使用Dagger或其他依赖注入框架,我们的很多网络代码都与Application类紧密耦合。但事实证明,这根本不是什么大问题!
我应该使用什么工具?
当我第一次尝试时,我尝试了WireMock。丝袜是一个HTTP模仿工具,允许你在Android设备上运行HTTP服务器,你的应用程序可以与之通信,而不是与你自己的服务器通信。188博金宝电子体育频道我了解WireMock是因为山姆·爱德华兹,2017年纽约Droidcon,他深入解释了如何使用WireMock来模拟HTTP api。如果你正在寻找关于WireMock的深入指南,我强烈推荐你。
然而,Sam也提到了另一种叫做MockWebServer.MockWebServer是一个Square库,它实现了相同的目标——为HTTP响应运行模拟服务。MockWebServer实际上更轻量级,在尝试了这两种方法之后,我发现MockWebServer的设置要容易得多(添加WireMock作为gradle依赖给我带来了一些必须解决的冲突)。因为MockWebServer拥有我需要的所有功能,所以我决定继续使用它。
奠定基础
在我们继续实现MockWebServer之前,让我们讨论一下实现这个功能所需做的一些基础工作。MockWebServer将在本地主机上启动一个web服务器,这意味着我们所有的请求都将通过http://localhost:8080
而不是http://myendpoint
.切换测试应用程序的端点可以通过两个步骤完成:
- 创建一个TestApplication来扩展您的普通应用程序类,并覆盖您的基URL。
- 创建一个将使用测试应用程序的自定义JUnit运行器。
创建一个TestApplication
首先,让我们考虑我们有自己的Application类,它公开我们的API url:
打开类OkApp: Application() {
open fun getApiUrl(): String {
返回“http://apiurl”
}
}
在AndroidTest188博金宝电子体育频道目录中,创建一个应用程序类,扩展你在应用程序中使用的类:
类TestApplication: OkApp() {
override fun getApiUrl(): String {
返回“http://127.0.0.1:8080”
}
}
注意,这里我重写了一个返回localhost API url的方法,而不是应用程序使用的那个。这是否意味着我们所有的应用请求都将击中本地主机?不完全是,Espresso仍然在使用我们的常规应用类。为了解决这个问题,我们需要一个自定义运行器。
自定义JUnit跑步
创建一个定制的JUnit运行器只需要一个快速的更新:覆盖newApplication
方法,并让它指向您的测试应用程序类,而不是原始的:
类MockTestRunner: Androi188博金宝电子体育频道dJUnitRunner() {
override fun onCreate(arguments: Bundle?) {
.build .permitAll StrictMode.setThreadPolicy (StrictMode.ThreadPolicy.Builder () () ())
super.onCreate(参数)
}
override fun newApplication(cl: ClassLoader?名称:字符串?):应用程序{
超级回报。newApplication (cl, TestApplication:: class.java.name,上下文)
}
}
然后,我们需要进入我们的应用程序的build.gradle文件,并将其配置为指向此Runner:
188博金宝电子体育频道android {
defaultconfig {
testInstrumentationRunner com.my.package.MockTestRunner”
}
}
现在我们有了基础设置,我们的应用指向localhost,让我们在那里运行一些东西。
MockWebServer设置
我们可以从包含MockWebServer依赖的复制/粘贴步骤开始:
188博金宝电子体育频道androidTestImplementation“com.squareup.okhttp3: mockwebserver: $ {version.okhttpVersion}”
接下来,我们需要为每个测试启动模拟web服务器。我们可以在测试类的setup and teardown方法中配置它:
@RunWith 188博金宝电子体育频道(AndroidJUnit4::类)
类MainActivityTest {
/ /……
private var mockWebServer = mockWebServer ()
@Before
有趣的设置(){
mockWebServer.start (8080)
}
@After
乐趣teardown () {
mockWebServer.shutdown ()
}
/ /……
}
当我们启动web服务器时,我们给它一个端口来运行。这与我们在测试应用程序类中使用的移植相同;这一点很重要。如果您希望这里更安全一些,可以将端口移到构建配置字段中。
响应模拟
恭喜你走了这么远!我相信这有很多要了解的。让我们看看现在的情况:
- 我们已经创建了一个TestApplication类,它允许我们覆盖常规应用程序将使用的任何内容。
- 我们已经学习了如何实现我们自己的定制JUnit运行程序来使用这个TestApplication类。
- 当我们的应用程序运行连接测试时,它将与本地主机通信,而不是到你公司的服务器。
所有伟大的事情!不幸的是,我们还没有嘲笑任何回应。如果按原样运行测试,您将看到大量404错误。我们来谈谈如何解决这个问题。
文件看样板
从广义上讲,这里我们有两种方法:以编程方式从模型对象中生成模拟响应,或者从JSON文件中读取模拟响应(如果您对其中一种优于另一种有什么想法,请告诉我在推特上因为我真的不知道这里什么是最好的)。我选择使用文件,我将简要讨论为实现这一点而添加的样板代码。
您希望将所有文件保存在以下位置:
app / src / debug / assets / network_files / endpoint_success.json
我选择调用这个文件夹network_files
,但你可以叫它任何你喜欢的名字。现在我们有了JSON文件,我们需要一种读取它们的方法。我创建了以下内容AssetReaderUtil.kt
文件,并存储在我的androidTest目录:188博金宝电子体育频道
对象AssetReaderUtil {
fun asset(context: context, assetPath: String): String {
尝试{
val inputStream = context.assets.open("network_files/$assetPath")
返回inputStreamToString (inputStream,“utf - 8”)
} catch (e: IOException) {
抛出RuntimeException (e)
}
}
private fun inputStreamToString(inputStream: inputStream, charsetName: String): String {
val builder = StringBuilder()
val reader = InputStreamReader(inputStream, charsetName)
reader.readLines()。forEach {
builder.append (it)
}
返回builder.toString ()
}
}
现在我们有这个,让我们使用这些模拟来走路。
调度程序
MockWebServer使用一种叫做调度程序来处理模拟的服务器响应。它包括一个用于覆盖的方法,该方法告诉我们请求是什么,并允许我们返回特定于该请求的模拟响应。
我的调度员是这样工作的:
- 它接受一个上下文,用于从我们在最后一步中创建的文件中读取内容。
- 它包含一个map属性,该属性将端点映射到模拟响应的文件名。
- 如果分派方法找到请求端点的文件名,它将返回模拟的响应。
- 如果没有找到模拟文件,则返回404。
这里是所有这些的代码:
类SuccessDispatcher (
private val context: context = InstrumentationRegistry.getInstrumentation().context
): Dispatcher() {
private val responseFilesByPath: Map = mapOf(
APIPaths。ENDPOINT_ONE MockFiles。ONE_SUCCESS_FILE,
APIPaths。ENDPOINT_TWO MockFiles。TWO_SUCCESS_FILE
)
override fun dispatch(request: RecordedRequest?): MockResponse {
val errorResponse = MockResponse(). setresponcode (404)
val pathWithoutQueryParams = Uri.parse(请求?.path)。path ?:返回errorResponse
val responseFile = responseFilesByPath[pathWithoutQueryParams]
return if (responseFile != null) {
val响应体=资产(上下文,responsefile)
MockResponse () .setResponseCode (200) .setBody (responseBody)
其他}{
errorResponse
}
}
}
我已经任命了这个调度员SuccessDispatcher
因为它只返回成功响应。如果要覆盖响应,我建议将远离端点的静态映射转向文件名,而是公开一种方法,允许您定义应为每个端点映射的内容。
开始测试前进行模拟
您需要注意的最后一件事是,您需要确保启动了MockWebServer之前运行您的申请。当您的应用程序初始化时,您的应用程序可能会使网络请求进行网络请求,因此如果您首先启动应用程序(默认为默认),则可能无法按预期工作。您可以调整ActivityTestrule以在设置方法内启动,如下所示:
@RunWith 188博金宝电子体育频道(AndroidJUnit4::类)
类MainActivityTest {
@JvmField
@Rule
var activityTestRule = activityTestRule (MainActivity::class.java, true, false)
private var mockWebServer = mockWebServer ()
@Before
有趣的设置(){
mockWebServer.start (BuildConfig.PORT)
mockWebServer。调度程序= SuccessDispatcher ()
activityTestRule.launchActivity(空)
}
@After
乐趣teardown () {
mockWebServer.shutdown ()
}
/ /……
}
样本
现在我们已经完成了所有这些,我们可以编写一个像这样的自动化测试,为我自己使用所有的模拟数据,而不是与真正的用户交互:

如果你想看到一个超级基础的应用程序使用这些工具的行动,请随时分叉此示例项目.
我希望这对你有帮助!如果您有任何问题,或有其他编写MockWebServer调度程序的独特方法,请访问我推特.
想和我一起去OkCupid吗?188bet金宝搏官网我们招聘!
最初发表在https://tech.188bet金宝搏官网okcupid.com2019年4月18日。