モバイルテストを自動化するための模擬サーバー

最新のプロジェクトに取り組んでいる間に、ビジネスロジックのレベルでさまざまなサードパーティサービスに接続されたモバイルアプリケーションのテストに直面しました。 これらのサービスのテストは私のタスクの一部ではありませんでしたが、APIの問題によりアプリケーション自体の動作がブロックされました-テストは内部の問題ではなく、必要な機能のチェックに到達する前でもAPIの動作不能のために落ちました。



従来、このようなアプリケーションのテストにはスタンドが使用されていました。 しかし、それらは常に正常に機能するとは限らず、これが作業の妨げになります。 代替ソリューションとして、mokiを使用しました。 今日、この厄介な道についてお話したいと思います。



画像



実際のプロジェクトのコード(NDAの下)に触れないようにするために、詳細な説明を明確にするために、必要なパラメーターを指定した特定のアドレスにHTTPリクエスト(GET / POST)を送信できるAndroid用のシンプルなRESTクライアントを作成しました。 テストします。

クライアントアプリケーションコード、ディスパッチャー、およびテストはGitLabからダウンロードできます。



オプションは何ですか?



私の場合、2つのアプローチがありました。





最初のオプションは、テストベンチと大差ありません。 実際、ネットワーク内のワークステーションをモックサーバーに割り当てることは可能ですが、テストスタンドのようにサポートする必要があります。 そしてここで、このアプローチの主な落とし穴に遭遇する必要があります。 リモートワークステーションが停止し、応答を停止し、何かが変更されました-監視、設定の変更、つまり 通常のテストベンチのサポートと同じことを行います。 私たちは自分のために状況を修正しません、そして、それは確かにどんなローカル操作よりも多くの時間がかかります。 したがって、特に私のプロジェクトでは、モックサーバーをローカルに上げる方が便利でした。



モックサーバーの選択



さまざまなツールがあります。 私はいくつかと協力しようとしましたが、ほとんどすべての人で特定の問題に遭遇しました:





仕事の原理を分析します



okhttpmockwebserverの現在のバージョンでは、いくつかの作業シナリオを実装できます。





各シナリオをより詳細に検討してください。



応答キュー



モックサーバーの最も単純な実装は、応答キューです。 テストの前に、モックサーバーが展開されるアドレスとポート、およびメッセージキューの原理(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の3つの異なるディスパッチャを作成しました。



シンプルディスパッチャー



Okhttpmockwebserverは、 Dispatcher()



を実装する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要求に関するメッセージを返します。 他の状況では、空のリクエストを返します。



テストでは、 dispatcher



が表示dispatcher



ます-前述の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”



など、何らかの種類のバンドルのパスに条件を設定でき“?queryKey=value”





次に、このクラスを使用すると、POSTまたはPUTリクエストの本文を取得できます。 たとえば、最初にtoString()



実行することにより、 contains



を使用できます。 この例では、 “bodyKey”:”value”



を含むPOST要求“bodyKey”:”value”



と、条件がトリガーされ“bodyKey”:”value”



。 同様に、要求header : value



header : value



)を検証できheader : value







テストの例については、リポジトリを参照することをお勧めします



ListingDispatcher



必要に応じて、より複雑なロジックであるListingDispatcherを実装できます。 同様に、 dispatch



機能をオーバーライドdispatch



ます。 しかし、今では、クラス内で、デフォルトのstubsList



セット( stubsList



)を設定します-さまざまな場面でのmokです。



 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



を作成しました。そのすべてのフィールドはデフォルトで空です。 このクラスでは、 MockResponse



オブジェクト(200応答または他のresponseText



を返す)を作成するresponse



関数と、 true



またはfalse



を返すmatcher



関数を定義します。



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





その結果、スタブの条件のより複雑な組み合わせを構築できます。 この設計はより柔軟に思えましたが、その中心にある原理は非常に単純です。



しかし、このアプローチの何よりも、1つのテストでモックサーバーの応答に何かを変更する必要がある場合、いくつかのスタブをすぐに置き換えることができるのが好きでした。 大規模なプロジェクトをテストする場合、たとえば特定のシナリオをチェックする場合など、この問題が頻繁に発生します。

交換は次のように実行できます。



 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



関数を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のいくつかのサイトで記事を公開しています。 VKFB、または電報チャネルのページを購読して、すべての出版物およびその他のMaxilectニュースについて学びます。



All Articles