Unit Testing

Testing Base Julia

Julia 正在快速发展,并且拥有一个广泛的测试套件,以验证多个平台上的功能。如果您从源代码构建 Julia,可以使用 make test 运行此测试套件。在二进制安装中,您可以使用 Base.runtests() 运行测试套件。

Base.runtests — Function
Base.runtests(tests=["all"]; ncores=ceil(Int, Sys.CPU_THREADS / 2),
              exit_on_error=false, revise=false, [seed])

运行列在 tests 中的 Julia 单元测试,tests 可以是一个字符串或字符串数组,使用 ncores 个处理器。如果 exit_on_error 为 false,当一个测试失败时,其他文件中的所有剩余测试仍然会被运行;否则,当 exit_on_error == true 时,它们会被丢弃。如果 revise 为 true,则在运行测试之前使用 Revise 包加载对 Base 或标准库的任何修改。如果通过关键字参数提供了种子,则在运行测试的上下文中用于初始化全局随机数生成器;否则,种子将随机选择。

source

Basic Unit Tests

Test 模块提供简单的 单元测试 功能。单元测试是一种通过检查结果是否符合预期来验证代码正确性的方法。它可以帮助确保在您进行更改后代码仍然正常工作,并且可以在开发时用作指定代码完成时应具有的行为的方式。您可能还想查看 adding tests to your Julia Package 的文档。

简单的单元测试可以使用 @test 和 @test_throws 宏进行:

Test.@test — Macro
@test ex
@test f(args...) key=val ...
@test ex broken=true
@test ex skip=true

测试表达式 ex 是否评估为 true。如果在 @testset 内执行,如果是,则返回 Pass Result,如果为 false,则返回 Fail Result,如果无法评估,则返回 Error Result。如果在 @testset 外执行,则抛出异常,而不是返回 Fail 或 Error。

示例

julia> @test true
测试通过

julia> @test [1, 2] + [2, 1] == [3, 3]
测试通过

@test f(args...) key=val... 形式等同于写 @test f(args..., key=val...),这在表达式使用中缀语法进行近似比较时非常有用:

julia> @test π ≈ 3.14 atol=0.01
测试通过

这等同于更丑陋的测试 @test ≈(π, 3.14, atol=0.01)。除非第一个是调用表达式,其余是赋值(k=v),否则提供多个表达式是错误的。

您可以为 key=val 参数使用任何键,除了 broken 和 skip,它们在 @test 的上下文中具有特殊含义:

  • broken=cond 表示一个应该通过但当前在 cond==true 时始终失败的测试。测试表达式 ex 是否评估为 false 或导致异常。如果是,则返回 Broken Result,如果表达式评估为 true,则返回 Error Result。常规的 @test ex 在 cond==false 时被评估。
  • skip=cond 标记一个不应执行但在 cond==true 时应包含在测试摘要报告中的测试,作为 Broken。这对于间歇性失败的测试或尚未实现的功能的测试非常有用。常规的 @test ex 在 cond==false 时被评估。

示例

julia> @test 2 + 2 ≈ 6 atol=1 broken=true
测试已损坏
  表达式: ≈(2 + 2, 6, atol = 1)

julia> @test 2 + 2 ≈ 5 atol=1 broken=false
测试通过

julia> @test 2 + 2 == 5 skip=true
测试已损坏
  跳过: 2 + 2 == 5

julia> @test 2 + 2 == 4 skip=false
测试通过
Julia 1.7

broken 和 skip 关键字参数需要至少 Julia 1.7。

source
Test.@test_throws — Macro
@test_throws exception expr

测试表达式 expr 是否抛出 exception。异常可以指定类型、字符串、正则表达式或在显示的错误消息中出现的字符串列表、匹配函数或值(将通过比较字段进行相等性测试)。请注意,@test_throws 不支持尾随关键字形式。

Julia 1.8

指定除类型或值以外的任何内容作为 exception 的能力需要 Julia v1.8 或更高版本。

示例

julia> @test_throws BoundsError [1, 2, 3][4]
测试通过
      抛出: BoundsError

julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
测试通过
      抛出: DimensionMismatch

julia> @test_throws "Try sqrt(Complex" sqrt(-1)
测试通过
     消息: "DomainError with -1.0:\nsqrt 被调用时使用了负实数参数,但仅在使用复数参数时才会返回复数结果。尝试使用 sqrt(Complex(x))。"

在最后一个示例中,除了匹配单个字符串外,还可以使用以下方式进行匹配:

  • ["Try", "Complex"](字符串列表)
  • r"Try sqrt\([Cc]omplex"(正则表达式)
  • str -> occursin("complex", str)(匹配函数)
