用于移动测试自动化的模拟服务器

在处理最新项目时,我面临着测试在移动逻辑上与各种第三方服务连接的移动应用程序的需求。 测试这些服务不是我的任务,但是,API的问题阻止了应用程序本身的工作-测试失败的原因不是内部的问题,而是因为API的不可操作性,甚至是在未检查必要的功能之前。

传统上,支架用于测试此类应用。 但是它们并不总是正常工作,这会干扰工作。 作为替代解决方案,我使用了moki。 我今天想谈谈这条棘手的道路。

图片

为了不涉及实际项目的代码(在NDA下),为进一步讨论的清晰起见,我为Android创建了一个简单的REST客户端,该客户端允许使用我需要的参数将HTTP请求(GET / POST)发送到某个地址。 我们将对其进行测试。
可以从GitLab下载客户端应用程序代码,调度程序和测试。

有哪些选择?


在我的情况下,有两种方法可以模仿:

  • 在云中或远程计算机上部署模拟服务器(如果我们正在谈论不能带到云中的机密开发);
  • 在要测试移动应用程序的电话上,在本地启动模拟服务器。

第一种选择与测试平台没有太大不同。 确实,可以在网络中为模拟服务器分配一个工作站,但是像任何测试台一样,它需要得到支持。 在这里,有必要遇到这种方法的主要陷阱。 远程工作站已死亡,停止响应,发生了某些更改-您需要监视,更改配置,即 与在常规测试台的支持下进行所有操作相同。 我们无法为自己纠正这种情况,而且肯定会比任何本地操作花费更多的时间。 因此,特别是在我的项目中,在本地提升模拟服务器更为方便。

选择模拟服务器


有很多不同的工具。 我尝试与几个人一起工作,几乎在每个人中我都遇到了一些问题:

  • 模拟服务器wiremock-两个无法在Android上正常运行的模拟服务器。 由于所有实验都是作为现场项目的一部分进行的,因此选择的时间有限。 和他们一起挖了几天后,我放弃了尝试。
  • Restmockokhttpmockwebserver的包装, 稍后将对其进行详细讨论。 看起来不错,它开始了,但是这个包装器的开发人员“在幕后”隐藏了设置模拟服务器的IP地址和端口的功能,对我来说这很关键。 Restmock在某个随机端口上启动。 仔细查看代码,我看到初始化服务器时,开发人员使用了一种方法来随机设置端口(如果它在输入中未接收到该端口)。 原则上,可以从此方法继承,但是问题出在私有构造函数中。 结果,我拒绝了包装纸。
  • Okhttpmockwebserver-尝试了不同的工具后,我停在了模拟服务器上,该服务器通常会聚在一起并在设备上本地启动。

我们分析工作原理


当前版本的okhttpmockwebserver允许您实现几种工作方案:

  • 答案排队 。 模拟服务器响应将添加到FIFO队列中。 不管我要访问哪个API和哪个路径,模拟服务器都会轮流向该队列中抛出消息。
  • 调度程序允许创建确定给出答案的规则。 假设请求来自包含路径的URL,例如/ get-login /。 在这个/ get-login /模拟服务器上,并给出一个预定义的响应。
  • 请求验证者 。 基于前面的场景,我可以检查应用程序发送的请求(在给定条件下,确实存在带有某些参数的请求)。 但是,答案并不重要,因为它取决于API的工作方式。 该脚本实现了请求验证器。

更详细地考虑每个方案。

响应队列


模拟服务器最简单的实现是响应队列。 在测试之前,我将确定将在其中部署模拟服务器的地址和端口,以及它将根据消息队列的原理工作的事实-FIFO(先进先出)。

接下来,运行模拟服务器。

class QueueTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.enqueue(MockResponse().setBody("1st message")) mockServer.enqueue(MockResponse().setBody("2nd message")) mockServer.enqueue(MockResponse().setBody("3rd message")) mockServer.start(ip, port) } @Test fun queueTest() { sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("1st message") returnFromResponseActivity() sendPostRequest("http://localhost:8080/getMessage") assertResponseMessage("2nd message") returnFromResponseActivity() sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("3rd message") returnFromResponseActivity() } } 

测试是使用Espresso框架编写的,该框架旨在在移动应用程序中执行操作。 在此示例中,我选择请求类型并依次发送它们。
开始测试后,模拟服务器将根据指定的队列为其提供答案,并且测试顺利通过且没有错误。

分派器实施


调度程序是模拟服务器操作所依据的一组规则。 为了方便起见,我创建了三个不同的调度程序:SimpleDispatcher,OtherParamsDispatcher和ListingDispatcher。

简单调度程序


Okhttpmockwebserver提供Dispatcher()类来实现调度程序。 您可以通过以自己的方式覆盖dispatch函数来从中继承。

 class SimpleDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { if (request.method == "GET"){ return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request" }""") } else if (request.method == "POST") { return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request" }""") } return MockResponse().setResponseCode(200) } } 

此示例中的逻辑很简单:如果GET到达,我将返回一条消息,表明这是GET请求。 如果是POST,我将返回有关POST请求的消息。 在其他情况下,我返回一个空请求。

SimpleDispatcher出现在测试中-我已在上面描述了SimpleDispatcher类的一个对象。 此外,如前面的示例中所示,模拟服务器已启动,仅在这一次才指示使用该模拟服务器的一种规则-相同的调度程序。

在存储库中可以找到带有SimpleDispatcher测试源。

其他ParamsDispatcher


覆盖dispatch功能,我可以从其他请求参数中推送来发送响应:

 class OtherParamsDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { return when { request.path.contains("?queryKey=value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request with query parameter queryKey equals value" }""") request.body.toString().contains("\"bodyKey\":\"value\"") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request with body parameter bodyKey equals value" }""") request.headers.toString().contains("header: value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was some request with header equals value" }""") else -> MockResponse().setResponseCode(200).setBody("""{ Wrong response }""") } } } 

在这种情况下,我将针对条件演示几种选择。

首先,您可以在地址栏中将参数传递给API。 因此,我可以在某种捆绑形式的路径上设置条件,例如“?queryKey=value”
其次,该类使您可以进入POST或PUT请求的主体内部。 例如,可以通过先执行toString()来使用contains 。 在我的示例中,当包含“bodyKey”:”value”的POST请求“bodyKey”:”value”时,触发条件。 同样,我可以验证请求header : valueheader : value )。

有关测试示例,建议您参考存储库

ListingDispatcher


如有必要,可以实现更复杂的逻辑-ListingDispatcher。 同样,我覆盖了dispatch功能。 但是,现在在该类中,我设置了默认的stubsList集( stubsListstubsList用于不同的场合。

 class ListingDispatcher: Dispatcher() { private var stubsList: ArrayList<RequestClass> = defaultRequests() @Override override fun dispatch(request: RecordedRequest): MockResponse = try { stubsList.first { it.matcher(request.path, request.body.toString()) }.response() } catch (e: NoSuchElementException) { Log.e("Unexisting request path =", request.path) MockResponse().setResponseCode(404) } private fun defaultRequests(): ArrayList<RequestClass> { val allStubs = ArrayList<RequestClass>() allStubs.add(RequestClass("/get", "queryParam=value", "", """{ "message" : "Request url starts with /get url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "queryParam=value", "", """{ "message" : "Request url starts with /post url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Request url starts with /post url and body contains bodyParam:value" }""")) return allStubs } fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) } fun addMockStub(stub: RequestClass) { stubsList.add(stub) } } 

为此,我创建了一个开放类RequestClass ,默认情况下其所有字段均为空。 对于此类,我定义了一个response函数,该函数创建一个MockResponse对象(返回200响应或其他一些responseText ),以及一个matcher函数,返回truefalse

 open class RequestClass(val path:String = "", val query: String = "", val body:String = "", val responseText: String = "") { open fun response(code: Int = 200): MockResponse = MockResponse() .setResponseCode(code) .setBody(responseText) open fun matcher(apiCall: String, apiBody: String): Boolean = apiCall.startsWith(path)&&apiCall.contains(query)&&apiBody.contains(body) } 

结果,我可以为存根建立更复杂的条件组合。 尽管其原理非常简单,但在我看来,这种设计更为灵活。

但最重要的是,如果需要在一个测试中更改模拟服务器的响应,我可以随时随地替换一些存根。 在测试大型项目时,例如,在检查某些特定方案时,经常会出现此问题。
更换可以如下进行:

 fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) } 

使用调度程序的这种实现,测试将保持简单。 我还启动了模拟服务器,仅选择ListingDispatcher

 class ListingDispatcherTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) private val dispatcher = ListingDispatcher() @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.setDispatcher(dispatcher) mockServer.start(ip, port) } . . . } 

为了进行实验,我将存根替换为POST:

 @Test fun postReplacedStubTest() { val params: HashMap<String, String> = hashMapOf("bodyParam" to "value") replacePostStub() sendPostRequest("http://localhost:8080/post", params = params) assertResponseMessage("""{ "message" : "Post request stub has been replaced" }""") } 

为此,请从常规dispatcher调用replacePostStub函数,并添加新的response

 private fun replacePostStub() { dispatcher.replaceMockStub(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Post request stub has been replaced" }""")) } 

