单元测试

测试 Julia Base 库

Julia 处于快速开发中,有着可以扩展的测试套件,用来跨平台测试功能。 如果你是通过源代码构建的 Julia ,你可以通过 make test 来运行这个测试套件。 如果是通过二进制包安装的,你可以通过 Base.runtests() 来运行这个测试套件。

Base.runtests — Function

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

Run the Julia unit tests listed in tests, which can be either a string or an array of strings, using ncores processors. If exit_on_error is false, when one test fails, all remaining tests in other files will still be run; they are otherwise discarded, when exit_on_error == true. If revise is true, the Revise package is used to load any modifications to Base or to the standard libraries before running the tests. If a seed is provided via the keyword argument, it is used to seed the global RNG in the context where the tests are run; otherwise the seed is chosen randomly.

source

基本的单元测试

The Test module provides simple unit testing functionality. Unit testing is a way to see if your code is correct by checking that the results are what you expect. It can be helpful to ensure your code still works after you make changes, and can be used when developing as a way of specifying the behaviors your code should have when complete.

简单的单元测试可以通过 @test@test_throws 宏来完成:

Test.@test — Macro

  1. @test ex
  2. @test f(args...) key=val ...
  3. @test ex broken=true
  4. @test ex skip=true

Tests that the expression ex evaluates to true. Returns a Pass Result if it does, a Fail Result if it is false, and an Error Result if it could not be evaluated.

Examples

  1. julia> @test true
  2. Test Passed
  3. Expression: true
  4. julia> @test [1, 2] + [2, 1] == [3, 3]
  5. Test Passed
  6. Expression: [1, 2] + [2, 1] == [3, 3]
  7. Evaluated: [3, 3] == [3, 3]

The @test f(args...) key=val... form is equivalent to writing @test f(args..., key=val...) which can be useful when the expression is a call using infix syntax such as approximate comparisons:

  1. julia> @test π 3.14 atol=0.01
  2. Test Passed
  3. Expression: ≈(π, 3.14, atol = 0.01)
  4. Evaluated: ≈(π, 3.14; atol = 0.01)

This is equivalent to the uglier test @test ≈(π, 3.14, atol=0.01). It is an error to supply more than one expression unless the first is a call expression and the rest are assignments (k=v).

You can use any key for the key=val arguments, except for broken and skip, which have special meanings in the context of @test:

  • broken=cond indicates a test that should pass but currently consistently fails when cond==true. Tests that the expression ex evaluates to false or causes an exception. Returns a Broken Result if it does, or an Error Result if the expression evaluates to true. Regular @test ex is evaluated when cond==false.
  • skip=cond marks a test that should not be executed but should be included in test summary reporting as Broken, when cond==true. This can be useful for tests that intermittently fail, or tests of not-yet-implemented functionality. Regular @test ex is evaluated when cond==false.

Examples

  1. julia> @test 2 + 2 6 atol=1 broken=true
  2. Test Broken
  3. Expression: ≈(2 + 2, 6, atol = 1)
  4. julia> @test 2 + 2 5 atol=1 broken=false
  5. Test Passed
  6. Expression: ≈(2 + 2, 5, atol = 1)
  7. Evaluated: ≈(4, 5; atol = 1)
  8. julia> @test 2 + 2 == 5 skip=true
  9. Test Broken
  10. Skipped: 2 + 2 == 5
  11. julia> @test 2 + 2 == 4 skip=false
  12. Test Passed
  13. Expression: 2 + 2 == 4
  14. Evaluated: 4 == 4

Julia 1.7

The broken and skip keyword arguments require at least Julia 1.7.

Test.@test_throws — Macro

  1. @test_throws exception expr

Tests that the expression expr throws exception. The exception may specify either a type, or a value (which will be tested for equality by comparing fields). Note that @test_throws does not support a trailing keyword form.

Examples

  1. julia> @test_throws BoundsError [1, 2, 3][4]
  2. Test Passed
  3. Expression: ([1, 2, 3])[4]
  4. Thrown: BoundsError
  5. julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
  6. Test Passed
  7. Expression: [1, 2, 3] + [1, 2]
  8. Thrown: DimensionMismatch

例如,假设我们想要测试新的函数 foo(x) 是否按照期望的方式工作:

  1. julia> using Test
  2. julia> foo(x) = length(x)^2
  3. foo (generic function with 1 method)