source

例如,假设我们想检查我们的新函数 foo(x) 是否按预期工作:

julia> using Test

julia> foo(x) = length(x)^2
foo (generic function with 1 method)

如果条件为真,则返回 Pass:

julia> @test foo("bar") == 9
Test Passed

julia> @test foo("fizz") >= 10
Test Passed

如果条件为假,则返回 Fail 并抛出异常:

julia> @test foo("f") == 20
Test Failed at none:1
  Expression: foo("f") == 20
   Evaluated: 1 == 20

ERROR: There was an error during testing

如果条件无法评估,因为抛出了一个异常,在这种情况下,因为符号没有定义 length,将返回一个 Error 对象并抛出一个异常:

julia> @test foo(:cat) == 1
Error During Test
  Test threw an exception of type MethodError
  Expression: foo(:cat) == 1
  MethodError: no method matching length(::Symbol)
  The function `length` exists, but no method is defined for this combination of argument types.

  Closest candidates are:
    length(::SimpleVector) at essentials.jl:256
    length(::Base.MethodList) at reflection.jl:521
    length(::MethodTable) at reflection.jl:597
    ...
  Stacktrace:
  [...]
ERROR: There was an error during testing

如果我们期望评估一个表达式 应该 抛出一个异常,那么我们可以使用 @test_throws 来检查这一点:

julia> @test_throws MethodError foo(:cat)
Test Passed
      Thrown: MethodError

Working with Test Sets

通常会使用大量测试来确保函数在一系列输入下正常工作。如果测试失败,默认行为是立即抛出异常。然而,通常更可取的是先运行其余的测试,以更好地了解被测试代码中有多少错误。

Note

@testset 在运行其中的测试时会创建一个自己的局部作用域。

@testset 宏可以用来将测试分组为 集合。测试集合中的所有测试都会被运行,并且在测试集合结束时会打印一个摘要。如果任何测试失败,或者由于错误无法评估,测试集合将抛出 TestSetException。

Test.@testset — Macro
@testset [CustomTestSet] [options...] ["描述"] begin test_ex end
@testset [CustomTestSet] [options...] ["描述 $v"] for v in itr test_ex end
@testset [CustomTestSet] [options...] ["描述 $v, $w"] for v in itrv, w in itrw test_ex end
@testset [CustomTestSet] [options...] ["描述"] test_func()
@testset let v = v, w = w; test_ex; end

使用 begin/end 或函数调用

当使用 @testset 时,使用 begin/end 或单个函数调用,宏会启动一个新的测试集来评估给定的表达式。

如果没有给定自定义测试集类型,则默认为创建一个 DefaultTestSet。DefaultTestSet 记录所有结果,如果有任何 Fail 或 Error,则在顶层(非嵌套)测试集的末尾抛出异常,并附上测试结果的摘要。

可以给定任何自定义测试集类型(AbstractTestSet 的子类型),它也将用于任何嵌套的 @testset 调用。给定的选项仅应用于给定的测试集。默认测试集类型接受三个布尔选项:

  • verbose:如果为 true,即使所有嵌套测试集都通过,结果摘要也会显示(默认值为 false)。
  • showtiming:如果为 true,则显示每个显示的测试集的持续时间(默认值为 true)。
  • failfast:如果为 true,任何测试失败或错误将导致测试集和任何子测试集立即返回(默认值为 false)。这也可以通过环境变量 JULIA_TEST_FAILFAST 全局设置。
Julia 1.8

@testset test_func() 至少需要 Julia 1.8。

Julia 1.9

failfast 至少需要 Julia 1.9。

描述字符串接受来自循环索引的插值。如果没有提供描述,则根据变量构造一个。如果提供了函数调用,则将使用其名称。显式描述字符串会覆盖此行为。

默认情况下,@testset 宏将返回测试集对象本身,尽管此行为可以在其他测试集类型中自定义。如果使用了 for 循环,则宏会收集并返回 finish 方法的返回值列表,默认情况下将返回每次迭代中使用的测试集对象列表。

在 @testset 的主体执行之前,会隐式调用 Random.seed!(seed),其中 seed 是全局随机数生成器的当前种子。此外,在主体执行后,全局随机数生成器的状态将恢复到 @testset 之前的状态。这旨在在失败的情况下简化可重现性,并允许无缝重新排列 @testset,而不考虑它们对全局随机数生成器状态的副作用。

示例

