Programming Phoenix勉強その19
Programming Phoenix勉強その19
その19です。ここからChapter12です。今まで作った InfoSys
アプリとかを アンブレラプロジェクトに変更してテストしやすくするみたいです。
rumbrellaプロジェクトの作成と設定
以下のコマンドでアンブレラプロジェクトを新たに生成します。 既存の rumbl
プロジェクトと混じらないように適当な場所で実行します。
$ mix new rumbrella --umbrella
アンブレラプロジェクトが作成されたので、 cd rumbrella/app
に移動して以下のコマンドを実行します。
app $ mix new info_sys
準備が出来たので既存の Rumbl.InfoSys
と Rumbl
を rumbrella
管理下に移植していきます。 まず Rumbl.InfoSys
からやっていきます。以下の流れです。
Rumbl.InfoSys
のモジュール名をInfoSys
に変更して、lib/rumbl/info_sys.ex
をapp
フォルダで作成したinfo_sys/lib/info_sys.ex
となるように移動します。Rumbl.InfoSys.Supervisor
も同じようにリネームしてinfo_sys/lib/info_sys/supervisor.ex
となるように移動します。Rumbl.InfoSys.Wolfram
もsupervisor
と同じようにして同じフォルダに移動します。Rumbl.InfoSys
となっている箇所をすべてInfoSys
に置換します。Wolfram Alpha
のAPIキーを取得する関数を以下のように変更します。
defp app_id, do: Application.get_env(:info_sys, :wolfram)[:app_id]
依存関係を移し替えておきます。
info_sys
のmix.exs
のdeps
に{:sweet_xml, "~> 0.5.0"}
を追加します。
これで InfoSys
の移動は完了です。以下を実行しておきます。
rumbrella $ mix deps.get
rumbrella $ mix test
次は Rumbl
本体の移植を行います。
Rumblプロジェクトのrumbrellaへの移植
Rumbl
プロジェクトを rumbrella
に移植します。
rumbl
ディレクトリをrumbrella/apps
以下に移動します。rumbl
のmix.exs
内のproject
関数にinfo_sys
のmix.exs
と合わせるような感じで以下を追加します。
def project do
[app: :rumbl,
version: "0.0.1",
elixir: "~> 1.2",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
aliases: aliases(),
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
deps: deps()]
end
3. mix.exs
の application
関数に :info_sys
を追加します。 :comeonin
の後に追加する感じです。
deps
の:sweet_xml
を削除して{:info_sys, in_umbrella: true}
を追加します。lib/rumbl.ex
からchildren
として追加していたRumbl.InfoSys
を削除します。video_channel.ex
で使っていたRumbl.InfoSys
をInfoSys
に変更します。dev.secret.exs
のWolframAlpha
のキー部分を:rumbl
から:info_sys
に変更します。
これで準備OKです。
最後に mix deps.get
と mix test
を実行しておきます。
OTPのテスト
ここで終わると短いので、このまま chapter13
に入って OTP
のテストを行います。 自動で生成されている info_sys_test.exs
を以下のように変更します。
defmodule InfoSysTest do
use ExUnit.Case
alias InfoSys.Result
defmodule TestBackend do
def start_link(query, ref, owner, limit) do
Task.start_link(__MODULE__, :fetch, [query, ref, owner, limit])
end
def fetch("result", ref, owner, _limit) do
send(owner, {:results, ref, [%Result{backend: "test", text: "result"}]})
end
def fetch("none", ref, owner, _limit) do
send(owner, {:results, ref, []})
end
def fetch("timeout", _ref, owner, _limit) do
# プロセス監視用にテスト実行元にpidを送る
send(owner, {:backend, self()})
:timer.sleep(:infinity)
end
def fetch("boom", _ref, _owner, _limit) do
raise "boom!"
end
end
test "compute/2 with backend results" do
assert [%Result{backend: "test", text: "result"}] =
InfoSys.compute("result", backends: [TestBackend])
end
test "compute/2 with no backend results" do
assert [] = InfoSys.compute("none", backends: [TestBackend])
end
test "compute/2 with timeout returns no results and kills workers" do
results = InfoSys.compute("timeout", backends: [TestBackend], timeout: 10)
assert results == []
# 上のfetch("timeout", 〜) 関数から送られてくるPID
assert_receive {:backend, backend_pid}
ref = Process.monitor(backend_pid)
assert_receive {:DOWN, ^ref, :process, _pid, _reason}
# receivedはすでに受信ボックスに入っているものを取り出す
# 受信をまったりはしない
refute_received {:DOWN, _, _, _, _}
refute_received :timeout
end
@tag :capture_log
test "compute/2 discards backend errors" do
assert InfoSys.compute("boom", backends: [TestBackend]) == []
refute_received {:DOWN, _, _, _, _}
refute_received :timeout
end
end
wolfram
などのバックエンドAPIの代わりとなるモジュールを内部に定義しています。タイムアウトの処理は
assert_receive
やrefute_received
を使ってタイムアウト時のメッセージを受け取ることで行っています。例外発生時のテストも似たような感じですが、普通にやるとコンソールにエラーメッセージが表示されると出 ``@tag :capture_log
で制御しています。 今までのテストとそう変わったところは無いかと思います。 ============================================ Wolfram APIの分離 ============================================ 現状の
Wolfram APIは
:httpcがハードコーディングされており、こいつを使うことが前提になっています。 これだとテストが難しいのでまずはこの取得先設定を外部ファイルにします。
wolfram.exを変更します。 .. code-block:: Elixir @http Application.get_env(:info_sys, :wolfram)[:http_client] || :httpc defp fetch_xml(query_str) do {:ok, {_, _, body}} = @http.request( String.to_char_list("http://api.wolframalpha.com/v2/query" <> "?appid=#{app_id()}" <> "&input=#{URI.encode_www_form(query_str)}&format=plaintext")) body end 接続に使うモジュールを
@httpと言うかたちで
config系統のファイルに外出しました。 また、
URI.eocode_www_formにしています。 外部ファイルを作ります。
config/text.exsを作ります。 .. code-block:: Elixir use Mix.Config config :info_sys, :wolfram, app_id: "1234", http_client: InfoSys.Test.HTTPClient 環境によって動的に設定ファイルを読み込むように
config.exsの
import_config "#{Mix.env}.exs"のコメントを外しておきます。 テスト以外の環境でも外部ファイルが必要となるので、
use Mix.Configだけ書いた
dev.exsと
prod.exsを作っておきます。 次にテスト用のXMLデータを持ってきます。 `本の公式サイトのソース置き場 <https://pragprog.com/titles/phoenix/source_code>`_ からソースを持ってきて
test/fixturesフォルダに
wolfram.xmlファイルを設置しておきます。
httpのクライアントも作ります。
test/backends/http_client.exsを以下の内容で作ります。 .. code-block:: Elixir defmodule InfoSys.Test.HTTPClient do @wolfram_xml File.read!("test/fixtures/wolfram.xml") def request(url) do url = to_string(url) cond do String.contains?(url, "1+%2B+1") -> {:ok, {[], [], @wolfram_xml}} true -> {:ok, {[], [], "<queryresult></queryresult>"}} end end end
test_helper.exsにこの外部ファイル化したモジュールが読み込まれていることを確認する設定を書きます。 .. code-block:: Elixir Code.require_file "backends/http_client.exs", __DIR__ ExUnit.start()
rumblの方にも似たように書きます。 .. code-block:: Elixir Code.require_file "../../info_sys/test/backends/http_client.exs", __DIR__ ExUnit.start Ecto.Adapters.SQL.Sandbox.mode(Rumbl.Repo, :manual) ここまで来てやっと最後にテストを書きます。
test/backends/woldram_test.exsです。 .. code-block:: Elixir defmodule InfoSys.Backends.WolframTest do use ExUnit.Case, async: true alias InfoSys.Wolfram test "makes request, reports results, them terminates" do ref = make_ref() {:ok, pid} = Wolfram.start_link("1 + 1", ref, self(), 1) Process.monitor(pid) assert_receive {:results, ^ref, [%InfoSys.Result{text: "2"}]} assert_receive {:DOWN, _ref, :process, ^pid, :normal} end test "no query results reports an empty list" do ref = make_ref() {:ok, _} = Wolfram.start_link("none", ref, self(), 1) assert_receive {:results, ^ref, []} end end これで基本的なテストはOKです。 ============================================ まとめ ============================================ -
umbrella`` プロジェクトを使うことでAPI同士の結合を弱めて、テストがやりやすくなる。テストをする際はスタブとなるような構造体などを作ってやると良い