查看源代码 ExUnit.DocTest (ExUnit v1.16.2)
从文档中提取测试用例。
Doctests 允许我们从在 @moduledoc
和 @doc
属性中找到的代码示例中生成测试。为此,请在测试用例中调用 doctest/1
宏,并确保您的代码示例是按照以下语法和指南编写的。
语法
每个新测试都从新行开始,并以 iex>
前缀开头。多行表达式可以通过在后续行之前添加 ...>
(推荐)或 iex>
来使用。
预期结果应该从 iex>
和 ...>
行之后的行开始,并以换行符结束。
示例
要运行 doctests,请将它们包含在使用 doctest
宏的 ExUnit 测试用例中
defmodule MyModuleTest do
use ExUnit.Case, async: true
doctest MyModule
end
doctest
宏循环遍历 MyModule
中定义的所有函数和宏,解析其文档以搜索代码示例。
一个非常基本的例子是
iex> 1 + 1
2
也支持多行表达式
iex> Enum.map([1, 2, 3], fn x ->
...> x * 2
...> end)
[2, 4, 6]
可以在同一个测试中检查多个结果
iex> a = 1
1
iex> a + 1
2
如果你想将两个测试分开,请在它们之间添加一个空行
iex> a = 1
1
iex> a + 1 # will fail with a `undefined variable "a"` error
2
如果你不想在 doctest 中断言每个结果,你可以省略结果。你可以在表达式之间这样做
iex> pid = spawn(fn -> :ok end)
iex> is_pid(pid)
true
以及在结尾
iex> Mod.do_a_call_that_should_not_raise!(...)
当结果是可变的(例如上面的 PID)或结果是一个复杂的数据结构,并且你不想显示它全部,而只想显示部分或某些属性时,这很有用。
与 IEx 类似,你可以在你的“提示”中使用数字
iex(1)> [1 + 2,
...(1)> 3]
[3, 3]
这在两种情况下很有用
- 能够引用特定的编号场景
- 从实际的 IEx 会话中复制粘贴示例
你也可以在调用 doctest
时选择或跳过函数。有关更多信息,请参阅下面关于 :except
和 :only
选项的文档。
不透明类型
某些类型的内部结构保持隐藏,并在检查时显示用户友好的结构。Elixir 中的惯用语是将这些数据类型打印为 #Name<...>
格式。由于这些值由于前导的 #
符号而被视为 Elixir 代码中的注释,因此在 doctests 中使用它们时需要特别注意。
假设你有一个包含 DateTime
的映射,并打印为
%{datetime: #DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo>}
如果你尝试匹配此类表达式,doctest
将无法编译。有两种方法可以解决这个问题。
第一种是依靠这样一个事实,即只要它们位于根部,doctest 就可以比较内部结构。因此,可以写
iex> map = %{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")}
iex> map.datetime
#DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo>
每当 doctest 以 "#Name<" 开头时,doctest
将执行字符串比较。例如,上面的测试将执行以下匹配
inspect(map.datetime) == "#DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo>"
或者,由于 doctest 结果实际上是经过计算的,你可以将 DateTime
构建表达式作为 doctest 结果
iex> %{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")}
%{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")}
这种方法的缺点是,doctest 结果不是用户在终端中看到的实际结果。
异常
你也可以展示引发异常的表达式,例如
iex(1)> raise "some error"
** (RuntimeError) some error
Doctest 将查找以 ** (
开头的行,并根据它进行解析以提取异常名称和消息。异常解析器将考虑所有以下行作为异常消息的一部分,直到出现空行或出现以 iex>
开头的新的表达式。因此,只要异常消息本身没有空行,就可以匹配多行消息。
何时不使用 doctest
通常,当你的代码示例包含副作用时,不建议使用 doctests。例如,如果 doctest 打印到标准输出,doctest 将不会尝试捕获输出。
同样,doctests 不会在任何类型的沙箱中运行。因此,在代码示例中定义的任何模块都将在整个测试套件运行过程中保留。
总结
函数
从模块文档生成测试用例。
调用 doctest(Module)
将为 module
中找到的所有 doctests 生成测试。
选项
:except
- 为除列出的所有函数之外的所有函数生成测试({function, arity}
元组列表,以及/或:moduledoc
)。:only
- 仅为列出的函数生成测试({function, arity}
元组列表,以及/或:moduledoc
)。:import
- 当true
时,可以测试在模块中定义的函数,而无需引用模块名称。但是,当与Kernel
等模块发生冲突时,这是不可行的。在这些情况下,:import
应设置为false
,而应使用Module.function(...)
。:tags
- 要应用于所有生成的 doctests 的标签列表。
示例
defmodule MyModuleTest do
use ExUnit.Case
doctest MyModule, except: [:moduledoc, trick_fun: 1]
end
此宏会自动导入到每个 ExUnit.Case
中。
从 markdown 文件生成测试用例。
选项
:tags
- 要应用于所有生成的 doctests 的标签列表。
示例
defmodule ReadmeTest do
use ExUnit.Case
doctest_file "README.md"
end
此宏会自动导入到每个 ExUnit.Case
中。