查看源代码 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 不会在任何类型的沙箱中运行。因此,在代码示例中定义的任何模块都将在整个测试套件运行过程中保留。

总结

函数

从模块文档生成测试用例。

从 markdown 文件生成测试用例。

函数

链接到此宏

doctest(module, opts \\ [])

查看源代码 (宏)

从模块文档生成测试用例。

调用 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 中。

链接到此宏

doctest_file(file, opts \\ [])

查看源代码 (自 1.15.0 起) (宏)

从 markdown 文件生成测试用例。

选项

  • :tags - 要应用于所有生成的 doctests 的标签列表。

示例

defmodule ReadmeTest do
  use ExUnit.Case
  doctest_file "README.md"
end

此宏会自动导入到每个 ExUnit.Case 中。