julia> @testset "三角恒等式" begin
           θ = 2/3*π
           @test sin(-θ) ≈ -sin(θ)
           @test cos(-θ) ≈ cos(θ)
           @test sin(2θ) ≈ 2*sin(θ)*cos(θ)
           @test cos(2θ) ≈ cos(θ)^2 - sin(θ)^2
       end;
测试摘要:            | 通过  总计  时间
三角恒等式 |    4      4  0.2s

@testset for

当使用 @testset for 时,宏会为提供的循环的每次迭代启动一个新的测试。每个测试集的语义与 begin/end 情况是相同的(就像在每次循环迭代中使用一样)。

@testset let

当使用 @testset let 时,宏会启动一个 透明 测试集,将给定对象作为上下文对象添加到其中的任何失败测试。这在对一个较大对象执行一组相关测试时非常有用,并且希望在任何单个测试失败时打印这个较大对象。透明测试集不会在测试集层次结构中引入额外的嵌套级别,并直接传递给父测试集(上下文对象附加到任何失败的测试中)。

Julia 1.9

@testset let 至少需要 Julia 1.9。

Julia 1.10

从 Julia 1.10 开始支持多个 let 赋值。

示例

julia> @testset let logi = log(im)
           @test imag(logi) == π/2
           @test !iszero(real(logi))
       end
测试失败于 none:3
  表达式: !(iszero(real(logi)))
     上下文: logi = 0.0 + 1.5707963267948966im

错误: 测试期间发生错误

julia> @testset let logi = log(im), op = !iszero
           @test imag(logi) == π/2
           @test op(real(logi))
       end
测试失败于 none:3
  表达式: op(real(logi))
     上下文: logi = 0.0 + 1.5707963267948966im
              op = !iszero

错误: 测试期间发生错误
source

我们可以将 foo(x) 函数的测试放入一个测试集:

julia> @testset "Foo Tests" begin
           @test foo("a")   == 1
           @test foo("ab")  == 4
           @test foo("abc") == 9
       end;
Test Summary: | Pass  Total  Time
Foo Tests     |    3      3  0.0s

测试集也可以嵌套:

julia> @testset "Foo Tests" begin
           @testset "Animals" begin
               @test foo("cat") == 9
               @test foo("dog") == foo("cat")
           end
           @testset "Arrays $i" for i in 1:3
               @test foo(zeros(i)) == i^2
               @test foo(fill(1.0, i)) == i^2
           end
       end;
Test Summary: | Pass  Total  Time
Foo Tests     |    8      8  0.0s

以及调用函数:

julia> f(x) = @test isone(x)
f (generic function with 1 method)

julia> @testset f(1);
Test Summary: | Pass  Total  Time
f             |    1      1  0.0s

这可以用于允许测试集的因式分解,使得通过运行相关函数来运行单个测试集变得更容易。请注意,在函数的情况下,测试集将被赋予被调用函数的名称。如果嵌套测试集没有失败,如这里发生的那样,它将在摘要中被隐藏,除非传递了 verbose=true 选项:

julia> @testset verbose = true "Foo Tests" begin
           @testset "Animals" begin
               @test foo("cat") == 9
               @test foo("dog") == foo("cat")
           end
           @testset "Arrays $i" for i in 1:3
               @test foo(zeros(i)) == i^2
               @test foo(fill(1.0, i)) == i^2
           end
       end;
Test Summary: | Pass  Total  Time
Foo Tests     |    8      8  0.0s
  Animals     |    2      2  0.0s
  Arrays 1    |    2      2  0.0s
  Arrays 2    |    2      2  0.0s
  Arrays 3    |    2      2  0.0s

如果我们确实有测试失败,只有失败测试集的详细信息会被显示:

julia> @testset "Foo Tests" begin
           @testset "Animals" begin
               @testset "Felines" begin
                   @test foo("cat") == 9
               end
               @testset "Canines" begin
                   @test foo("dog") == 9
               end
           end
           @testset "Arrays" begin
               @test foo(zeros(2)) == 4
               @test foo(fill(1.0, 4)) == 15
           end
       end

Arrays: Test Failed
  Expression: foo(fill(1.0, 4)) == 15
   Evaluated: 16 == 15
[...]
Test Summary: | Pass  Fail  Total  Time
Foo Tests     |    3     1      4  0.0s
  Animals     |    2            2  0.0s
  Arrays      |    1     1      2  0.0s
ERROR: Some tests did not pass: 3 passed, 1 failed, 0 errored, 0 broken.

Testing Log Statements

可以使用 @test_logs 宏来测试日志语句,或者使用 TestLogger。

Test.@test_logs — Macro
@test_logs [log_patterns...] [keywords] 表达式