在上面的测试中,我确认存根已被替换。
然后,我添加了一个新的存根,它不是默认值。

 @Test fun getNewStubTest() { addSomeStub() sendGetRequest("http://localhost:8080/some_specific_url") assertResponseMessage("""{ "message" : "U have got specific message" }""") } 

 private fun addSomeStub() { dispatcher.addMockStub(RequestClass("/some_specific_url", "", "", """{ "message" : "U have got specific message" }""")) } 

请求验证者


最后一种情况-请求验证程序-不提供监听功能,但会检查应用程序发送的请求。 为此,我只是通过实现调度程序来启动模拟服务器,以便应用程序至少返回某些内容。
从测试发送请求时,它会到达模拟服务器。 通过它,我可以使用takeRequest()访问请求参数。

 @Test fun requestVerifierTest() { val params: HashMap<String, String> = hashMapOf("bodyKey" to "value") val headers: HashMap<String, String> = hashMapOf("header" to "value") sendPostRequest("http://localhost:8080/post", headers = headers, params = params) val request = mockServer.takeRequest() assertEquals("POST", request.method) assertEquals("value", request.getHeader("header")) assertTrue(request.body.toString().contains("\"bodyKey\":\"value\"")) assertTrue(request.path.startsWith("/post")) } 

上面,我通过一个简单的示例显示了该测试。 完全相同的方法可用于复杂的JSON,包括检查请求的整个结构(您可以在JSON级别进行比较或将JSON解析为对象并在对象级别检查是否相等)。

总结


通常,我喜欢该工具(okhttpmockwebserver),并在大型项目中使用它。 当然,有些小事情我想改变。
例如,我不喜欢您必须在应用程序的配置中敲入本地地址(在我们的示例中为localhost:8080)。 也许我仍然可以找到一种配置所有内容的方法,以便模拟服务器在尝试向任何地址发送请求时做出响应。
另外,如果模拟服务器没有合适的存根,则我无法重定向请求-当模拟服务器进一步发送请求时。 此模拟服务器中没有这种方法。 但是,它甚至还没有实现,因为目前“战斗”项目还没有这样的任务。

文章作者:Ruslan Abdulin

PS:我们在Runet的多个站点上发表文章。 订阅我们在VKFBTelegram频道上的页面,以查找有关我们所有出版物和其他Maxilect新闻的信息。

Source: https://habr.com/ru/post/zh-CN430530/


All Articles