文章目录

Elixir 测试:ExUnit 框架

发布于 2026-04-02 04:57:28 · 浏览 11 次 · 评论 0 条

Elixir 测试:ExUnit 框架

Elixir 自带的测试框架叫 ExUnit,它轻量、快速,并与语言深度集成。创建运行组织测试都只需几行代码。


创建第一个测试

  1. 进入你的 Elixir 项目目录(如果没有项目,先用 mix new my_app 创建一个)。

  2. 打开 test/test_helper.exs 文件,确认里面有这行:

    ExUnit.start()

    这行代码会在运行测试时自动加载 ExUnit。

  3. test/ 目录下新建一个文件,比如 my_test.exs(注意后缀必须是 .exs,不是 .ex)。

  4. 写入以下内容:

    defmodule MyTest do
      use ExUnit.Case
    
      test "2 + 2 equals 4" do
        assert 2 + 2 == 4
      end
    end
    • use ExUnit.Case 让模块获得测试能力。
    • test 是定义测试用例的宏,后面跟描述字符串和一个 do...end 块。
    • assert 是核心断言函数:如果表达式为 falsenil,测试失败;否则通过。
  5. 在终端运行

    mix test

    你会看到类似输出:

    .
    
    Finished in 0.02 seconds
    1 doctest, 1 test, 0 failures

编写更实用的测试

假设你有一个模块 Calculator,放在 lib/calculator.ex

defmodule Calculator do
  def add(a, b), do: a + b
  def divide(a, b) when b != 0, do: a / b
  def divide(_a, 0), do: {:error, "Division by zero"}
end

现在为它写测试:

  1. 创建 test/calculator_test.exs

  2. 输入

    defmodule CalculatorTest do
      use ExUnit.Case
      doctest Calculator
    
      test "add/2 returns the sum of two numbers" do
        assert Calculator.add(3, 5) == 8
      end
    
      test "divide/2 returns a float when divisor is not zero" do
        assert Calculator.divide(10, 2) == 5.0
      end
    
      test "divide/2 returns error tuple when divisor is zero" do
        assert Calculator.divide(10, 0) == {:error, "Division by zero"}
      end
    end
    • doctest Calculator 会自动运行模块文档中的示例(如果你写了 @doc 并包含 iex> 示例)。
    • 每个 test 描述清晰,只测一个行为。
    • 使用 assert 验证返回值是否符合预期。
  3. 运行 mix test test/calculator_test.exs 只跑这个文件,或 mix test 跑全部。


处理失败与调试

如果断言失败,ExUnit 会清晰告诉你哪里错了。

例如,把上面的加法测试改成:

test "add/2 returns the sum of two numbers" do
  assert Calculator.add(3, 5) == 9
end

运行 mix test 会得到:

1) test add/2 returns the sum of two numbers (CalculatorTest)
   test/calculator_test.exs:5
   Assertion with == failed
   code:  assert Calculator.add(3, 5) == 9
   left:  8
   right: 9
  • left 是实际结果,right 是期望结果。
  • 无需额外工具,错误信息已足够定位问题。

设置测试前置条件(setup)

当多个测试需要相同数据或状态时,用 setup 避免重复代码。

  1. 修改 calculator_test.exs,加入 setup:

    defmodule CalculatorTest do
      use ExUnit.Case
    
      setup do
        {:ok, a: 10, b: 2}
      end
    
      test "add/2 works with setup values", %{a: a, b: b} do
        assert Calculator.add(a, b) == 12
      end
    
      test "divide/2 works with setup values", %{a: a, b: b} do
        assert Calculator.divide(a, b) == 5.0
      end
    end
    • setup 块返回 {:ok, map},map 中的键值对会注入到每个测试的上下文(即 %{} 参数)。
    • 测试函数多一个参数 %{a: a, b: b} 来接收这些值。
  2. 运行测试,依然通过。


测试异常情况

有时你要验证函数在错误输入下是否抛出异常。

ExUnit 提供 assert_raise

  1. 添加一个新测试:

    test "calling head on empty list raises Enum.EmptyError" do
      assert_raise Enum.EmptyError, fn ->
        Enum.at([], 0)
      end
    end
    • 第一个参数是期望的异常类型。
    • 第二个参数是一个匿名函数(fn -> ... end),里面放可能抛异常的代码。
    • 如果函数执行时抛出指定异常,测试通过;否则失败。

组织大型测试套件

随着项目变大,测试文件增多,合理组织很重要。

  • 按模块对应lib/user.extest/user_test.exs
  • 使用 describe 分组:把相关测试包在一起,提高可读性。

例如:

defmodule UserServiceTest do
  use ExUnit.Case

  describe "create/1" do
    test "returns {:ok, user} when email is valid" do
      # ...
    end

    test "returns {:error, reason} when email is invalid" do
      # ...
    end
  end

  describe "delete/1" do
    test "removes user from database" do
      # ...
    end
  end
end

运行 mix test --trace 可以看到带分组名的详细输出。


控制测试执行

  • 只跑某个测试文件mix test test/user_test.exs

  • 只跑某一行的测试mix test test/user_test.exs:15(15 是 test 所在行号)

  • 跳过某个测试:在 test 前加 @tag :skip

    @tag :skip
    test "temporarily disabled test" do
      # ...
    end

    运行时会显示 (skipped)

  • 只跑带特定标签的测试

    @tag :integration
    test "calls external API" do
      # ...
    end

    然后运行 mix test --only integration


测试异步代码

Elixir 天生支持并发,但 ExUnit 默认不并行运行测试,保证隔离性。

如果你的代码涉及进程(如 GenServer),ExUnit 仍能正确测试,因为每个测试在独立进程中运行。

例如测试一个简单 Agent:

defmodule Counter do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -> initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, & &1)
  end

  def increment do
    Agent.update(__MODULE__, &(&1 + 1))
  end
end

测试它:

defmodule CounterTest do
  use ExUnit.Case

  setup do
    Counter.start_link(0)
    :ok
  end

  test "starts at 0" do
    assert Counter.value() == 0
  end

  test "increments correctly" do
    Counter.increment()
    assert Counter.value() == 1
  end
end

注意:setup 启动了 Agent,ExUnit 会在每个测试前重新运行 setup,确保状态干净。


查看测试覆盖率

ExUnit 本身不提供覆盖率,但可搭配 mix test --cover

  1. mix.exsproject 函数中加入:

    def project do
      [
        # 其他配置...
        test_coverage: [tool: ExCoveralls]
      ]
    end

    或者直接用内置 coverage(Elixir 1.10+):

    def project do
      [
        # ...
        preferred_cli_env: [coveralls: :test],
        test_coverage: [tool: :cover]
      ]
    end
  2. 运行

    mix test --cover

    输出会包含覆盖率百分比,并生成 cover/ 目录下的 HTML 报告。


常见断言方法速查

断言方法 用途 示例
assert expr 表达式为真 assert 1 == 1
refute expr 表达式为假 refute 1 == 2
assert_raise Error, func 函数抛指定异常 assert_raise ArgumentError, fn -> ... end
assert_receive msg 接收消息(用于进程测试) assert_receive {:ok, result}
assert_in_delta a, b, delta 两数在误差范围内 assert_in_delta 0.1 + 0.2, 0.3, 0.0001

运行 mix test 是你日常开发中最频繁的操作之一。保持测试快速、独立、可读,就能持续获得可靠反馈。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文