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 endtest_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同士の結合を弱めて、テストがやりやすくなる。テストをする際はスタブとなるような構造体などを作ってやると良い