使用 collect_test_logs 收集由 表达式 生成的日志记录列表,检查它们是否与序列 log_patterns 匹配,并返回 表达式 的值。keywords 提供了一些简单的日志记录过滤:min_level 关键字控制将为测试收集的最低日志级别,match_mode 关键字定义将如何进行匹配(默认的 :all 检查所有日志和模式是否逐对匹配;使用 :any 检查模式是否至少在序列中的某个地方匹配一次。)

最有用的日志模式是简单的元组,形式为 (level,message)。可以使用不同数量的元组元素来匹配其他日志元数据,对应于通过 handle_message 函数传递给 AbstractLogger 的参数:(level,message,module,group,id,file,line)。存在的元素将默认使用 == 与日志记录字段逐对匹配,特殊情况是可以使用 Symbol 作为标准日志级别,而模式中的 Regex 将使用 occursin 匹配字符串或 Symbol 字段。

示例

考虑一个记录警告和几个调试消息的函数:

function foo(n)
    @info "Doing foo with n=$n"
    for i=1:n
        @debug "Iteration $i"
    end
    42
end

我们可以使用以下方式测试信息消息:

@test_logs (:info,"Doing foo with n=2") foo(2)

如果我们还想测试调试消息,则需要使用 min_level 关键字启用它们:

using Logging
@test_logs (:info,"Doing foo with n=2") (:debug,"Iteration 1") (:debug,"Iteration 2") min_level=Logging.Debug foo(2)

如果您想测试某些特定消息的生成,同时忽略其余消息,可以将关键字 match_mode=:any 设置为:

using Logging
@test_logs (:info,) (:debug,"Iteration 42") min_level=Logging.Debug match_mode=:any foo(100)

该宏可以与 @test 链接,以测试返回值:

@test (@test_logs (:info,"Doing foo with n=2") foo(2)) == 42

如果您想测试没有警告的情况,可以省略指定日志模式,并相应地设置 min_level:

# 测试当记录器级别为警告时,表达式不记录任何消息:
@test_logs min_level=Logging.Warn @info("Some information") # 通过
@test_logs min_level=Logging.Warn @warn("Some information") # 失败

如果您想测试在 stderr 中没有由 @warn 生成的警告(或错误消息),请参见 @test_nowarn。

source
Test.TestLogger — Type
TestLogger(; min_level=Info, catch_exceptions=false)

创建一个 TestLogger,它在其 logs::Vector{LogRecord} 字段中捕获记录的消息。

设置 min_level 来控制 LogLevel,catch_exceptions 用于决定是否捕获作为日志事件生成一部分抛出的异常,以及 respect_maxlog 用于决定是否遵循使用 maxlog=n 记录消息的约定,其中某个整数 n 最多记录 n 次。

另请参见: LogRecord。

示例

julia> using Test, Logging

julia> f() = @info "Hi" number=5;

julia> test_logger = TestLogger();

julia> with_logger(test_logger) do
           f()
           @info "Bye!"
       end

julia> @test test_logger.logs[1].message == "Hi"
Test Passed

julia> @test test_logger.logs[1].kwargs[:number] == 5
Test Passed

julia> @test test_logger.logs[2].message == "Bye!"
Test Passed
source
Test.LogRecord — Type
LogRecord

存储单个日志事件的结果。字段:

  • level: 日志消息的 LogLevel
  • message: 日志消息的文本内容
  • _module: 日志事件的模块
  • group: 日志组(默认情况下为包含日志事件的文件名)
  • id: 日志事件的 ID
  • file: 包含日志事件的文件
  • line: 日志事件在文件中的行
  • kwargs: 传递给日志事件的任何关键字参数
source

Other Test Macros

由于对浮点值的计算可能不精确,您可以使用 @test a ≈ b 进行近似相等检查(其中 ≈ 是通过键入 \approx 的选项完成来输入的,是 isapprox 函数),或者直接使用 4d61726b646f776e2e436f64652822222c20226973617070726f782229_40726566。

julia> @test 1 ≈ 0.999999999
Test Passed

julia> @test 1 ≈ 0.999999
Test Failed at none:1
  Expression: 1 ≈ 0.999999
   Evaluated: 1 ≈ 0.999999

ERROR: There was an error during testing

您可以通过在 ≈ 比较后设置 isapprox 的 rtol 和 atol 关键字参数来指定相对和绝对容忍度:

julia> @test 1 ≈ 0.999999  rtol=1e-5
Test Passed

请注意,这不是 ≈ 的特定功能,而是 @test 宏的一个通用特性:@test a <op> b key=val 被宏转换为 @test op(a, b, key=val)。然而,这对于 ≈ 测试特别有用。

