Programming Phoenix勉強その12
Programming Phoenix勉強その12
その12です。テストの続きです。ビュー周りのテストからです。
テンプレートのテスト
何個か前の章でやったようにテンプレートのレンダリングも単なる関数なので簡単にテスト可能です。
defmodule Rumbl.VideoViewTest do
use Rumbl.ConnCase, async: true
import Phoenix.View
test "renders index.html", %{conn: conn} do
videos = [%Rumbl.Video{id: "1", title: "dogs"},
%Rumbl.Video{id: "2", title: "cats"}]
# テンプレートを文字列としてレンダリングする
content = render_to_string(Rumbl.VideoView, "index.html",
conn: conn, videos: videos)
assert String.contains?(content, "Listing videos")
# 内包表記は中の式は評価される
for video <- videos do
assert String.contains?(content, video.title)
end
end
test "renders new.html", %{conn: conn} do
changeset = Rumbl.Video.changeset(%Rumbl.Video{})
categories = [{"cats", 123}]
content = render_to_string(Rumbl.VideoView, "new.html",
conn: conn, changeset: changeset, categories: categories)
assert String.contains?(content, "New video")
end
end
注目すべきは render_to_string
関数でテンプレートを文字列としてレンダリングしている点かと思います。 実際にレンダリングをHTMLとして行わなくてもテストが出来ています。 render_to_string
関数のオプションにテンプレート側で使う変数を割り当てられるようです。
モデルのテスト(非同期)
モデルのテストを行う前に model_case.ex
を確認しておきます。ついでに import Rumbl.TestHelpers
を追記もしておきます。
defmodule Rumbl.ModelCase do
use ExUnit.CaseTemplate
using do
quote do
alias Rumbl.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Rumbl.TestHelpers
import Rumbl.ModelCase
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Rumbl.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Rumbl.Repo, {:shared, self()})
end
:ok
end
def errors_on(struct, data) do
struct.__struct__.changeset(struct, data)
|> Ecto.Changeset.traverse_errors(&Rumbl.ErrorHelpers.translate_error/1)
|> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
end
end
書籍と比べて error_on
関数が変更されてますが余りきにしなくて良さそうです。ぱっとみエラーメッセージをマップに変更しているだけに見えます。
model/user_test.exs
を作成し以下のように実装します。
defmodule Rumbl.UserTest do
use Rumbl.ModelCase, async: true
alias Rumbl.User
@valid_attrs %{name: "A User", username: "eva", password: "secret"}
@invalid_attrs %{}
test "changeset with valid attributes" do
changeset = User.changeset(%User{}, @valid_attrs)
assert changeset.valid?
end
test "changeset with invalid attributes" do
changeset = User.changeset(%User{}, @invalid_attrs)
refute changeset.valid?
end
test "changeset does not accepts long usernames" do
attrs = Map.put(@valid_attrs, :username, String.duplicate("a", 30))
assert {:username, "should be at most 20 character(s)"} in
errors_on(%User{}, attrs)
end
test "registration_changeset password must be at least 6 chars long" do
attrs = Map.put(@valid_attrs, :password, "12345")
changeset = User.registration_changeset(%User{}, attrs)
assert {:password, {"should be at least %{count} character(s)", [count: 6, validation: :length, min: 6]}}
in changeset.errors
end
test "registration_changeset with valid attributes hashes password" do
attrs = Map.put(@valid_attrs, :password, "123456")
changeset = User.registration_changeset(%User{}, attrs)
%{password: pass, password_hash: pass_hash} = changeset.changes
assert changeset.valid?
assert pass_hash
assert Comeonin.Bcrypt.checkpw(pass, pass_hash)
end
end
erros_on
を使っている場所は ここを参考 にしました。 これらのテストは副作用を起こさないテストでまとめたため、 async: true
にして並列実行しているようです。
副作用のあるテスト
副作用が無く非同期に実行できるテストに対して、実際に Repo.insert
したりするようなテストは副作用が発生します。その為、同じモデルのテストでも副作用あり/無しで分離してテストを書きます。
model/user_repo_test.exs
を以下のように作成します。
defmodule Rumbl.UserRepoTest do
use Rumbl.ModelCase
alias Rumbl.User
@valid_attrs %{name: "A User", username: "eva"}
test "converts unique_constraint on username to error" do
insert_user(username: "eric")
attrs = Map.put(@valid_attrs, :username, "eric")
changeset = User.changeset(%User{}, attrs)
assert {:error, changeset} = Repo.insert(changeset)
# changeset.errorsはキーワードリストになっている
# キーワードリストの各要素は最初の値がアトムとなるタプルとしても認識される
assert {:username, {"has already been taken", []}} in changeset.errors
end
end
実際にインサートを行っている以外には大した違いが無いです。 async
オプションはデフォルトで false
なので指定をしていません。 関係ないですが、キーワードリストについて忘れてて若干ハマりました・・・
同様に category_repo_test.exs
を以下のように作ります。
defmodule Rumbl.CategoryRepoTest do
use Rumbl.ModelCase
alias Rumbl.Category
test "alphabetical/1 orders by name" do
Repo.insert!(%Category{name: "c"})
Repo.insert!(%Category{name: "a"})
Repo.insert!(%Category{name: "b"})
query = Category |> Category.alphabetical()
query = from c in query, select: c.name
assert Repo.all(query) == ~w(a b c)
end
end
別段躓くところはなかったです。
まとめ
ビューは単なる関数なので
render_to_string
などを使って簡単にテストができる。副作用が無いテストを分離することで非同期にテストを実行できる。
NUnit
とか使ってうっかり先に書いたテストに依存するようなテストを書いちゃうことは結構ありましたが、今回のように改めてテストの同期/非同期を意識したのは新鮮でした。 書籍の区分け的にはここで一段落です。以降からパート2に入ります。 Channel
とかは目玉昨日の一つだと思うのでやっていきます。