If the condition is true, a Pass is returned:

  1. julia> @test foo("bar") == 9
  2. Test Passed
  3. Expression: foo("bar") == 9
  4. Evaluated: 9 == 9
  5. julia> @test foo("fizz") >= 10
  6. Test Passed
  7. Expression: foo("fizz") >= 10
  8. Evaluated: 16 >= 10

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

  1. julia> @test foo("f") == 20
  2. Test Failed at none:1
  3. Expression: foo("f") == 20
  4. Evaluated: 1 == 20
  5. ERROR: There was an error during testing

If the condition could not be evaluated because an exception was thrown, which occurs in this case because length is not defined for symbols, an Error object is returned and an exception is thrown:

  1. julia> @test foo(:cat) == 1
  2. Error During Test
  3. Test threw an exception of type MethodError
  4. Expression: foo(:cat) == 1
  5. MethodError: no method matching length(::Symbol)
  6. Closest candidates are:
  7. length(::SimpleVector) at essentials.jl:256
  8. length(::Base.MethodList) at reflection.jl:521
  9. length(::MethodTable) at reflection.jl:597
  10. ...
  11. Stacktrace:
  12. [...]
  13. ERROR: There was an error during testing

If we expect that evaluating an expression should throw an exception, then we can use @test_throws to check that this occurs:

  1. julia> @test_throws MethodError foo(:cat)
  2. Test Passed
  3. Expression: foo(:cat)
  4. Thrown: MethodError

Working with Test Sets

Typically a large number of tests are used to make sure functions work correctly over a range of inputs. In the event a test fails, the default behavior is to throw an exception immediately. However, it is normally preferable to run the rest of the tests first to get a better picture of how many errors there are in the code being tested.

Note

The @testset will create a local scope of its own when running the tests in it.

The @testset macro can be used to group tests into sets. All the tests in a test set will be run, and at the end of the test set a summary will be printed. If any of the tests failed, or could not be evaluated due to an error, the test set will then throw a TestSetException.

Test.@testset — Macro

  1. @testset [CustomTestSet] [option=val ...] ["description"] begin ... end
  2. @testset [CustomTestSet] [option=val ...] ["description $v"] for v in (...) ... end
  3. @testset [CustomTestSet] [option=val ...] ["description $v, $w"] for v in (...), w in (...) ... end

Starts a new test set, or multiple test sets if a for loop is provided.

If no custom testset type is given it defaults to creating a DefaultTestSet. DefaultTestSet records all the results and, if there are any Fails or Errors, throws an exception at the end of the top-level (non-nested) test set, along with a summary of the test results.

Any custom testset type (subtype of AbstractTestSet) can be given and it will also be used for any nested @testset invocations. The given options are only applied to the test set where they are given. The default test set type accepts the verbose boolean option: if true, the result summary of the nested testsets is shown even when they all pass (the default is false).

The description string accepts interpolation from the loop indices. If no description is provided, one is constructed based on the variables.

By default the @testset macro will return the testset object itself, though this behavior can be customized in other testset types. If a for loop is used then the macro collects and returns a list of the return values of the finish method, which by default will return a list of the testset objects used in each iteration.

Before the execution of the body of a @testset, there is an implicit call to Random.seed!(seed) where seed is the current seed of the global RNG. Moreover, after the execution of the body, the state of the global RNG is restored to what it was before the @testset. This is meant to ease reproducibility in case of failure, and to allow seamless re-arrangements of @testsets regardless of their side-effect on the global RNG state.

Examples

  1. julia> @testset "trigonometric identities" begin
  2. θ = 2/3
  3. @test sin(-θ) -sin(θ)
  4. @test cos(-θ) cos(θ)
  5. @test sin(2θ) 2*sin(θ)*cos(θ)
  6. @test cos(2θ) cos(θ)^2 - sin(θ)^2
  7. end;
  8. Test Summary: | Pass Total
  9. trigonometric identities | 4 4

Test.TestSetException — Type

  1. TestSetException

Thrown when a test set finishes and not all tests passed.

We can put our tests for the foo(x) function in a test set:

  1. julia> @testset "Foo Tests" begin
  2. @test foo("a") == 1
  3. @test foo("ab") == 4
  4. @test foo("abc") == 9
  5. end;
  6. Test Summary: | Pass Total
  7. Foo Tests | 3 3

测试集可以嵌套:

  1. julia> @testset "Foo Tests" begin
  2. @testset "Animals" begin
  3. @test foo("cat") == 9
  4. @test foo("dog") == foo("cat")
  5. end
  6. @testset "Arrays $i" for i in 1:3
  7. @test foo(zeros(i)) == i^2
  8. @test foo(fill(1.0, i)) == i^2
  9. end
  10. end;
  11. Test Summary: | Pass Total
  12. Foo Tests | 8 8