Test.@inferred — Macro
@inferred [允许类型] f(x)

测试调用表达式 f(x) 是否返回与编译器推断的相同类型的值。这对于检查类型稳定性非常有用。

f(x) 可以是任何调用表达式。如果类型匹配,则返回 f(x) 的结果;如果发现不同类型,则返回 Error Result。

可选地,允许类型 放宽了测试,当 f(x) 的类型与推断类型在 允许类型 的模下匹配,或者返回类型是 允许类型 的子类型时,测试将通过。这在测试返回小联合类型的函数的类型稳定性时非常有用,例如 Union{Nothing, T} 或 Union{Missing, T}。

julia> f(a) = a > 1 ? 1 : 1.0
f (generic function with 1 method)

julia> typeof(f(2))
Int64

julia> @code_warntype f(2)
MethodInstance for f(::Int64)
  from f(a) @ Main none:1
Arguments
  #self#::Core.Const(f)
  a::Int64
Body::UNION{FLOAT64, INT64}
1 ─ %1 = (a > 1)::Bool
└──      goto #3 if not %1
2 ─      return 1
3 ─      return 1.0

julia> @inferred f(2)
错误: 返回类型 Int64 与推断返回类型 Union{Float64, Int64} 不匹配
[...]

julia> @inferred max(1, 2)
2

julia> g(a) = a < 10 ? missing : 1.0
g (generic function with 1 method)

julia> @inferred g(20)
错误: 返回类型 Float64 与推断返回类型 Union{Missing, Float64} 不匹配
[...]

julia> @inferred Missing g(20)
1.0

julia> h(a) = a < 10 ? missing : f(a)
h (generic function with 1 method)

julia> @inferred Missing h(20)
错误: 返回类型 Int64 与推断返回类型 Union{Missing, Float64, Int64} 不匹配
[...]
source
Test.@test_deprecated — Macro
@test_deprecated [pattern] 表达式

当 --depwarn=yes 时,测试 表达式 是否发出弃用警告,并返回 表达式 的值。日志消息字符串将与默认的 r"deprecated"i 的 pattern 进行匹配。

当 --depwarn=no 时,简单地返回执行 表达式 的结果。当 --depwarn=error 时,检查是否抛出 ErrorException。

示例

# 在 julia 0.7 中已弃用
@test_deprecated num2hex(1)

# 返回的值可以通过与 @test 链接进行测试:
@test (@test_deprecated num2hex(1)) == "0000000000000001"
source
Test.@test_warn — Macro
@test_warn msg expr

测试评估 expr 是否产生包含 msg 字符串或与 msg 正则表达式匹配的 stderr 输出。如果 msg 是一个布尔函数,则测试 msg(output) 是否返回 true。如果 msg 是一个元组或数组,则检查错误输出是否包含/匹配 msg 中的每个项。返回评估 expr 的结果。

另请参见 @test_nowarn 以检查错误输出的缺失。

注意:由 @warn 生成的警告不能使用此宏进行测试。请改用 @test_logs。

source
Test.@test_nowarn — Macro
@test_nowarn expr

测试评估 expr 是否产生空的 stderr 输出(没有警告或其他消息)。返回评估 expr 的结果。

注意:无法使用此宏测试 @warn 生成的警告缺失。请改用 @test_logs。

source

Broken Tests

如果一个测试持续失败,可以将其更改为使用 @test_broken 宏。这将标记该测试为 Broken,如果测试继续失败,并在测试成功时通过 Error 提醒用户。

Test.@test_broken — Macro
@test_broken ex
@test_broken f(args...) key=val ...

表示一个应该通过但当前始终失败的测试。测试表达式 ex 是否评估为 false 或导致异常。如果是,则返回 Broken Result,如果表达式评估为 true,则返回 Error Result。这等同于 @test ex broken=true。

@test_broken f(args...) key=val... 的形式与 @test 宏的用法相同。

示例

julia> @test_broken 1 == 2
测试失败
  表达式: 1 == 2

julia> @test_broken 1 == 2 atol=0.1
测试失败
  表达式: ==(1, 2, atol = 0.1)
source

@test_skip 也可用于跳过测试而不进行评估,但在测试集报告中计入跳过的测试。该测试将不会运行,但会给出一个 Broken Result。

Test.@test_skip — Macro
@test_skip ex
@test_skip f(args...) key=val ...

标记一个不应执行但应包含在测试摘要报告中的测试,状态为 Broken。这对于间歇性失败的测试或尚未实现功能的测试非常有用。这相当于 @test ex skip=true。

