单元测试

测试 Base Julia

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

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

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

源代码

基本单元测试

Test 模块提供简单的单元测试功能。单元测试是一种通过检查结果是否符合预期来查看代码是否正确的方法。在更改代码后确保代码仍然有效非常有用,并且可以在开发时用作指定代码在完成时应具有的行为的方式。你可能还想查看有关将测试添加到 Julia 包的文档。

可以使用 @test@test_throws 宏执行简单的单元测试

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

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

示例

julia> @test true
Test Passed

julia> @test [1, 2] + [2, 1] == [3, 3]
Test Passed

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

julia> @test π ≈ 3.14 atol=0.01
Test Passed

这等效于更难看的测试 @test ≈(π, 3.14, atol=0.01)。如果提供多个表达式,除非第一个是调用表达式,其余是赋值 (k=v),否则会出错。

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

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

示例

julia> @test 2 + 2 ≈ 6 atol=1 broken=true
Test Broken
  Expression: ≈(2 + 2, 6, atol = 1)

julia> @test 2 + 2 ≈ 5 atol=1 broken=false
Test Passed

julia> @test 2 + 2 == 5 skip=true
Test Broken
  Skipped: 2 + 2 == 5

julia> @test 2 + 2 == 4 skip=false
Test Passed
Julia 1.7

brokenskip 关键字参数需要至少 Julia 1.7。

源代码
Test.@test_throws
@test_throws exception expr

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

Julia 1.8

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

示例

julia> @test_throws BoundsError [1, 2, 3][4]
Test Passed
      Thrown: BoundsError

julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
Test Passed
      Thrown: DimensionMismatch

julia> @test_throws "Try sqrt(Complex" sqrt(-1)
Test Passed
     Message: "DomainError with -1.0:\nsqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x))."

在最后一个示例中,除了匹配单个字符串之外,还可以使用

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

例如,假设我们要检查新的函数 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)
  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

使用测试集

通常使用大量测试来确保函数在各种输入情况下都能正常工作。如果测试失败,默认行为是立即抛出异常。但是,通常最好先运行其余测试,以更好地了解被测代码中存在多少错误。

注意

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

@testset 宏可用于将测试分组为。将运行测试集中的所有测试,并在测试集结束时打印摘要。如果任何测试失败或由于错误而无法评估,则测试集将抛出 TestSetException

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

使用 begin/end 或函数调用

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

如果没有给出自定义测试集类型,它将默认创建 DefaultTestSetDefaultTestSet 记录所有结果,如果存在任何 FailError,则在顶层 (非嵌套) 测试集结束时抛出异常,以及测试结果的摘要。

可以给出任何自定义测试集类型 (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 是全局 RNG 的当前种子。此外,在执行主体之后,全局 RNG 的状态将恢复到 @testset 之前的状态。这样做是为了在发生错误时简化可重复性,并允许在不影响全局 RNG 状态的情况下无缝重新排列 @testset

示例

julia> @testset "trigonometric identities" begin
           θ = 2/3*π
           @test sin(-θ) ≈ -sin(θ)
           @test cos(-θ) ≈ cos(θ)
           @test sin(2θ) ≈ 2*sin(θ)*cos(θ)
           @test cos(2θ) ≈ cos(θ)^2 - sin(θ)^2
       end;
Test Summary:            | Pass  Total  Time
trigonometric identities |    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
Test Failed at none:3
  Expression: !(iszero(real(logi)))
     Context: logi = 0.0 + 1.5707963267948966im

ERROR: There was an error during testing

julia> @testset let logi = log(im), op = !iszero
           @test imag(logi) == π/2
           @test op(real(logi))
       end
Test Failed at none:3
  Expression: op(real(logi))
     Context: logi = 0.0 + 1.5707963267948966im
              op = !iszero

ERROR: There was an error during testing
源代码

我们可以将针对 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.

测试日志语句

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

Test.@test_logs
@test_logs [log_patterns...] [keywords] expression

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

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

示例

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

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 that the expression logs no messages when the logger level is warn:
@test_logs min_level=Logging.Warn @info("Some information") # passes
@test_logs min_level=Logging.Warn @warn("Some information") # fails

如果您想测试 stderr 中未由 @warn 生成的警告(或错误消息)的缺失,请参阅 @test_nowarn

源代码
Test.TestLogger类型
TestLogger(; min_level=Info, catch_exceptions=false)

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

设置 min_level 以控制 LogLevel,设置 catch_exceptions 以控制是否应捕获作为日志事件生成的一部分而引发的异常,以及设置 respect_maxlog 以控制是否遵循将具有 maxlog=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
源代码
Test.LogRecord类型
LogRecord

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

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

其他测试宏

由于浮点值上的计算可能不精确,因此可以使用 @test a ≈ b(其中 通过 \approx 的制表符完成输入,是 isapprox 函数)或直接使用 isapprox 来执行近似相等性检查。

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

您可以通过分别在 比较之后设置 isapproxrtolatol 关键字参数来指定相对容差和绝对容差

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
@inferred [AllowedType] f(x)

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

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

可选地,AllowedType 通过使测试在以下情况通过来放宽测试:f(x) 的类型与推断的类型匹配 modulo AllowedType,或者返回类型是 AllowedType 的子类型。当测试返回小联合(例如 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)
ERROR: return type Int64 does not match inferred return type 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)
ERROR: return type Float64 does not match inferred return type 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)
ERROR: return type Int64 does not match inferred return type Union{Missing, Float64, Int64}
[...]
源代码
Test.@test_deprecated
@test_deprecated [pattern] expression

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