In the event that a nested test set has no failures, as happened here, it will be hidden in the summary, unless the verbose=true option is passed:

  1. julia> @testset verbose = true "Foo Tests" begin
  2. @testset "Animals" begin
  3. @test foo("cat") == 9
  4. @test foo("dog") == foo("cat")
  5. end
  6. @testset "Arrays $i" for i in 1:3
  7. @test foo(zeros(i)) == i^2
  8. @test foo(fill(1.0, i)) == i^2
  9. end
  10. end;
  11. Test Summary: | Pass Total
  12. Foo Tests | 8 8
  13. Animals | 2 2
  14. Arrays 1 | 2 2
  15. Arrays 2 | 2 2
  16. Arrays 3 | 2 2

If we do have a test failure, only the details for the failed test sets will be shown:

  1. julia> @testset "Foo Tests" begin
  2. @testset "Animals" begin
  3. @testset "Felines" begin
  4. @test foo("cat") == 9
  5. end
  6. @testset "Canines" begin
  7. @test foo("dog") == 9
  8. end
  9. end
  10. @testset "Arrays" begin
  11. @test foo(zeros(2)) == 4
  12. @test foo(fill(1.0, 4)) == 15
  13. end
  14. end
  15. Arrays: Test Failed
  16. Expression: foo(fill(1.0, 4)) == 15
  17. Evaluated: 16 == 15
  18. [...]
  19. Test Summary: | Pass Fail Total
  20. Foo Tests | 3 1 4
  21. Animals | 2 2
  22. Arrays | 1 1 2
  23. ERROR: Some tests did not pass: 3 passed, 1 failed, 0 errored, 0 broken.

Other Test Macros

As calculations on floating-point values can be imprecise, you can perform approximate equality checks using either @test a ≈ b (where , typed via tab completion of \approx, is the isapprox function) or use isapprox directly.

  1. julia> @test 1 0.999999999
  2. Test Passed
  3. Expression: 1 0.999999999
  4. Evaluated: 1 0.999999999
  5. julia> @test 1 0.999999
  6. Test Failed at none:1
  7. Expression: 1 0.999999
  8. Evaluated: 1 0.999999
  9. ERROR: There was an error during testing

You can specify relative and absolute tolerances by setting the rtol and atol keyword arguments of isapprox, respectively, after the comparison:

  1. julia> @test 1 0.999999 rtol=1e-5
  2. Test Passed
  3. Expression: ≈(1, 0.999999, rtol = 1.0e-5)
  4. Evaluated: ≈(1, 0.999999; rtol = 1.0e-5)

Note that this is not a specific feature of the but rather a general feature of the @test macro: @test a <op> b key=val is transformed by the macro into @test op(a, b, key=val). It is, however, particularly useful for tests.

Test.@inferred — Macro

  1. @inferred [AllowedType] f(x)

Tests that the call expression f(x) returns a value of the same type inferred by the compiler. It is useful to check for type stability.

f(x) can be any call expression. Returns the result of f(x) if the types match, and an Error Result if it finds different types.

Optionally, AllowedType relaxes the test, by making it pass when either the type of f(x) matches the inferred type modulo AllowedType, or when the return type is a subtype of AllowedType. This is useful when testing type stability of functions returning a small union such as Union{Nothing, T} or Union{Missing, T}.

  1. julia> f(a) = a > 1 ? 1 : 1.0
  2. f (generic function with 1 method)
  3. julia> typeof(f(2))
  4. Int64
  5. julia> @code_warntype f(2)
  6. MethodInstance for f(::Int64)
  7. from f(a) in Main at none:1
  8. Arguments
  9. #self#::Core.Const(f)
  10. a::Int64
  11. Body::UNION{FLOAT64, INT64}
  12. 1 %1 = (a > 1)::Bool
  13. └── goto #3 if not %1
  14. 2 return 1
  15. 3 return 1.0
  16. julia> @inferred f(2)
  17. ERROR: return type Int64 does not match inferred return type Union{Float64, Int64}
  18. [...]
  19. julia> @inferred max(1, 2)
  20. 2
  21. julia> g(a) = a < 10 ? missing : 1.0
  22. g (generic function with 1 method)
  23. julia> @inferred g(20)
  24. ERROR: return type Float64 does not match inferred return type Union{Missing, Float64}
  25. [...]
  26. julia> @inferred Missing g(20)
  27. 1.0
  28. julia> h(a) = a < 10 ? missing : f(a)
  29. h (generic function with 1 method)
  30. julia> @inferred Missing h(20)
  31. ERROR: return type Int64 does not match inferred return type Union{Missing, Float64, Int64}
  32. [...]

