测试与部署 #
一、测试概述 #
1.1 测试类型 #
| 类型 | 说明 |
|---|---|
| 单元测试 | 测试单个函数或模块 |
| 集成测试 | 测试多个组件协作 |
| 控制器测试 | 测试HTTP请求 |
| LiveView测试 | 测试实时视图 |
1.2 测试目录结构 #
text
test/
├── hello/ # 业务逻辑测试
│ └── accounts_test.exs
├── hello_web/ # Web层测试
│ ├── controllers/
│ └── live/
├── support/ # 测试辅助
│ ├── fixtures/
│ ├── conn_case.ex
│ └── data_case.ex
└── test_helper.exs
二、单元测试 #
2.1 Context测试 #
elixir
defmodule Hello.AccountsTest do
use Hello.DataCase
alias Hello.Accounts
describe "users" do
alias Hello.Accounts.User
test "list_users/0 returns all users" do
user = user_fixture()
assert Accounts.list_users() == [user]
end
test "get_user!/1 returns the user with given id" do
user = user_fixture()
assert Accounts.get_user!(user.id) == user
end
test "create_user/1 with valid data creates a user" do
attrs = %{email: "test@example.com", name: "Test User"}
assert {:ok, %User{} = user} = Accounts.create_user(attrs)
assert user.email == "test@example.com"
assert user.name == "Test User"
end
test "create_user/1 with invalid data returns error changeset" do
attrs = %{email: nil, name: nil}
assert {:error, %Ecto.Changeset{}} = Accounts.create_user(attrs)
end
test "update_user/2 with valid data updates the user" do
user = user_fixture()
attrs = %{name: "Updated Name"}
assert {:ok, %User{} = user} = Accounts.update_user(user, attrs)
assert user.name == "Updated Name"
end
test "delete_user/1 deletes the user" do
user = user_fixture()
assert {:ok, %User{}} = Accounts.delete_user(user)
assert_raise Ecto.NoResultsError, fn -> Accounts.get_user!(user.id) end
end
end
defp user_fixture(attrs \\ %{}) do
attrs = Enum.into(attrs, %{email: "test@example.com", name: "Test User"})
{:ok, user} = Accounts.create_user(attrs)
user
end
end
2.2 测试辅助 #
elixir
defmodule Hello.DataCase do
use ExUnit.CaseTemplate
using do
quote do
alias Hello.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Hello.DataCase
end
end
setup tags do
Hello.DataCase.setup_sandbox(tags)
:ok
end
def setup_sandbox(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Hello.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
end
三、控制器测试 #
3.1 控制器测试 #
elixir
defmodule HelloWeb.UserControllerTest do
use HelloWeb.ConnCase
describe "index" do
test "lists all users", %{conn: conn} do
user = user_fixture()
conn = get(conn, ~p"/users")
assert html_response(conn, 200) =~ user.name
end
end
describe "show" do
test "shows user", %{conn: conn} do
user = user_fixture()
conn = get(conn, ~p"/users/#{user.id}")
assert html_response(conn, 200) =~ user.name
end
end
describe "create" do
test "redirects when data is valid", %{conn: conn} do
attrs = %{email: "test@example.com", name: "Test User"}
conn = post(conn, ~p"/users", user: attrs)
assert redirected_to(conn) =~ ~p"/users/"
end
test "renders errors when data is invalid", %{conn: conn} do
attrs = %{email: nil, name: nil}
conn = post(conn, ~p"/users", user: attrs)
assert html_response(conn, 200) =~ "can't be blank"
end
end
end
3.2 ConnCase #
elixir
defmodule HelloWeb.ConnCase do
use ExUnit.CaseTemplate
using do
quote do
use HelloWeb, :verified_routes
import Plug.Conn
import Phoenix.ConnTest
import HelloWeb.ConnCase
end
end
setup tags do
Hello.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
四、LiveView测试 #
4.1 LiveView测试 #
elixir
defmodule HelloWeb.UserLiveTest do
use HelloWeb.ConnCase
import Phoenix.LiveViewTest
describe "Index" do
test "lists all users", %{conn: conn} do
user = user_fixture()
{:ok, _index_live, html} = live(conn, ~p"/users")
assert html =~ user.name
end
test "saves new user", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/users")
assert index_live |> element("a", "New User") |> render_click() =~
"New User"
assert_patch(index_live, ~p"/users/new")
assert index_live
|> form("#user-form", user: %{email: "test@example.com", name: "Test"})
|> render_submit()
assert_patch(index_live, ~p"/users")
html = render(index_live)
assert html =~ "Test"
end
test "updates user", %{conn: conn} do
user = user_fixture()
{:ok, index_live, _html} = live(conn, ~p"/users")
assert index_live |> element("#user-#{user.id} a", "Edit") |> render_click() =~
"Edit User"
assert_patch(index_live, ~p"/users/#{user.id}/edit")
assert index_live
|> form("#user-form", user: %{name: "Updated"})
|> render_submit()
assert_patch(index_live, ~p"/users")
html = render(index_live)
assert html =~ "Updated"
end
test "deletes user", %{conn: conn} do
user = user_fixture()
{:ok, index_live, _html} = live(conn, ~p"/users")
assert index_live |> element("#user-#{user.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#user-#{user.id}")
end
end
end
五、运行测试 #
5.1 测试命令 #
bash
mix test
mix test --trace
mix test --only tag
mix test --exclude tag
mix test --stale
mix test --cover
5.2 测试配置 #
elixir
config :hello, Hello.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "hello_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
六、生产部署 #
6.1 创建Release #
bash
mix phx.gen.release
MIX_ENV=prod mix release
6.2 Release配置 #
elixir
config :hello, HelloWeb.Endpoint,
url: [host: System.get_env("HOST") || "example.com", port: 443, scheme: "https"],
http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
secret_key_base: System.get_env("SECRET_KEY_BASE"),
server: true
6.3 运行Release #
bash
_build/prod/rel/hello/bin/hello start
_build/prod/rel/hello/bin/hello stop
_build/prod/rel/hello/bin/hello restart
_build/prod/rel/hello/bin/hello eval "Hello.Release.migrate()"
七、Docker部署 #
7.1 Dockerfile #
dockerfile
FROM elixir:1.15-alpine AS build
WORKDIR /app
RUN apk add --no-cache build-base git
RUN mix local.hex --force && \
mix local.rebar --force
COPY mix.exs mix.lock ./
RUN mix deps.get --only prod
COPY lib lib
COPY priv priv
COPY assets assets
RUN mix assets.deploy
RUN mix compile
RUN mix release
FROM alpine:3.18
WORKDIR /app
RUN apk add --no-cache openssl ncurses-libs
COPY --from=build /app/_build/prod/rel/hello ./
ENV HOME=/app
CMD ["bin/hello", "start"]
7.2 docker-compose.yml #
yaml
version: '3.8'
services:
web:
build: .
ports:
- "4000:4000"
environment:
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
- DATABASE_URL=${DATABASE_URL}
- HOST=localhost
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=hello_prod
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
八、环境变量 #
8.1 必需的环境变量 #
bash
export SECRET_KEY_BASE="your_secret_key_base"
export DATABASE_URL="ecto://user:pass@host/database"
export HOST="example.com"
export PORT="4000"
8.2 生成Secret #
bash
mix phx.gen.secret
九、数据库迁移 #
9.1 生产迁移 #
elixir
defmodule Hello.Release do
@app :hello
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.load(@app)
end
end
9.2 运行迁移 #
bash
_build/prod/rel/hello/bin/hello eval "Hello.Release.migrate()"
十、监控 #
10.1 日志配置 #
elixir
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
10.2 健康检查 #
elixir
defmodule HelloWeb.HealthController do
use HelloWeb, :controller
def check(conn, _params) do
json(conn, %{status: "ok", timestamp: DateTime.utc_now()})
end
end
十一、总结 #
11.1 测试核心 #
| 类型 | 说明 |
|---|---|
| 单元测试 | 测试业务逻辑 |
| 控制器测试 | 测试HTTP请求 |
| LiveView测试 | 测试实时视图 |
11.2 部署核心 #
| 步骤 | 说明 |
|---|---|
| 编译资源 | mix assets.deploy |
| 创建Release | mix release |
| 运行迁移 | Release.migrate() |
| 启动服务 | bin/hello start |
11.3 完成 #
恭喜你完成了Phoenix完全指南的学习!现在你已经掌握了从基础到进阶的Phoenix开发技能,可以开始构建自己的Phoenix应用了!
最后更新:2026-03-28