@test_skip f(args...) key=val... 的形式与 @test 宏的用法相同。

示例

julia> @test_skip 1 == 2
测试已损坏
  跳过: 1 == 2

julia> @test_skip 1 == 2 atol=0.1
测试已损坏
  跳过: ==(1, 2, atol = 0.1)
source

Test result types

Test.Result — Type
Test.Result

所有测试都会生成一个结果对象。这个对象可能会被存储,也可能不会,具体取决于测试是否是测试集的一部分。

source
Test.Pass — Type
Test.Pass <: Test.Result

测试条件为真,即表达式评估为真或抛出了正确的异常。

source
Test.Fail — Type
Test.Fail <: Test.Result

测试条件为假,即表达式的计算结果为假,或者没有抛出正确的异常。

source
Test.Error — Type
Test.Error <: Test.Result

测试条件由于异常无法评估,或者评估为除 Bool 之外的其他内容。在 @test_broken 的情况下,它用于指示发生了意外的 Pass Result。

source
Test.Broken — Type
Test.Broken <: Test.Result

测试条件是预期的(失败的)破损测试结果,或者是通过 @test_skip 明确跳过的。

source

Creating Custom AbstractTestSet Types

包可以通过实现 record 和 finish 方法来创建自己的 AbstractTestSet 子类型。子类型应具有一个接受描述字符串的单参数构造函数,任何选项都作为关键字参数传递。

Test.record — Function
record(ts::AbstractTestSet, res::Result)

将结果记录到测试集。每当包含的 @test 宏完成时,此函数会被 @testset 基础设施调用,并给出测试结果(可能是一个 Error)。如果在测试块内但在 @test 上下文之外抛出异常,也会调用此函数并传入一个 Error。

source
Test.finish — Function
finish(ts::AbstractTestSet)

对给定的测试集进行任何必要的最终处理。这是在测试块执行后由 @testset 基础设施调用的。

自定义的 AbstractTestSet 子类型应该在其父类(如果有的话)上调用 record,以将自己添加到测试结果树中。这可以实现为:

if get_testset_depth() != 0
    # 将此测试集附加到父测试集
    parent_ts = get_testset()
    record(parent_ts, self)
    return self
end
source

Test 负责维护在执行时嵌套测试集的堆栈,但任何结果的累积都是 AbstractTestSet 子类型的责任。您可以通过 get_testset 和 get_testset_depth 方法访问此堆栈。请注意,这些函数未被导出。

Test.get_testset — Function
get_testset()

从任务的本地存储中检索活动测试集。如果没有活动测试集,则使用备用默认测试集。

source

Test 还确保嵌套的 @testset 调用使用与其父级相同的 AbstractTestSet 子类型,除非明确设置。它不会传播测试集的任何属性。选项继承行为可以通过使用 Test 提供的堆栈基础设施由包实现。

定义一个基本的 AbstractTestSet 子类型可能看起来像:

import Test: Test, record, finish
using Test: AbstractTestSet, Result, Pass, Fail, Error
using Test: get_testset_depth, get_testset
struct CustomTestSet <: Test.AbstractTestSet
    description::AbstractString
    foo::Int
    results::Vector
    # constructor takes a description string and options keyword arguments
    CustomTestSet(desc; foo=1) = new(desc, foo, [])
end

record(ts::CustomTestSet, child::AbstractTestSet) = push!(ts.results, child)
record(ts::CustomTestSet, res::Result) = push!(ts.results, res)
function finish(ts::CustomTestSet)
    # just record if we're not the top-level parent
    if get_testset_depth() > 0
        record(get_testset(), ts)
        return ts
    end

    # so the results are printed if we are at the top level
    Test.print_test_results(ts)
    return ts
end

并且使用该测试集看起来像:

@testset CustomTestSet foo=4 "custom testset inner 2" begin
    # this testset should inherit the type, but not the argument.
    @testset "custom testset inner" begin
        @test true
    end
end

为了使用自定义测试集并将记录的结果作为任何外部默认测试集的一部分打印出来,还需要定义 Test.get_test_counts。这可能看起来像这样:

using Test: AbstractTestSet, Pass, Fail, Error, Broken, get_test_counts, TestCounts, format_duration