Test.@test_logs — Macro

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

Collect a list of log records generated by expression using collect_test_logs, check that they match the sequence log_patterns, and return the value of expression. The keywords provide some simple filtering of log records: the min_level keyword controls the minimum log level which will be collected for the test, the match_mode keyword defines how matching will be performed (the default :all checks that all logs and patterns match pairwise; use :any to check that the pattern matches at least once somewhere in the sequence.)

The most useful log pattern is a simple tuple of the form (level,message). A different number of tuple elements may be used to match other log metadata, corresponding to the arguments to passed to AbstractLogger via the handle_message function: (level,message,module,group,id,file,line). Elements which are present will be matched pairwise with the log record fields using == by default, with the special cases that Symbols may be used for the standard log levels, and Regexs in the pattern will match string or Symbol fields using occursin.

Examples

Consider a function which logs a warning, and several debug messages:

  1. function foo(n)
  2. @info "Doing foo with n=$n"
  3. for i=1:n
  4. @debug "Iteration $i"
  5. end
  6. 42
  7. end

We can test the info message using

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

If we also wanted to test the debug messages, these need to be enabled with the min_level keyword:

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

If you want to test that some particular messages are generated while ignoring the rest, you can set the keyword match_mode=:any:

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

The macro may be chained with @test to also test the returned value:

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

If you want to test for the absence of warnings, you can omit specifying log patterns and set the min_level accordingly:

  1. # test that the expression logs no messages when the logger level is warn:
  2. @test_logs min_level=Logging.Warn @info("Some information") # passes
  3. @test_logs min_level=Logging.Warn @warn("Some information") # fails

If you want to test the absence of warnings (or error messages) in stderr which are not generated by @warn, see @test_nowarn.

Test.@test_deprecated — Macro

  1. @test_deprecated [pattern] expression

When --depwarn=yes, test that expression emits a deprecation warning and return the value of expression. The log message string will be matched against pattern which defaults to r"deprecated"i.

When --depwarn=no, simply return the result of executing expression. When --depwarn=error, check that an ErrorException is thrown.

Examples

  1. # Deprecated in julia 0.7
  2. @test_deprecated num2hex(1)
  3. # The returned value can be tested by chaining with @test:
  4. @test (@test_deprecated num2hex(1)) == "0000000000000001"

Test.@test_warn — Macro

  1. @test_warn msg expr

Test whether evaluating expr results in stderr output that contains the msg string or matches the msg regular expression. If msg is a boolean function, tests whether msg(output) returns true. If msg is a tuple or array, checks that the error output contains/matches each item in msg. Returns the result of evaluating expr.

See also @test_nowarn to check for the absence of error output.

Note: Warnings generated by @warn cannot be tested with this macro. Use @test_logs instead.

Test.@test_nowarn — Macro

  1. @test_nowarn expr

Test whether evaluating expr results in empty stderr output (no warnings or other messages). Returns the result of evaluating expr.

Note: The absence of warnings generated by @warn cannot be tested with this macro. Use @test_logs instead.

Broken Tests

If a test fails consistently it can be changed to use the @test_broken macro. This will denote the test as Broken if the test continues to fail and alerts the user via an Error if the test succeeds.

Test.@test_broken — Macro

  1. @test_broken ex
  2. @test_broken f(args...) key=val ...

Indicates a test that should pass but currently consistently fails. Tests that the expression ex evaluates to false or causes an exception. Returns a Broken Result if it does, or an Error Result if the expression evaluates to true. This is equivalent to @test ex broken=true.

The @test_broken f(args...) key=val... form works as for the @test macro.

Examples

  1. julia> @test_broken 1 == 2
  2. Test Broken
  3. Expression: 1 == 2
  4. julia> @test_broken 1 == 2 atol=0.1
  5. Test Broken
  6. Expression: ==(1, 2, atol = 0.1)

@test_skip is also available to skip a test without evaluation, but counting the skipped test in the test set reporting. The test will not run but gives a Broken Result.

Test.@test_skip — Macro

  1. @test_skip ex
  2. @test_skip f(args...) key=val ...

