おそらくユニットテストと統合テストを書き始めた誰もが、脆弱なテストにつながるモカミの乱用の問題に直面していました。 後者は、テストが作業にのみ干渉するという誤った信念をプログラマに与えます。
以下は、Elixirの作成者であるJoséValimがモックの使用の問題について意見を表明した記事の無料翻訳です。
数日前、私は自分の考えをTwitterのムックで共有しました。
Mockはテストに役立つツールですが、既存のテストライブラリとフレームワークはこのツールの悪用につながることがよくあります。 以下では、mokを使用する最良の方法を検討します。
モックとは?
英語版ウィキペディアの定義:モック-実際のオブジェクトの動作を模倣するカスタムオブジェクトを使用します。 後でこれに焦点を当てますが、私にとっては、mokは常に名詞であり、動詞ではありません[ わかりやすくするために、動詞のモックは「ロックする」ようにどこでも翻訳されます。 perev。 ]。
例として外部APIを使用する
標準的な実際の例を見てみましょう:外部API。
PhoenixまたはRailsフレームワークを使用するWebアプリケーションでTwitter APIを使用するとします。 アプリケーションはコントローラーにリダイレクトされるリクエストを受信し、コントローラーは外部APIにリクエストを送信します。 外部APIの呼び出しは、コントローラーで直接行われます。
defmodule MyApp.MyController do def show(conn, %{"username" => username}) do # ... MyApp.TwitterClient.get_username(username) # ... end end
そのようなコードをテストするときの標準的なアプローチは、 MyApp.TwitterClien
使用するHTTPClient
ロックすることです(危険です!この場合のロックは動詞です!)。
mock(HTTPClient, :get, to_return: %{..., "username" => "josevalim", ...})
次に、アプリケーションの他の部分で同じアプローチを使用し、単体テストと統合テストを完了します。 次に進む時間ですか?
それほど速くない。 HTTPClient HTTPClient
の主な問題は、強力な外部依存関係を作成することです。 カップリングはどこでも「依存」として翻訳されます-約 perev。 ]特定のHTTPClient
。 たとえば、アプリケーションの動作を変更せずに新しい高速HTTPクライアントを使用することにした場合、すべてのHTTPClient
れた特定のHTTPClientに依存するため、ほとんどの統合テストはドロップします。 つまり、システムの動作を変更せずに実装を変更すると、テストが低下することになります。 これは悪い兆候です。
さらに、上記のモックはモジュールをグローバルに変更するため、これらのテストをElixirで並行して実行することはできなくなります。
解決策
HTTPClient
をMyApp.TwitterClient
代わりに、テスト中にMyApp.TwitterClient
を別のものに置き換えることができます。 ソリューションがElixirでどのように見えるかを見てみましょう。
Elixirでは、すべてのアプリケーションに設定ファイルとそれらを読み取るためのメカニズムがあります。 このメカニズムを使用して、さまざまな環境向けにTwitterクライアントを構成します。 コントローラコードは次のようになります。
defmodule MyApp.MyController do @twitter_api Application.get_env(:my_app, :twitter_api) def show(conn, %{"username" => username}) do # ... @twitter_api.get_username(username) # ... end end
さまざまな環境に適した設定:
# config/dev.exs config :my_app, :twitter_api, MyApp.Twitter.Sandbox # config/test.exs config :my_app, :twitter_api, MyApp.Twitter.InMemory # config/prod.exs config :my_app, :twitter_api, MyApp.Twitter.HTTPClient
これで、環境ごとにTwitterからデータを受信するための最適な戦略を選択できます。 Twitterが開発用に何らかのサンドボックスを提供している場合、サンドボックスは便利です。 HTTPClient
クローズバージョンは、実際のHTTP要求を回避しました。 この場合の同じ機能の実装:
defmodule MyApp.Twitter.InMemory do def get_username("josevalim") do %MyApp.Twitter.User{ username: "josevalim" } end end
コードはシンプルでクリーンであり、 HTTPClient
への強力な外部依存性はありません。 MyApp.Twitter.InMemory
はmok 、つまり名詞であり、作成にライブラリは必要ありません!
明示的な契約の必要性
Mockは、実際のオブジェクトを置き換えることを目的としています。つまり、実際のオブジェクトの動作が明示的に定義されている場合にのみ有効です。 そうしないと、モックがより硬くなり始め、テストされたコンポーネント間の依存関係が増加する状況に陥ることがあります。 明示的な契約がなければ、気付くことは困難です。
Twitter APIの実装はすでに3つありますが、それらの契約を明示することをお勧めします。 Elixirでは、 振る舞いを使用して明示的なコントラクトを記述できます 。
defmodule MyApp.Twitter do @doc "..." @callback get_username(username :: String.t) :: %MyApp.Twitter.User{} @doc "..." @callback followers_for(username :: String.t) :: [%MyApp.Twitter.User{}] end
ここで、この契約を実装する各モジュールに@behaviour MyApp.Twitter
を追加すると、Elixirが期待されるAPIの作成を支援します。
Elixirでは、常にこのような動作に依存しています。Plugを使用するとき、 Ectoでデータベースを操作するとき、 Phoenixチャンネルをテストするときなどです。
国境試験
最初は、明示的なコントラクトがない場合、アプリケーションの境界は次のようになりました。
[MyApp] -> [HTTPClient] -> [Twitter API]
したがって、 HTTPClient
変更により、統合テストが低下する可能性があります。 これで、アプリケーションはコントラクトに依存し、このコントラクトの1つの実装のみがHTTPで機能します。
[MyApp] -> [MyApp.Twitter (contract)]
[MyApp.Twitter.HTTP (contract impl)] -> [HTTPClient] -> [Twitter API]
このようなアプリケーションのテストは、 HTTPClient
およびTwitter APIから分離されています。 しかし、どうやってMyApp.Twitter.HTTP
をテストするのMyApp.Twitter.HTTP
うか?
大規模システムのテストの難しさは、コンポーネント間の明確な境界を定義することです。 統合テストがない場合の分離レベルが高すぎると、テストが脆弱になり、ほとんどの問題が実稼働時にのみ検出されます。 一方、低い分離レベルでは、テストの完了にかかる時間が長くなり、テストの維持が困難になります。 単一の正しい決定はありません。分離のレベルは、チームの信頼度やその他の要因によって異なります。
個人的には、開発中およびプロジェクトをビルドするたびに必要に応じてこれらのテストを実行し、実際のTwitter APIでMyApp.Twitter.HTTP
をテストします。 ElixirでテストするためのライブラリであるExUnitのタグシステムは、この動作を実装します。
defmodule MyApp.Twitter.HTTPTest do use ExUnit.Case, async: true # Twitter API @moduletag :twitter_api # ... end
Twitter APIを使用してテストを除外します。
ExUnit.configure exclude: [:twitter_api]
必要に応じて、一般的なテスト実行にそれらを含めます。
mix test --include twitter_api
それらを個別に実行することもできます。
mix test --only twitter_api
私はこのアプローチを好んではいますが、APIリクエストの最大数などの外部の制約は役に立たないことがあります。 この場合、使用が以前に定義されたルールに違反しない場合、おそらくHTTPClient
モックを使用する必要があります。
-
HTTPClient
の変更により、MyApp.Twitter.HTTP
テストがMyApp.Twitter.HTTP
するMyApp.Twitter.HTTP
- 濡れない(注意!この場合、モックは動詞です!)
HTTPClient
。 代わりに、設定ファイルを介して依存関係として渡します。これは、Twitter APIに対して行ったのと同様です。 - 運用環境に展開する前に、クライアントの作業をテストする方法が必要です。
HTTPClient HTTPClient
を作成する代わりに、Twitter APIをエミュレートするダミーサーバーを作成できます。 バイパスは、これを支援できるプロジェクトの1つです。 考えられるすべてのオプションについてチームと話し合う必要があります。
注釈
mokのほぼすべての議論で浮かび上がるいくつかの一般的な問題についての議論で、この記事を終わりたいと思います。
「テスト」コードの作成
elixir-talkメーリングリストからの引用:
提案されたソリューションは生産コードをより「テスト可能」にしますが、各関数呼び出しのアプリケーション構成に移動する必要が生じますか? 何かを「テスト可能」にするための不必要なオーバーヘッドがあることは、良い解決策とは思えません。
これは、「テスト可能な」コードを作成することではなく、デザインを改善することです[ 英語から。 コードの設計-約 perev。 ]。
テストは、作成する他のコードと同様に、APIのユーザーです。 TDDのアイデアの1つは、テストはコードであり、コードと変わらないということです。 「コードをテスト可能にしたくない」と言う場合は、「コンポーネント間の依存関係を減らしたくない」または「これらのコンポーネントのコントラクト(インターフェイス)を考えたくない」という意味です。
コンポーネント間の依存関係を減らしたくないのは問題ありません。 たとえば、URIを扱うモジュールについて話している場合[ ElixirのURIモジュールを意味する -約。 perev。 ]。 しかし、外部APIのような複雑なものについて話す場合、明示的なコントラクトを定義し、このコントラクトの実装を置き換える機能があると、コードが便利で維持しやすくなります。
さらに、Elixirアプリケーションの構成はETSに保存されるため、オーバーヘッドは最小限に抑えられます。つまり、メモリから直接読み取られます。
地元のモキ
アプリケーション構成を使用して外部APIの問題を解決しましたが、依存関係を引数として渡す方が簡単な場合があります。 たとえば、一部の関数は、テストで分離する長い計算を実行します。
defmodule MyModule do def my_function do # ... SomeDependency.heavy_work(arg1, arg2) # ... end end
引数として渡すことにより、依存関係を取り除くことができます。 この場合、匿名関数を渡すだけで十分です。
defmodule MyModule do def my_function(heavy_work \\ &SomeDependency.heavy_work/2) do # ... heavy_work.(arg1, arg2) # ... end end
テストは次のようになります。
test "my function performs heavy work" do # heavy_work = fn(_, _) -> send(self(), :heavy_work) end MyModule.my_function(heavy_work) assert_received :heavy_work end
または、前述のように、契約を定義してモジュール全体を転送できます。
defmodule MyModule do def my_function(dependency \\ SomeDependency) # ... dependency.heavy_work(arg1, arg2) # ... end end
テストを変更します。
test "my function performs heavy work" do # defmodule TestDependency do def heavy_work(_arg1, _arg2) do send self(), :heavy_work end end MyModule.my_function(TestDependency) assert_received :heavy_work end
データ構造の形式で依存関係を表し、 protocolを使用してコントラクトを定義することもできます 。
依存関係を引数として渡すのははるかに簡単なので、可能であれば、このような方法は、構成ファイルとApplication.get_env/3
を使用するよりも望ましいはずです。
モックは名詞です
mokasを名詞と考える方が良いです。 API(wet-verb)を濡らす代わりに、必要なAPIを実装するmok(mok-noun)を作成する必要があります。
mokaを使用した場合のほとんどの問題は、mokaを動詞として使用したときに発生します。 何かを濡らした場合、既存のオブジェクトを変更しますが、これらの変更はグローバルなものです。 たとえば、SomeDependencyモジュールをウェットすると、グローバルに変更されます。
mock(SomeDependency, :heavy_work, to_return: true)
mokaを名詞として使用する場合、何か新しいものを作成する必要があります。もちろん、既存のSomeDependency
モジュールにすることはできません。 「mokは名詞であり、動詞ではない」というルールは、「悪い」mokaを見つけるのに役立ちます。 しかし、あなたの経験は私の経験とは異なるかもしれません。
mokを作成するためのライブラリ
「mokを作成するためにライブラリを放棄する必要がありますか?」
それはすべて状況に依存します。 ライブラリがグローバルオブジェクトの置換(またはモックを動詞として使用)、オブジェクト指向の静的メソッドの変更、または関数型プログラミングのモジュールの置換を促した場合、つまり、上記のモックの作成規則に違反した場合、それを拒否する方がよいでしょう。
ただし、上記のアンチパターンを使用するように促さないmokaを作成するためのライブラリがあります。 このようなライブラリは、「モックオブジェクト」または「モックモジュール」を提供します。これらは引数としてテスト対象のシステムに渡され、モックの呼び出し回数と呼び出された引数に関する情報を収集します。
おわりに
システムをテストするタスクの1つは、コンポーネント間の適切な契約と境界を見つけることです。 明示的な契約がある場合にのみmokaを使用すると、次のことが可能になります。
- 契約はシステムの必要な部分に対してのみ作成されるため、mokaの支配から身を守ります。 前述のように、標準の
URI
およびEnum
モジュールとのやり取りをコントラクトの下で非表示にすることはほとんどありません。 - コンポーネントのサポートを簡素化します。 依存関係に新しい機能を追加するときは、契約を更新する必要があります(Elixirに新しい
@callback
を追加します)。@callback
の無限の成長は、依存性が大きすぎることを示し、より早く問題に対処できます。 - 複雑なコンポーネント間の相互作用が隔離されるため、システムをテスト可能にします。
明示的なコントラクトにより、アプリケーションの依存関係の複雑さを確認できます。 複雑さはすべてのアプリケーションに存在するため、常に可能な限り明確にするようにしてください。