モキず明瀺的な契玄

おそらくナニットテストず統合テストを曞き始めた誰もが、脆匱なテストに぀ながるモカミの乱甚の問題に盎面しおいたした。 埌者は、テストが䜜業にのみ干枉するずいう誀った信念をプログラマに䞎えたす。


以䞋は、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モックを䜿甚する必芁がありたす。


  1. HTTPClientの倉曎により、 MyApp.Twitter.HTTPテストがMyApp.Twitter.HTTPするMyApp.Twitter.HTTP
  2. 濡れない泚意この堎合、モックは動詞です HTTPClient 。 代わりに、蚭定ファむルを介しお䟝存関係ずしお枡したす。これは、Twitter APIに察しお行ったのず同様です。
  3. 運甚環境に展開する前に、クラむアントの䜜業をテストする方法が必芁です。

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を䜿甚するず、次のこずが可胜になりたす。


  1. 契玄はシステムの必芁な郚分に察しおのみ䜜成されるため、mokaの支配から身を守りたす。 前述のように、暙準のURIおよびEnumモゞュヌルずのやり取りをコントラクトの䞋で非衚瀺にするこずはほずんどありたせん。
  2. コンポヌネントのサポヌトを簡玠化したす。 䟝存関係に新しい機胜を远加するずきは、契玄を曎新する必芁がありたすElixirに新しい@callbackを远加したす。 @callbackの無限の成長は、䟝存性が倧きすぎるこずを瀺し、より早く問題に察凊できたす。
  3. 耇雑なコンポヌネント間の盞互䜜甚が隔離されるため、システムをテスト可胜にしたす。

明瀺的なコントラクトにより、アプリケヌションの䟝存関係の耇雑さを確認できたす。 耇雑さはすべおのアプリケヌションに存圚するため、垞に可胜な限り明確にするようにしおください。



Source: https://habr.com/ru/post/J338066/


All Articles