Marks a test that should not be executed but should be included in test summary reporting as Broken. This can be useful for tests that intermittently fail, or tests of not-yet-implemented functionality. This is equivalent to @test ex skip=true.

The @test_skip f(args...) key=val... form works as for the @test macro.

Examples

  1. julia> @test_skip 1 == 2
  2. Test Broken
  3. Skipped: 1 == 2
  4. julia> @test_skip 1 == 2 atol=0.1
  5. Test Broken
  6. Skipped: ==(1, 2, atol = 0.1)

Creating Custom AbstractTestSet Types

Packages can create their own AbstractTestSet subtypes by implementing the record and finish methods. The subtype should have a one-argument constructor taking a description string, with any options passed in as keyword arguments.

Test.record — Function

  1. record(ts::AbstractTestSet, res::Result)

Record a result to a testset. This function is called by the @testset infrastructure each time a contained @test macro completes, and is given the test result (which could be an Error). This will also be called with an Error if an exception is thrown inside the test block but outside of a @test context.

Test.finish — Function

  1. finish(ts::AbstractTestSet)

Do any final processing necessary for the given testset. This is called by the @testset infrastructure after a test block executes.

Custom AbstractTestSet subtypes should call record on their parent (if there is one) to add themselves to the tree of test results. This might be implemented as:

  1. if get_testset_depth() != 0
  2. # Attach this test set to the parent test set
  3. parent_ts = get_testset()
  4. record(parent_ts, self)
  5. return self
  6. end

Test takes responsibility for maintaining a stack of nested testsets as they are executed, but any result accumulation is the responsibility of the AbstractTestSet subtype. You can access this stack with the get_testset and get_testset_depth methods. Note that these functions are not exported.

Test.get_testset — Function

  1. get_testset()

Retrieve the active test set from the task’s local storage. If no test set is active, use the fallback default test set.

Test.get_testset_depth — Function

  1. get_testset_depth()

Returns the number of active test sets, not including the default test set

Test also makes sure that nested @testset invocations use the same AbstractTestSet subtype as their parent unless it is set explicitly. It does not propagate any properties of the testset. Option inheritance behavior can be implemented by packages using the stack infrastructure that Test provides.

Defining a basic AbstractTestSet subtype might look like:

  1. import Test: Test, record, finish
  2. using Test: AbstractTestSet, Result, Pass, Fail, Error
  3. using Test: get_testset_depth, get_testset
  4. struct CustomTestSet <: Test.AbstractTestSet
  5. description::AbstractString
  6. foo::Int
  7. results::Vector
  8. # constructor takes a description string and options keyword arguments
  9. CustomTestSet(desc; foo=1) = new(desc, foo, [])
  10. end
  11. record(ts::CustomTestSet, child::AbstractTestSet) = push!(ts.results, child)
  12. record(ts::CustomTestSet, res::Result) = push!(ts.results, res)
  13. function finish(ts::CustomTestSet)
  14. # just record if we're not the top-level parent
  15. if get_testset_depth() > 0
  16. record(get_testset(), ts)
  17. end
  18. ts
  19. end

And using that testset looks like:

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

Test utilities

Test.GenericArray — Type

The GenericArray can be used to test generic array APIs that program to the AbstractArray interface, in order to ensure that functions can work with array types besides the standard Array type.

Test.GenericDict — Type

The GenericDict can be used to test generic dict APIs that program to the AbstractDict interface, in order to ensure that functions can work with associative types besides the standard Dict type.

Test.GenericOrder — Type

The GenericOrder can be used to test APIs for their support of generic ordered types.

Test.GenericSet — Type

The GenericSet can be used to test generic set APIs that program to the AbstractSet interface, in order to ensure that functions can work with set types besides the standard Set and BitSet types.

Test.GenericString — Type

The GenericString can be used to test generic string APIs that program to the AbstractString interface, in order to ensure that functions can work with string types besides the standard String type.

Test.detect_ambiguities — Function

  1. detect_ambiguities(mod1, mod2...; recursive=false, ambiguous_bottom=false)

Returns a vector of (Method,Method) pairs of ambiguous methods defined in the specified modules. Use recursive=true to test in all submodules.

ambiguous_bottom controls whether ambiguities triggered only by Union{} type parameters are included; in most cases you probably want to set this to false. See Base.isambiguous.

Test.detect_unbound_args — Function

  1. detect_unbound_args(mod1, mod2...; recursive=false)

Returns a vector of Methods which may have unbound type parameters. Use recursive=true to test in all submodules.