使用MockWebServer升级你的UI测试

今年春天,我花了一周时间,终于深入研究了Espresso咖啡的测试。我以前也用过这个工具来自动化应用程序,但是Espresso测试有一个问题一直困扰着我:网络嘲笑。

通过亚当麦克尼尔公司

一年三次,OkCupid的开发人员可以188bet金宝搏官网把我们的日常职责放在一边,享受“Hack Week”,这是一个完整的工作周,让我们有更多的自由去探索和学习,去解决我们想要解决的问题。

有很多导游,包括官方导游咖啡文档这可以帮助你开始自动化应用,但如果你想编写可靠的UI测试,你需要模拟你的网络层。如果不这样做,您的测试可能不一致。让我们考虑在OkCupid上测试DoubleTake(这是主屏幕,你可以在188bet金宝搏官网这里向左/向右滑动用户卡片来投票)。如果不是嘲笑网络,我不知道

  • 哪个用户将首先出现在卡片堆栈中。
  • 如果只有用户卡会出现(而不是广告或特别公告)。
  • 如果喜欢一个用户将导致相互匹配,即使我的测试并不期望它。

对于OkCupi188bet金宝搏官网d应用程序来说,嘲笑网络层似乎是一项特别具有挑战性的任务。我们现在还没有使用Dagger或其他依赖注入框架,而且我们的很多网络代码都与Application类紧密耦合。但事实证明,这根本不是一个大问题!

我应该使用什么工具?

当我第一次尝试时,我尝试了WireMock。WireMock是一个HTTP模拟工具,它允许你在你的Android设备上运行一个HTTP服务器,你的应用程序可以与它通信,而不是与你自己的服务器通信。188博金宝电子体育频道我了解到WireMock多亏了Sam Edwards在2017纽约机器人大会上,他深入地解释了如何使用WireMock模拟HTTP api。如果你正在寻找关于WireMock的深入指南,我强烈推荐你。

但是,Sam还提到了另一种称为MockWebServer.MockWebServer是一个Square库,它实现了相同的目标——为HTTP响应运行模拟服务。MockWebServer实际上更轻量级,在尝试了这两种方法后,我发现MockWebServer的设置要简单得多(添加WireMock作为gradle依赖项给了我一些必须解决的冲突)。因为MockWebServer拥有我需要的所有功能,所以我决定继续使用它。

奠定基础

在继续实现MockWebServer之前,我们先讨论一下实现该功能需要做的一些基础工作。MockWebServer将在本地主机上启动一个web服务器,这意味着我们所有的请求都将通过http://localhost:8080而不是http://myendpoint.为你的测试应用程序交换端点可以通过两个步骤完成:

  1. 创建一个TestApplication扩展你的常规应用程序类,并覆盖你的基URL。
  2. 创建一个将使用您的测试应用程序的定制JUnit运行器。

创建一个TestApplication

首先,让我们考虑我们有自己的Application类,它公开我们的API url:

打开OkApp: Application() {
gettapiurl (): String {
返回“http://apiurl”

在你的AndroidTe188博金宝电子体育频道st目录中,创建一个应用类,扩展你在应用中使用的类:

类TestApplication: OkApp() {
gettapiurl (): String {
返回“http://127.0.0.1:8080”

注意,这里我重写了一个返回localhost API url的方法,而不是应用程序使用的方法。这是否意味着我们的应用程序的所有请求都将访问本地主机?不完全是,Espresso仍然在使用我们的常规应用程序类。为了解决这个问题,我们需要一个定制的运行器。

自定义JUnit跑步

创建自定义JUnit运行器只需要一个快速更新:覆盖newApplication方法,并让它指向你的测试应用程序类,而不是原来的:

MockTestRunner: Android188博金宝电子体育频道JUnitRunner() {
重写fun onCreate(arguments: Bundle?) {
.build .permitAll StrictMode.setThreadPolicy (StrictMode.ThreadPolicy.Builder () () ())
super.onCreate(参数)


override fun newApplication(cl: ClassLoader?名称:字符串?):应用程序{
超级回报。newApplication (cl, TestApplication:: class.java.name,上下文)

然后,我们需要进入应用的构建。Gradle文件,并配置它指向这个运行器:

188博金宝电子体育频道android {
defaultConfig {
testInstrumentationRunner com.my.package.MockTestRunner”

现在我们已经完成了基础设置,我们的应用程序已经指向localhost,让我们在那里运行一些东西。

MockWebServer设置

我们可以从复制/粘贴步骤开始,包括MockWebServer依赖:

188博金宝电子体育频道androidTestImplementation“com.squareup.okhttp3: mockwebserver: $ {version.okhttpVersion}”

接下来,我们需要为每个测试启动模拟web服务器。我们可以在测试类的setup和teardown方法中配置它:

@RunWith 188博金宝电子体育频道(AndroidJUnit4::类)
类MainActivityTest {
/ /……

私有var mockWebServer = mockWebServer ()

@Before
有趣的设置(){
mockWebServer.start (8080)


@After
乐趣teardown () {
mockWebServer.shutdown ()


/ /……

当我们启动我们的web服务器时,我们给它一个运行的端口。这和我们在测试应用类中的端口是一样的;这些对齐很重要。如果您希望这里更安全一点,可以将端口移到构建配置字段中。

响应模拟

祝贺你走了这么远!我知道你有很多话要说。让我们看看现在的情况:

  • 我们已经创建了一个TestApplication类,它允许我们覆盖常规应用程序将使用的任何东西。
  • 我们已经学习了如何实现我们自己的定制JUnit运行器来使用这个TestApplication类。
  • 我们这样做了,当我们的应用程序运行连接测试时,它将与localhost对话,而不是到你公司的服务器。

所有伟大的事情!不幸的是,我们还没有嘲笑任何回应。如果按原样运行测试,就会看到很多404错误。我们来谈谈如何解决这个问题。

文件看样板

从广义上说,这里有两种方法:通过编程方式从模型对象中生成模拟响应,或者从JSON文件中读取模拟响应(如果您认为其中一种比另一种更好,请告诉我在推特上因为我真的不知道这里什么是最好的)。我选择使用文件,我将简要地介绍为实现这一目的而添加的样板代码。

你想把所有文件保存在以下位置:

应用程序/ src /调试/资产/ 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 . getininstrumentation ().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().setResponseCode(404)

val pathWithoutQueryParams = Uri.parse(request?.path)。: return errorResponse
val responseFile = responseFilesByPath[pathWithoutQueryParams]

return if (responseFile != null) {
val responseBody = asset(context, responseFile)
MockResponse () .setResponseCode (200) .setBody (responseBody)
其他}{
errorResponse


我命名了这个调度员SuccessDispatcher因为它只返回成功响应。如果希望覆盖响应,我建议不要使用端点到文件名的静态映射,而是公开一个方法,该方法允许您定义每个端点应该模拟的内容。

开始测试前模拟

您需要注意的最后一件事是,您需要确保启动了MockWebServer之前运行您的应用程序。可能你的应用程序在初始化时发出网络请求,所以如果你首先启动应用程序(这是Espresso默认值),事情可能不会像预期的那样工作。你可以调整你的ActivityTestRule在setup方法中启动,像这样:

@RunWith 188博金宝电子体育频道(AndroidJUnit4::类)
类MainActivityTest {
@JvmField
@Rule
var activityTestRule (MainActivity::class.java, true, false)

私有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日。

188bet金宝搏官网OkCupid科技博客

阅读OkCupid工程团队每天连接数百万人的188bet金宝搏官网故事

188bet金宝搏官网OkCupid科技博客

188bet金宝搏官网OkCupid的工程团队负责每天为数百万人配对。在OkCupid科技博客上阅读他们的故事188bet金宝搏官网

188bet金宝搏官网OkCupid科技博客

188bet金宝搏官网OkCupid的工程团队负责每天为数百万人配对。在OkCupid科技博客上阅读他们的故事188bet金宝搏官网