function Test.get_test_counts(ts::CustomTestSet)
    passes, fails, errors, broken = 0, 0, 0, 0
    # cumulative results
    c_passes, c_fails, c_errors, c_broken = 0, 0, 0, 0

    for t in ts.results
        # count up results
        isa(t, Pass)   && (passes += 1)
        isa(t, Fail)   && (fails  += 1)
        isa(t, Error)  && (errors += 1)
        isa(t, Broken) && (broken += 1)
        # handle children
        if isa(t, AbstractTestSet)
            tc = get_test_counts(t)::TestCounts
            c_passes += tc.passes + tc.cumulative_passes
            c_fails  += tc.fails + tc.cumulative_fails
            c_errors += tc.errors + tc.cumulative_errors
            c_broken += tc.broken + tc.cumulative_broken
        end
    end
    # get a duration, if we have one
    duration = format_duration(ts)
    return TestCounts(true, passes, fails, errors, broken, c_passes, c_fails, c_errors, c_broken, duration)
end
Test.TestCounts — Type
TestCounts

用于递归收集测试集结果以供显示的状态。

字段:

  • customized: 函数 get_test_counts 是否为此计数对象所对应的 AbstractTestSet 定制。如果定义了自定义方法,请始终将 true 传递给构造函数。
  • passes: 通过的 @test 调用次数。
  • fails: 失败的 @test 调用次数。
  • errors: 出错的 @test 调用次数。
  • broken: 破损的 @test 调用次数。
  • passes: 通过的 @test 调用的累计次数。
  • fails: 失败的 @test 调用的累计次数。
  • errors: 出错的 @test 调用的累计次数。
  • broken: 破损的 @test 调用的累计次数。
  • duration: 相关 AbstractTestSet 运行的总时长,以格式化的 String 表示。
source
Test.get_test_counts — Function

" gettestcounts(::AbstractTestSet) -> TestCounts

递归函数,直接在测试集中计算每种类型的测试结果数量,并在子测试集中进行汇总。

自定义的 AbstractTestSet 应该实现此函数,以便其总数能够与 DefaultTestSet 一起被计算和显示。

如果自定义的 TestSet 没有实现此功能,则打印将回退为报告失败的 x 和持续时间的 ?s。"

source
Test.format_duration — Function
format_duration(::AbstractTestSet)

返回一个格式化的字符串,用于打印测试集运行的持续时间。

如果未定义,则回退到 "?s"。

source
Test.print_test_results — Function
print_test_results(ts::AbstractTestSet, depth_pad=0)

打印 AbstractTestSet 的结果作为格式化表格。

depth_pad 指的是在所有输出前应添加多少填充。

在 Test.finish 内部调用,如果 finish 的测试集是最上层的测试集。

source

Test utilities

Test.GenericArray — Type

GenericArray 可以用来测试编程为 AbstractArray 接口的通用数组 API,以确保函数可以与标准 Array 类型以外的数组类型一起工作。

source
Test.GenericDict — Type

GenericDict 可以用来测试编程为 AbstractDict 接口的通用字典 API,以确保函数可以与除了标准 Dict 类型之外的关联类型一起工作。

source
Test.GenericSet — Type

GenericSet 可以用来测试编程为 AbstractSet 接口的通用集合 API,以确保函数可以与标准 Set 和 BitSet 类型以外的集合类型一起工作。

source
Test.GenericString — Type

GenericString 可以用来测试通用字符串 API,这些 API 针对 AbstractString 接口进行编程,以确保函数能够与标准 String 类型以外的字符串类型一起工作。

source
Test.detect_ambiguities — Function
detect_ambiguities(mod1, mod2...; recursive=false,
                                  ambiguous_bottom=false,
                                  allowed_undefineds=nothing)

返回一个包含在指定模块中定义的模糊方法的 (Method,Method) 对的向量。使用 recursive=true 在所有子模块中进行测试。

ambiguous_bottom 控制是否包含仅由 Union{} 类型参数触发的模糊性;在大多数情况下,您可能希望将其设置为 false。请参见 Base.isambiguous。

有关 allowed_undefineds 的解释,请参见 Test.detect_unbound_args。

Julia 1.8

allowed_undefineds 至少需要 Julia 1.8。

source
Test.detect_unbound_args — Function
detect_unbound_args(mod1, mod2...; recursive=false, allowed_undefineds=nothing)

返回一个可能具有未绑定类型参数的 Method 向量。使用 recursive=true 在所有子模块中进行测试。

默认情况下,任何未定义的符号都会触发警告。可以通过提供一个 GlobalRef 的集合来抑制此警告,以跳过警告。例如,设置

allowed_undefineds = Set([GlobalRef(Base, :active_repl),
                          GlobalRef(Base, :active_repl_backend)])

将抑制关于 Base.active_repl 和 Base.active_repl_backend 的警告。

Julia 1.8

allowed_undefineds 至少需要 Julia 1.8。

source

