Unit Testing
Testing Base Julia
Julia 正在快速发展,并且拥有一个广泛的测试套件,以验证多个平台上的功能。如果您从源代码构建 Julia,可以使用 make test
运行此测试套件。在二进制安装中,您可以使用 Base.runtests()
运行测试套件。
Base.runtests
— FunctionBase.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
或标准库的任何修改。如果通过关键字参数提供了种子,则在运行测试的上下文中用于初始化全局随机数生成器;否则,种子将随机选择。
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
测试通过
broken
和 skip
关键字参数需要至少 Julia 1.7。
Test.@test_throws
— Macro@test_throws exception expr
测试表达式 expr
是否抛出 exception
。异常可以指定类型、字符串、正则表达式或在显示的错误消息中出现的字符串列表、匹配函数或值(将通过比较字段进行相等性测试)。请注意,@test_throws
不支持尾随关键字形式。
指定除类型或值以外的任何内容作为 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)
(匹配函数)
例如,假设我们想检查我们的新函数 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
通常会使用大量测试来确保函数在一系列输入下正常工作。如果测试失败,默认行为是立即抛出异常。然而,通常更可取的是先运行其余的测试,以更好地了解被测试代码中有多少错误。
@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
全局设置。
@testset test_func()
至少需要 Julia 1.8。
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
时,宏会启动一个 透明 测试集,将给定对象作为上下文对象添加到其中的任何失败测试。这在对一个较大对象执行一组相关测试时非常有用,并且希望在任何单个测试失败时打印这个较大对象。透明测试集不会在测试集层次结构中引入额外的嵌套级别,并直接传递给父测试集(上下文对象附加到任何失败的测试中)。
@testset let
至少需要 Julia 1.9。
从 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
错误: 测试期间发生错误
Test.TestSetException
— TypeTestSetException
当测试集完成且并非所有测试都通过时抛出。
我们可以将 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
。
Test.TestLogger
— TypeTestLogger(; 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
Test.LogRecord
— TypeLogRecord
存储单个日志事件的结果。字段:
level
: 日志消息的LogLevel
message
: 日志消息的文本内容_module
: 日志事件的模块group
: 日志组(默认情况下为包含日志事件的文件名)id
: 日志事件的 IDfile
: 包含日志事件的文件line
: 日志事件在文件中的行kwargs
: 传递给日志事件的任何关键字参数
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} 不匹配
[...]
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"
Test.@test_warn
— Macro@test_warn msg expr
测试评估 expr
是否产生包含 msg
字符串或与 msg
正则表达式匹配的 stderr
输出。如果 msg
是一个布尔函数,则测试 msg(output)
是否返回 true
。如果 msg
是一个元组或数组,则检查错误输出是否包含/匹配 msg
中的每个项。返回评估 expr
的结果。
另请参见 @test_nowarn
以检查错误输出的缺失。
注意:由 @warn
生成的警告不能使用此宏进行测试。请改用 @test_logs
。
Test.@test_nowarn
— Macro@test_nowarn expr
测试评估 expr
是否产生空的 stderr
输出(没有警告或其他消息)。返回评估 expr
的结果。
注意:无法使用此宏测试 @warn
生成的警告缺失。请改用 @test_logs
。
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)
@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)
Test result types
Test.Result
— TypeTest.Result
所有测试都会生成一个结果对象。这个对象可能会被存储,也可能不会,具体取决于测试是否是测试集的一部分。
Test.Pass
— TypeTest.Pass <: Test.Result
测试条件为真,即表达式评估为真或抛出了正确的异常。
Test.Fail
— TypeTest.Fail <: Test.Result
测试条件为假,即表达式的计算结果为假,或者没有抛出正确的异常。
Test.Error
— TypeTest.Error <: Test.Result
测试条件由于异常无法评估,或者评估为除 Bool
之外的其他内容。在 @test_broken
的情况下,它用于指示发生了意外的 Pass
Result
。
Test.Broken
— TypeTest.Broken <: Test.Result
测试条件是预期的(失败的)破损测试结果,或者是通过 @test_skip
明确跳过的。
Creating Custom AbstractTestSet
Types
包可以通过实现 record
和 finish
方法来创建自己的 AbstractTestSet
子类型。子类型应具有一个接受描述字符串的单参数构造函数,任何选项都作为关键字参数传递。
Test.record
— Functionrecord(ts::AbstractTestSet, res::Result)
将结果记录到测试集。每当包含的 @test
宏完成时,此函数会被 @testset
基础设施调用,并给出测试结果(可能是一个 Error
)。如果在测试块内但在 @test
上下文之外抛出异常,也会调用此函数并传入一个 Error
。
Test.finish
— Functionfinish(ts::AbstractTestSet)
对给定的测试集进行任何必要的最终处理。这是在测试块执行后由 @testset
基础设施调用的。
自定义的 AbstractTestSet
子类型应该在其父类(如果有的话)上调用 record
,以将自己添加到测试结果树中。这可以实现为:
if get_testset_depth() != 0
# 将此测试集附加到父测试集
parent_ts = get_testset()
record(parent_ts, self)
return self
end
Test
负责维护在执行时嵌套测试集的堆栈,但任何结果的累积都是 AbstractTestSet
子类型的责任。您可以通过 get_testset
和 get_testset_depth
方法访问此堆栈。请注意,这些函数未被导出。
Test.get_testset
— Functionget_testset()
从任务的本地存储中检索活动测试集。如果没有活动测试集,则使用备用默认测试集。
Test.get_testset_depth
— Functionget_testset_depth()
返回活动测试集的数量,不包括默认测试集
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
— TypeTestCounts
用于递归收集测试集结果以供显示的状态。
字段:
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
表示。
Test.get_test_counts
— Function" gettestcounts(::AbstractTestSet) -> TestCounts
递归函数,直接在测试集中计算每种类型的测试结果数量,并在子测试集中进行汇总。
自定义的 AbstractTestSet
应该实现此函数,以便其总数能够与 DefaultTestSet
一起被计算和显示。
如果自定义的 TestSet
没有实现此功能,则打印将回退为报告失败的 x
和持续时间的 ?s
。"
Test.format_duration
— Functionformat_duration(::AbstractTestSet)
返回一个格式化的字符串,用于打印测试集运行的持续时间。
如果未定义,则回退到 "?s"
。
Test.print_test_results
— Functionprint_test_results(ts::AbstractTestSet, depth_pad=0)
打印 AbstractTestSet
的结果作为格式化表格。
depth_pad
指的是在所有输出前应添加多少填充。
在 Test.finish
内部调用,如果 finish
的测试集是最上层的测试集。
Test utilities
Test.GenericArray
— TypeGenericArray
可以用来测试编程为 AbstractArray
接口的通用数组 API,以确保函数可以与标准 Array
类型以外的数组类型一起工作。
Test.GenericDict
— TypeGenericDict
可以用来测试编程为 AbstractDict
接口的通用字典 API,以确保函数可以与除了标准 Dict
类型之外的关联类型一起工作。
Test.GenericOrder
— TypeGenericOrder
可以用来测试 API 对通用有序类型的支持。
Test.GenericSet
— TypeGenericSet
可以用来测试编程为 AbstractSet
接口的通用集合 API,以确保函数可以与标准 Set
和 BitSet
类型以外的集合类型一起工作。
Test.GenericString
— TypeGenericString
可以用来测试通用字符串 API,这些 API 针对 AbstractString
接口进行编程,以确保函数能够与标准 String
类型以外的字符串类型一起工作。
Test.detect_ambiguities
— Functiondetect_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
。
allowed_undefineds
至少需要 Julia 1.8。
Test.detect_unbound_args
— Functiondetect_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
的警告。
allowed_undefineds
至少需要 Julia 1.8。
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 起,覆盖率不再在包的预编译阶段收集。