ExUnit
ExUnit 有三個主要的 macros
setup
setup code that runs once before each test
test
單一 isolated test,每次執行 test 前,都會先執行 setup
assert
驗證結果
defmodule MyTest do
use ExUnit.Case, async: true
setup do
# run some tedious setup code
:ok
end
test "pass" do
assert true
end
test "fail" do
assert false
end
end
以 Mix 執行 Phoenix Tests
Phoenix 會產生幾個 test,ex: test/controllers/videocontrollertest.exs,要先修改 /config/test.exs 的 DB 設定,再用 mix test 執行測試
mix test
/test/support/conn_case.ex
defmodule Rumbl.ConnCase do
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest
alias Rumbl.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Rumbl.Router.Helpers
# The default endpoint for testing
@endpoint Rumbl.Endpoint
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, conn: Phoenix.ConnTest.build_conn()}
end
end
注意,他是使用 @endpoint Rumbl.Endpoint
test/controllers/pagecontrollertest.ex
defmodule Rumbl.PageControllerTest do
use Rumbl.ConnCase
test "GET /", %{conn: conn} do
conn = get conn, "/"
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
end
end
這個測試會驗證三個部分
- 檢查 conn 的 response 是否為 200
- 檢查 response content-type 是否為 text/html
- 回傳 response body
如果是 json 就改成 assert %{user_id: user.id} = json_response(conn, 200)
修改剛剛的測試
assert html_response(conn, 200) =~ "Welcome to Rumbl.io"
可得到 pass 結果正確
$ mix test test/controllers/page_controller_test.exs
.
Finished in 0.1 seconds
1 test, 0 failures
Randomized with seed 354626
Integration Tests
Creating Test Data
新增 test/support/test_helpers.ex ,新增 user 及 video
defmodule Rumbl.TestHelpers do
alias Rumbl.Repo
def insert_user(attrs \\ %{}) do
changes = Dict.merge(%{
name: "Some User",
username: "user#{Base.encode16(:crypto.rand_bytes(8))}",
password: "supersecret",
}, attrs)
%Rumbl.User{}
|> Rumbl.User.registration_changeset(changes)
|> Repo.insert!()
end
def insert_video(user, attrs \\ %{}) do
user
|> Ecto.build_assoc(:videos, attrs)
|> Repo.insert!()
end
end
Testing Logged-Out Users
修改 /test/support/conn_case.ex
using do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest
alias Rumbl.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query, only: [from: 1, from: 2]
import Rumbl.Router.Helpers
# 增加 TestHelpers
import Rumbl.TestHelpers
# The default endpoint for testing
@endpoint Rumbl.Endpoint
end
end
新增 /test/controllers/videocontrollertest.ex,因為沒有登入,測試所有的連線都是以 302 為結果
defmodule Rumbl.VideoControllerTest do
use Rumbl.ConnCase
test "requires user authentication on all actions", %{conn: conn} do
Enum.each([
get(conn, video_path(conn, :new)),
get(conn, video_path(conn, :index)),
get(conn, video_path(conn, :show, "123")),
get(conn, video_path(conn, :edit, "123")),
put(conn, video_path(conn, :update, "123", %{})),
post(conn, video_path(conn, :create, %{})),
delete(conn, video_path(conn, :delete, "123")),
], fn conn ->
assert html_response(conn, 302)
assert conn.halted
end)
end
end
Preparing for Logged-In Users
修改 /web/controllers/auth.ex,
使用 cond 檢查多個條件
def call(conn, repo) do
user_id = get_session(conn, :user_id)
cond do
user = conn.assigns[:current_user] ->
conn
user = user_id && repo.get(Rumbl.User, user_id) ->
assign(conn, :current_user, user)
true ->
assign(conn, :current_user, nil)
end
end
舊版本為
# 收到 init 的 repository
def call(conn, repo) do
# 檢查 session 是否有存在 :user_id
user_id = get_session(conn, :user_id)
# 如果有 user_id 且 User DB 有這個 user_id
# 利用 assign 把這個 user 資料存放在 conn.assigns
user = user_id && repo.get(Rumbl.User, user_id)
# 後面可以用 :current_user 取得 User 資料
assign(conn, :current_user, user)
end
Testing Logged-In Users
/test/controllers/videocontrollertest.ex
setup do
user = insert_user(username: "max")
conn = assign(conn(), :current_user, user)
{:ok, conn: conn, user: user}
end
test "lists all user's videos on index", %{conn: conn, user: user} do
user_video = insert_video(user, title: "funny cats")
other_video = insert_video(insert_user(username: "other"), title: "another video")
conn = get conn, video_path(conn, :index)
assert html_response(conn, 200) =~ ~r/Listing videos/
assert String.contains?(conn.resp_body, user_video.title)
refute String.contains?(conn.resp_body, other_video.title)
end
Controlling Duplication with Tagging
因有些測試需要登入,有些不要,setup 要區分這兩種狀況。可使用 ExUnit tags 解決此問題
setup %{conn: conn} = config do
if username = config[:login_as] do
user = insert_user(username: username)
conn = assign(conn, :current_user, user)
{:ok, conn: conn, user: user}
else
:ok
end
end
@tag login_as: "max"
test "lists all user's videos on index", %{conn: conn, user: user} do
user_video = insert_video(user, title: "funny cats")
other_video = insert_video(insert_user(username: "other"), title: "another video")
conn = get conn, video_path(conn, :index)
assert html_response(conn, 200) =~ ~r/Listing videos/
assert String.contains?(conn.resp_body, user_video.title)
refute String.contains?(conn.resp_body, other_video.title)
end
測試
$ mix test test/controllers --only login_as
Compiling 13 files (.ex)
warning: function authenticate/2 is unused
web/controllers/user_controller.ex:40
Including tags: [:login_as]
Excluding tags: [:test]
.
Finished in 0.8 seconds
3 tests, 0 failures, 2 skipped
Randomized with seed 158230
增加 create a video 的測試
alias Rumbl.Video
@valid_attrs %{url: "http://youtu.be", title: "vid", description: "a vid"}
@invalid_attrs %{title: "invalid"}
defp video_count(query), do: Repo.one(from v in query, select: count(v.id))
@tag login_as: "max"
test "creates user video and redirects", %{conn: conn, user: user} do
conn = post conn, video_path(conn, :create), video: @valid_attrs
assert redirected_to(conn) == video_path(conn, :index)
assert Repo.get_by!(Video, @valid_attrs).user_id == user.id
end
@tag login_as: "max"
test "does not create video and renders errors when invalid", %{conn: conn} do
count_before = video_count(Video)
conn = post conn, video_path(conn, :create), video: @invalid_attrs
assert html_response(conn, 302) =~ "redirected"
# assert video_count(Video) == count_before
end
@tag login_as: "max"
test "authorizes actions against access by other users",
%{user: owner, conn: conn} do
video = insert_video(owner, @valid_attrs)
non_owner = insert_user(username: "sneaky")
conn = assign(conn, :current_user, non_owner)
assert_error_sent :not_found, fn ->
get(conn, video_path(conn, :show, video))
end
assert_error_sent :not_found, fn ->
get(conn, video_path(conn, :edit, video))
end
assert_error_sent :not_found, fn ->
put(conn, video_path(conn, :update, video, video: @valid_attrs))
end
assert_error_sent :not_found, fn ->
delete(conn, video_path(conn, :delete, video))
end
end
Unit-Testing Plugs
/test/controllers/auth_test.exs
defmodule Rumbl.AuthTest do
use Rumbl.ConnCase
alias Rumbl.Auth
setup %{conn: conn} do
conn =
conn
|> bypass_through(Rumbl.Router, :browser)
|> get("/")
{:ok, %{conn: conn}}
end
test "authenticate_user halts when no current_user exists",
%{conn: conn} do
conn = Auth.authenticate_user(conn, [])
assert conn.halted
end
test "authenticate_user continues when the current_user exists",
%{conn: conn} do
conn =
conn
|> assign(:current_user, %Rumbl.User{})
|> Auth.authenticate_user([])
refute conn.halted
end
end
測試 login logout
test "login puts the user in the session", %{conn: conn} do
# 新的 connection
login_conn =
conn
|> Auth.login(%Rumbl.User{id: 123})
|> send_resp(:ok, "")
next_conn = get(login_conn, "/")
# 檢查是否在 session 裡面
assert get_session(next_conn, :user_id) == 123
end
test "logout drops the session", %{conn: conn} do
logout_conn =
conn
|> put_session(:user_id, 123)
|> Auth.logout()
|> send_resp(:ok, "")
next_conn = get(logout_conn, "/")
refute get_session(next_conn, :user_id)
end
檢查 assigns 裡面的 current_user
test "call places user from session into assigns", %{conn: conn} do
user = insert_user()
conn =
conn
|> put_session(:user_id, user.id)
|> Auth.call(Repo)
assert conn.assigns.current_user.id == user.id
end
test "call with no session sets current_user assign to nil", %{conn: conn} do
conn = Auth.call(conn, Repo)
assert conn.assigns.current_user == nil
end
test "login with a valid username and pass", %{conn: conn} do
user = insert_user(username: "me", password: "secret")
{:ok, conn} =
Auth.login_by_username_and_pass(conn, "me", "secret", repo: Repo)
assert conn.assigns.current_user.id == user.id
end
test "login with a not found user", %{conn: conn} do
assert {:error, :not_found, _conn} =
Auth.login_by_username_and_pass(conn, "me", "secret", repo: Repo)
end
test "login with password mismatch", %{conn: conn} do
_ = insert_user(username: "me", password: "secret")
assert {:error, :unauthorized, _conn} =
Auth.login_by_username_and_pass(conn, "me", "wrong", repo: Repo)
end
Testing Views and Templates
defmodule Rumbl.VideoViewTest do
use Rumbl.ConnCase, async: true
import Phoenix.View
test "renders index.html", %{conn: conn} do
# 以 videos render 頁面
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 及 categories assigns 去 render 頁面
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
Spliting Side Effects in Model Tests
Testing Side Effect-Free Model Code
在 test/support/modelcase.ex 中 import TestHelpers,修改 errorson function
using do
quote do
alias Rumbl.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query, only: [from: 1, from: 2]
import Rumbl.TestHelpers
import Rumbl.ModelCase
end
end
def errors_on(model, data) do
model.__struct__.changeset(model, data).errors
end
新增 /test/models/user_test.exs
defmodule Rumbl.UserTest do
# 設定為 async,因為目標是要區隔每個 test,可平行處理
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?
assert changeset.valid?
end
test "changeset does not accept long usernames" do
attrs = Map.put(@valid_attrs, :username, String.duplicate("a", 30))
# 使用 ModelCase 定義的 error_on function,快速取得 changeset 裡面的 errors
# assert {:username, {"should be at most %{count} character(s)", [count: 20]}} in
# errors_on(%User{}, attrs)
assert [username: {"should be at most %{count} character(s)", [count: 20, validation: :length, max: 20]}] ==
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}}
# in changeset.errors
assert [password: {"should be at least %{count} character(s)",
[count: 6, validation: :length, min: 6]}]
== 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
Testing Code with Side Effect
新增 /test/models/userrepotest.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)
# assert {:username, "has already been taken"} in changeset.errors
assert [username: {"has already been taken", []}] == changeset.errors
end
end
$ mix test test/models/user_repo_test.exs
.
Finished in 0.4 seconds
1 test, 0 failures
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 ~w(a b c) == Repo.all(query)
assert ~w(a Action b c Comedy Drama Romance Sci-fi) == Repo.all(query)
end
end
$ mix test test/models/category_repo_test.exs
.
Finished in 0.08 seconds
1 test, 0 failures
沒有留言:
張貼留言