Workflow for Testing Packages

使用我们在前面部分中可用的工具,以下是创建一个包并向其添加测试的潜在工作流程。

Generating an Example Package

对于这个工作流程,我们将创建一个名为 Example 的包:

pkg> generate Example
shell> cd Example
shell> mkdir test
pkg> activate .

Creating Sample Functions

测试一个包的首要要求是拥有可测试的功能。为此,我们将向 Example 添加一些简单的函数,以便进行测试。将以下内容添加到 src/Example.jl:

module Example

function greet()
    "Hello world!"
end

function simple_add(a, b)
    a + b
end

function type_multiply(a::Float64, b::Float64)
    a * b
end

export greet, simple_add, type_multiply

end

Creating a Test Environment

从 Example 包的根目录中,导航到 test 目录,在那里激活一个新环境,并将 Test 包添加到该环境中:

shell> cd test
pkg> activate .
(test) pkg> add Test

Testing Our Package

现在,我们准备为 Example 添加测试。标准做法是在 test 目录中创建一个名为 runtests.jl 的文件,其中包含我们想要运行的测试集。请在 test 目录中创建该文件,并添加以下代码:

using Example
using Test

@testset "Example tests" begin

    @testset "Math tests" begin
        include("math_tests.jl")
    end

    @testset "Greeting tests" begin
        include("greeting_tests.jl")
    end
end

我们需要创建这两个包含的文件,math_tests.jl 和 greeting_tests.jl,并向它们添加一些测试。

注意: 请注意,我们不需要在 test 环境的 Project.toml 中指定添加 Example。这是 Julia 测试系统的一个好处,您可以 read about more here。

Writing Tests for math_tests.jl

使用我们对 Test.jl 的了解,这里有一些我们可以添加到 math_tests.jl 的示例测试:

@testset "Testset 1" begin
    @test 2 == simple_add(1, 1)
    @test 3.5 == simple_add(1, 2.5)
        @test_throws MethodError simple_add(1, "A")
        @test_throws MethodError simple_add(1, 2, 3)
end

@testset "Testset 2" begin
    @test 1.0 == type_multiply(1.0, 1.0)
        @test isa(type_multiply(2.0, 2.0), Float64)
    @test_throws MethodError type_multiply(1, 2.5)
end

Writing Tests for greeting_tests.jl

使用我们对 Test.jl 的了解,以下是我们可以添加到 greeting_tests.jl 的一些示例测试:

@testset "Testset 3" begin
    @test "Hello world!" == greet()
    @test_throws MethodError greet("Antonia")
end

Testing Our Package

现在我们已经在 test 中添加了我们的测试和 runtests.jl 脚本,我们可以通过返回到 Example 包环境的根目录并重新激活 Example 环境来测试我们的 Example 包:

shell> cd ..
pkg> activate .

从那里,我们终于可以按如下方式运行我们的测试套件:

(Example) pkg> test
     Testing Example
      Status `/tmp/jl_Yngpvy/Project.toml`
  [fa318bd2] Example v0.1.0 `/home/src/Projects/tmp/errata/Example`
  [8dfed614] Test `@stdlib/Test`
      Status `/tmp/jl_Yngpvy/Manifest.toml`
  [fa318bd2] Example v0.1.0 `/home/src/Projects/tmp/errata/Example`
  [2a0f44e3] Base64 `@stdlib/Base64`
  [b77e0a4c] InteractiveUtils `@stdlib/InteractiveUtils`
  [56ddb016] Logging `@stdlib/Logging`
  [d6f4376e] Markdown `@stdlib/Markdown`
  [9a3f8284] Random `@stdlib/Random`
  [ea8e919c] SHA `@stdlib/SHA`
  [9e88b42a] Serialization `@stdlib/Serialization`
  [8dfed614] Test `@stdlib/Test`
     Testing Running tests...
Test Summary: | Pass  Total
Example tests |    9      9
     Testing Example tests passed

如果一切顺利,你应该会看到与上面类似的输出。使用 Test.jl,可以为包添加更复杂的测试,但这应该理想地指引开发者如何开始测试他们自己创建的包。

Code Coverage

在测试期间,可以使用 pkg> test --coverage 标志启用代码覆盖率跟踪(或使用 --code-coverage julia 参数在更低级别启用)。在 julia-runtest GitHub 操作中,默认启用此功能。

要评估覆盖率,可以手动检查生成在源文件旁边的 .cov 文件,或者在 CI 中使用 julia-processcoverage GitHub 操作。

Julia 1.11

自 Julia 1.11 起,覆盖率不再在包的预编译阶段收集。