--depwarn=no 时,只需返回执行 expression 的结果。当 --depwarn=error 时,检查是否抛出 ErrorException。

示例

# Deprecated in julia 0.7
@test_deprecated num2hex(1)

# The returned value can be tested by chaining with @test:
@test (@test_deprecated num2hex(1)) == "0000000000000001"
源代码
Test.@test_warn
@test_warn msg expr

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

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

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

源代码
Test.@test_nowarn
@test_nowarn expr

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

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

源代码

损坏的测试

如果测试始终失败,则可以更改为使用 @test_broken 宏。如果测试继续失败,则将表示该测试为 Broken,如果测试成功,则会通过 Error 向用户发出警报。

Test.@test_broken
@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
Test Broken
  Expression: 1 == 2

julia> @test_broken 1 == 2 atol=0.1
Test Broken
  Expression: ==(1, 2, atol = 0.1)
源代码

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

Test.@test_skip
@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
Test Broken
  Skipped: 1 == 2

julia> @test_skip 1 == 2 atol=0.1
Test Broken
  Skipped: ==(1, 2, atol = 0.1)
源代码

测试结果类型

Test.Result类型
Test.Result

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

源代码
Test.Pass类型
Test.Pass <: Test.Result

测试条件为真,即表达式计算结果为真或抛出了正确的异常。

源代码
Test.Fail类型
Test.Fail <: Test.Result

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

源代码
Test.Error类型
Test.Error <: Test.Result

由于异常导致测试条件无法评估,或者评估结果不是 Bool。在 @test_broken 的情况下,它用于指示发生了意外的 Pass Result

源代码
Test.Broken类型
Test.Broken <: Test.Result

测试条件是断裂测试的预期(失败)结果,或者使用 @test_skip 显式跳过。

源代码

创建自定义 AbstractTestSet 类型

包可以通过实现 recordfinish 方法来创建自己的 AbstractTestSet 子类型。子类型应该有一个带描述字符串的单参数构造函数,并将任何选项作为关键字参数传入。

Test.record函数
record(ts::AbstractTestSet, res::Result)

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

源代码
Test.finish函数
finish(ts::AbstractTestSet)

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

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

if get_testset_depth() != 0
    # Attach this test set to the parent test set
    parent_ts = get_testset()
    record(parent_ts, self)
    return self
end
源代码

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

Test.get_testset函数
get_testset()

从任务的本地存储中检索活动测试集。如果没有任何测试集处于活动状态,请使用回退默认测试集。

源代码

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)
    end
    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.GenericArray类型

GenericArray 可用于测试对 AbstractArray 接口进行编程的通用数组 API,以确保函数可以与除标准 Array 类型之外的数组类型一起使用。

源代码
Test.GenericDict类型

GenericDict 可用于测试对 AbstractDict 接口进行编程的通用字典 API,以确保函数可以与除标准 Dict 类型之外的关联类型一起使用。

源代码
Test.GenericSet类型

GenericSet 可用于测试对 AbstractSet 接口进行编程的通用集合 API,以确保函数可以与除标准 SetBitSet 类型之外的集合类型一起使用。

源代码
Test.GenericString类型

GenericString 可用于测试对 AbstractString 接口进行编程的通用字符串 API,以确保函数可以与除标准 String 类型之外的字符串类型一起使用。

源代码
Test.detect_ambiguities函数
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。

源代码
Test.detect_unbound_args函数
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_replBase.active_repl_backend 的警告。

Julia 1.8

allowed_undefineds 至少需要 Julia 1.8。

源代码

包测试工作流程

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

生成示例包

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

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

创建示例函数

测试包的首要要求是拥有要测试的功能。为此,我们将向 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

end

创建测试环境

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

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

测试我们的包

现在,我们准备向 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.jlgreeting_tests.jl,并向其中添加一些测试。

注意:请注意,我们无需在 test 环境的 Project.toml 中指定添加 Example。这是 Julia 测试系统的优势,您可以 在这里阅读更多相关信息

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

greeting_tests.jl 编写测试

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

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

测试我们的包

现在我们已经添加了测试和 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,可以为包添加更复杂的测试,但理想情况下,这应该能使开发人员了解如何开始测试自己创建的包。