Elixir 测试:ExUnit 框架
Elixir 自带的测试框架叫 ExUnit,它轻量、快速,并与语言深度集成。创建、运行和组织测试都只需几行代码。
创建第一个测试
-
进入你的 Elixir 项目目录(如果没有项目,先用
mix new my_app创建一个)。 -
打开
test/test_helper.exs文件,确认里面有这行:ExUnit.start()这行代码会在运行测试时自动加载 ExUnit。
-
在
test/目录下新建一个文件,比如my_test.exs(注意后缀必须是.exs,不是.ex)。 -
写入以下内容:
defmodule MyTest do use ExUnit.Case test "2 + 2 equals 4" do assert 2 + 2 == 4 end enduse ExUnit.Case让模块获得测试能力。test是定义测试用例的宏,后面跟描述字符串和一个do...end块。assert是核心断言函数:如果表达式为false或nil,测试失败;否则通过。
-
在终端运行:
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
现在为它写测试:
-
创建
test/calculator_test.exs。 -
输入:
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 enddoctest Calculator会自动运行模块文档中的示例(如果你写了@doc并包含iex>示例)。- 每个
test描述清晰,只测一个行为。 - 使用
assert验证返回值是否符合预期。
-
运行
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 避免重复代码。
-
修改
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 endsetup块返回{:ok, map},map 中的键值对会注入到每个测试的上下文(即%{}参数)。- 测试函数多一个参数
%{a: a, b: b}来接收这些值。
-
运行测试,依然通过。
测试异常情况
有时你要验证函数在错误输入下是否抛出异常。
ExUnit 提供 assert_raise。
-
添加一个新测试:
test "calling head on empty list raises Enum.EmptyError" do assert_raise Enum.EmptyError, fn -> Enum.at([], 0) end end- 第一个参数是期望的异常类型。
- 第二个参数是一个匿名函数(
fn -> ... end),里面放可能抛异常的代码。 - 如果函数执行时抛出指定异常,测试通过;否则失败。
组织大型测试套件
随着项目变大,测试文件增多,合理组织很重要。
- 按模块对应:
lib/user.ex→test/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。
-
在
mix.exs的project函数中加入: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 -
运行:
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 是你日常开发中最频繁的操作之一。保持测试快速、独立、可读,就能持续获得可靠反馈。

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