従来、このようなアプリケーションのテストにはスタンドが使用されていました。 しかし、それらは常に正常に機能するとは限らず、これが作業の妨げになります。 代替ソリューションとして、mokiを使用しました。 今日、この厄介な道についてお話したいと思います。
実際のプロジェクトのコード(NDAの下)に触れないようにするために、詳細な説明を明確にするために、必要なパラメーターを指定した特定のアドレスにHTTPリクエスト(GET / POST)を送信できるAndroid用のシンプルなRESTクライアントを作成しました。 テストします。
クライアントアプリケーションコード、ディスパッチャー、およびテストはGitLabからダウンロードできます。
オプションは何ですか?
私の場合、2つのアプローチがありました。
- クラウドまたはリモートマシンにモックサーバーを展開します(クラウドに持ち込むことができない機密の開発について話している場合)。
- モックサーバーをローカルで起動します-モバイルアプリケーションがテストされている電話で。
最初のオプションは、テストベンチと大差ありません。 実際、ネットワーク内のワークステーションをモックサーバーに割り当てることは可能ですが、テストスタンドのようにサポートする必要があります。 そしてここで、このアプローチの主な落とし穴に遭遇する必要があります。 リモートワークステーションが停止し、応答を停止し、何かが変更されました-監視、設定の変更、つまり 通常のテストベンチのサポートと同じことを行います。 私たちは自分のために状況を修正しません、そして、それは確かにどんなローカル操作よりも多くの時間がかかります。 したがって、特に私のプロジェクトでは、モックサーバーをローカルに上げる方が便利でした。
モックサーバーの選択
さまざまなツールがあります。 私はいくつかと協力しようとしましたが、ほとんどすべての人で特定の問題に遭遇しました:
- モックサーバー 、 wiremock -2台のモックサーバー 。Androidでは正常に実行できませんでした。 すべての実験はライブプロジェクトの一部として行われたため、選択の時間は限られていました。 数日彼らと掘った後、私は試してみました。
- Restmockはokhttpmockwebserverのラッパーです。これについては後で詳しく説明します。 最初は良さそうでしたが、このラッパーの開発者は、「裏側」で、模擬サーバーのIPアドレスとポートを設定する機能を隠していました。 ランダムポートでRestmockが起動しました。 コードをいじってみると、サーバーが初期化されたときに、開発者が入力でポートを受信しなかった場合にポートをランダムに設定するメソッドを使用していることがわかりました。 原則として、このメソッドから継承できますが、問題はプライベートコンストラクターにありました。 その結果、ラッパーを拒否しました。
- Okhttpmockwebserver-さまざまなツールを試した後、通常は一緒になってデバイス上でローカルに起動するモックサーバーで停止しました。
仕事の原理を分析します
okhttpmockwebserverの現在のバージョンでは、いくつかの作業シナリオを実装できます。
- 回答のキュー 。 モックサーバーの応答がFIFOキューに追加されます。 どのAPIとどのパスにアクセスするかは重要ではありません。Mokサーバーは順番にこのキューにメッセージを投げます。
- ディスパッチャを使用すると、与える答えを決定するルールを作成できます 。 / get-login /など、パスを含むURLからリクエストが来たとします。 この/ get-login / mockサーバーで、事前定義された単一の応答を提供します。
- 要求検証者 。 前のシナリオに基づいて、アプリケーションが送信する要求を確認できます(特定の条件下では、特定のパラメーターを持つ要求が実際に送信されます)。 答えは重要ではありません。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の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のいくつかのサイトで記事を公開しています。 VK 、 FB、または電報チャネルのページを購読して、すべての出版物およびその他のMaxilectニュースについて学びます。