Elixir测试进阶 #

一、Mock技术 #

1.1 使用mox #

添加依赖:

elixir
defp deps do
  [
    {:mox, "~> 1.0", only: :test}
  ]
end

1.2 定义行为 #

elixir
defmodule MyApp.HTTPClient do
  @callback get(String.t()) :: {:ok, String.t()} | {:error, term()}
  @callback post(String.t(), map()) :: {:ok, String.t()} | {:error, term()}
end

1.3 定义Mock #

elixir
defmodule MyApp.HTTPClientMock do
  @behaviour MyApp.HTTPClient

  @impl true
  def get(_url), do: {:ok, "mocked response"}

  @impl true
  def post(_url, _body), do: {:ok, "created"}
end

1.4 配置Mock #

elixir
Mox.defmock(MyApp.HTTPClientMock, for: MyApp.HTTPClient)

Application.put_env(:my_app, :http_client, MyApp.HTTPClientMock)

1.5 使用Mock #

elixir
defmodule MyApp.ServiceTest do
  use ExUnit.Case
  import Mox

  setup :verify_on_exit!

  test "fetches data successfully" do
    expect(MyApp.HTTPClientMock, :get, fn "http://api.example.com/data" ->
      {:ok, ~s({"status": "ok"})}
    end)

    assert {:ok, %{"status" => "ok"}} = MyApp.Service.fetch_data()
  end

  test "handles error" do
    expect(MyApp.HTTPClientMock, :get, fn _url ->
      {:error, :timeout}
    end)

    assert {:error, :timeout} = MyApp.Service.fetch_data()
  end
end

二、异步测试 #

2.1 异步测试配置 #

elixir
defmodule MyApp.AsyncTest do
  use ExUnit.Case, async: true
end

2.2 测试异步函数 #

elixir
defmodule MyApp.TaskTest do
  use ExUnit.Case

  test "async task" do
    task = Task.async(fn -> 1 + 1 end)
    assert Task.await(task) == 2
  end

  test "multiple async tasks" do
    tasks = Enum.map(1..5, fn n ->
      Task.async(fn -> n * 2 end)
    end)

    results = Enum.map(tasks, &Task.await/1)
    assert results == [2, 4, 6, 8, 10]
  end
end

2.3 测试GenServer #

elixir
defmodule MyApp.CounterTest do
  use ExUnit.Case

  setup do
    {:ok, pid} = Counter.start_link(0)
    {:ok, counter: pid}
  end

  test "concurrent increments", %{counter: counter} do
    tasks = Enum.map(1..100, fn _ ->
      Task.async(fn -> Counter.increment(counter) end)
    end)

    Enum.each(tasks, &Task.await/1)
    assert Counter.get(counter) == 100
  end
end

三、集成测试 #

3.1 Phoenix集成测试 #

elixir
defmodule MyAppWeb.UserControllerTest do
  use MyAppWeb.ConnCase

  test "lists users", %{conn: conn} do
    user = user_fixture()

    conn = get(conn, ~p"/users")

    assert html_response(conn, 200) =~ user.name
  end

  test "creates user", %{conn: conn} do
    conn = post(conn, ~p"/users", user: %{name: "Alice", email: "alice@example.com"})

    assert redirected_to(conn) == ~p"/users"
  end
end

3.2 API测试 #

elixir
defmodule MyAppWeb.Api.UserControllerTest do
  use MyAppWeb.ConnCase

  test "lists users as JSON", %{conn: conn} do
    user = user_fixture()

    conn = get(conn, ~p"/api/users")

    assert json_response(conn, 200) == [%{
      "id" => user.id,
      "name" => user.name,
      "email" => user.email
    }]
  end
end

四、LiveView测试 #

4.1 测试LiveView #

elixir
defmodule MyAppWeb.UserLiveTest do
  use MyAppWeb.ConnCase

  import Phoenix.LiveViewTest

  test "displays users", %{conn: conn} do
    user = user_fixture()

    {:ok, view, html} = live(conn, ~p"/users")

    assert html =~ user.name
  end

  test "creates user", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/users/new")

    view
    |> element("form")
    |> render_submit(user: %{name: "Alice", email: "alice@example.com"})

    assert_redirected(view, ~p"/users")
  end
end

五、测试数据库 #

5.1 使用DataCase #

elixir
defmodule MyApp.DataCase do
  use ExUnit.CaseTemplate

  using do
    quote do
      alias MyApp.Repo

      import Ecto
      import Ecto.Changeset
      import Ecto.Query
      import MyApp.DataCase
    end
  end

  setup tags do
    MyApp.DataCase.setup_sandbox(tags)
    :ok
  end

  def setup_sandbox(tags) do
    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
  end
end

5.2 使用DataCase #

elixir
defmodule MyApp.AccountsTest do
  use MyApp.DataCase

  alias MyApp.Accounts

  test "create_user/1 creates a user" do
    assert {:ok, user} = Accounts.create_user(%{name: "Alice"})
    assert user.name == "Alice"
  end
end

六、测试覆盖率 #

6.1 运行覆盖率 #

bash
mix test --cover

6.2 配置覆盖率 #

elixir
def project do
  [
    test_coverage: [tool: ExCoveralls]
  ]
end

七、测试最佳实践 #

7.1 测试命名 #

elixir
test "create_user/1 with valid params returns ok" do
  # ...
end

test "create_user/1 with invalid params returns error" do
  # ...
end

7.2 测试组织 #

elixir
defmodule MyApp.UserTest do
  use ExUnit.Case

  describe "create/1" do
    test "with valid params"
    test "with invalid params"
    test "with duplicate email"
  end

  describe "update/2" do
    test "updates name"
    test "updates email"
  end
end

八、总结 #

本章学习了:

特性 用途
Mox Mock库
async 异步测试
ConnCase HTTP测试
LiveViewTest LiveView测试
DataCase 数据库测试

恭喜你完成了Elixir语言完全指南!

最后更新:2026-03-27