ãããããŠããããã¹ããšçµ±åãã¹ããæžãå§ãã誰ãããè匱ãªãã¹ãã«ã€ãªããã¢ã«ãã®ä¹±çšã®åé¡ã«çŽé¢ããŠããŸããã åŸè
ã¯ããã¹ããäœæ¥ã«ã®ã¿å¹²æžãããšãã誀ã£ã信念ãããã°ã©ãã«äžããŸãã
以äžã¯ã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.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
ããŸããŸãªç°å¢ã«é©ããèšå®ïŒ
# 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
åŒæ°ãšããŠæž¡ãããšã«ãããäŸåé¢ä¿ãåãé€ãããšãã§ããŸãã ãã®å Žåãå¿å颿°ãæž¡ãã ãã§ååã§ãã
defmodule MyModule do def my_function(heavy_work \\ &SomeDependency.heavy_work/2) do
ãã¹ãã¯æ¬¡ã®ããã«ãªããŸãã
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)
ãã¹ãã倿ŽããŸãã
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
ã®ç¡éã®æé·ã¯ãäŸåæ§ã倧ããããããšã瀺ããããæ©ãåé¡ã«å¯ŸåŠã§ããŸãã - è€éãªã³ã³ããŒãã³ãéã®çžäºäœçšãéé¢ããããããã·ã¹ãã ããã¹ãå¯èœã«ããŸãã
æç€ºçãªã³ã³ãã©ã¯ãã«ãããã¢ããªã±ãŒã·ã§ã³ã®äŸåé¢ä¿ã®è€éãã確èªã§ããŸãã è€éãã¯ãã¹ãŠã®ã¢ããªã±ãŒã·ã§ã³ã«ååšãããããåžžã«å¯èœãªéãæç¢ºã«ããããã«ããŠãã ããã