Unit Testing¶
Testing Base Julia¶
Julia is under rapid development and has an extensive test suite to
verify functionality across multiple platforms. If you build Julia
from source, you can run this test suite with maketest. In a
binary install, you can run the test suite using Base.runtests().
runtests([tests=["all"][, numcores=ceil(Integer, Sys.CPU_CORES / 2)]])¶Run the Julia unit tests listed in
tests, which can be either a string or an array of strings, usingnumcoresprocessors. (not exported)
Basic Unit Tests¶
The Base.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.
Simple unit testing can be performed with the @test() and @test_throws()
macros:
@test exTests that the expression
exevaluates totrue. Returns aPassResultif it does, aFailResultif it isfalse, and anErrorResultif it could not be evaluated.
@test_throws extype exTests that the expression
exthrows an exception of typeextype.
For example, suppose we want to check our new function foo(x) works
as expected:
julia>usingBase.Testjulia>foo(x)=length(x)^2foo(genericfunction with1method)If the condition is true, a Pass is returned:
julia>@testfoo("bar")==9TestPassedExpression:foo("bar")==9Evaluated:9==9julia>@testfoo("fizz")>=10TestPassedExpression:foo("fizz")>=10Evaluated:16>=10If the condition is false, then a Fail is returned and an
exception is thrown:
julia>@testfoo("f")==20TestFailedExpression:foo("f")==20Evaluated:1==20ERROR:Therewasanerrorduringtestinginrecordattest.jl:268indo_testattest.jl:191If 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:
julia>@testfoo(:cat)==1ErrorDuringTestTestthrewanexceptionoftype MethodErrorExpression:foo(:cat)==1MethodError:`length`hasnomethodmatchinglength(::Symbol)infooatnone:1inanonymousattest.jl:159indo_testattest.jl:180ERROR:Therewasanerrorduringtestinginrecordattest.jl:268indo_testattest.jl:191If we expect that evaluating an expression should throw an exception,
then we can use @test_throws() to check that this occurs:
julia>@test_throwsMethodErrorfoo(:cat)TestPassedExpression:foo(:cat)Evaluated:MethodErrorWorking with Test Sets¶
Typically a large of 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.
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.
@testset [CustomTestSet] [option=val ...] ["description"] begin ... end@testset [CustomTestSet] [option=val ...] ["description $v"] for v in (...) ... end@testset [CustomTestSet] [option=val ...] ["description $v, $w"] for v in (...), w in (...) ... endStarts a new test set, or multiple test sets if a
forloop is provided.If no custom testset type is given it defaults to creating a
DefaultTestSet.DefaultTestSetrecords all the results and, and if there are anyFails orErrors, 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@testsetinvocations. The given options are only applied to the test set where they are given. The default test set type does not take any options.The description string accepts interpolation from the loop indices. If no description is provided, one is constructed based on the variables.
By default the
@testsetmacro will return the testset object itself, though this behavior can be customized in other testset types. If aforloop is used then the macro collects and returns a list of the return values of thefinishmethod, which by default will return a list of the testset objects used in each iteration.
We can put our tests for the foo(x) function in a test set:
julia>@testset"Foo Tests"begin@testfoo("a")==1@testfoo("ab")==4@testfoo("abc")==9endTestSummary:|PassTotalFooTests|33Test sets can also be nested:
julia>@testset"Foo Tests"begin@testset"Animals"begin@testfoo("cat")==9@testfoo("dog")==foo("cat")end@testset"Arrays $i"foriin1:3@testfoo(zeros(i))==i^2@testfoo(ones(i))==i^2endendTestSummary:|PassTotalFooTests|88In the event that a nested test set has no failures, as happened here, it will be hidden in the summary. If we do have a test failure, only the details for the failed test sets will be shown:
julia>@testset"Foo Tests"begin@testset"Animals"begin@testset"Felines"begin@testfoo("cat")==9end@testset"Canines"begin@testfoo("dog")==9endend@testset"Arrays"begin@testfoo(zeros(2))==4@testfoo(ones(4))==15endendArrays:TestFailedExpression:foo(ones(4))==15Evaluated:16==15inrecordattest.jl:297indo_testattest.jl:191TestSummary:|PassFailTotalFooTests|314Animals|22Arrays|112ERROR:Sometestsdidnotpass:3passed,1failed,0errored,0broken.infinishattest.jl:362Other Test Macros¶
As calculations on floating-point values can be imprecise, you can
perform approximate equality checks using either @testa≈b
(where ≈, typed via tab completion of \approx,
is the isapprox() function) or use isapprox() directly.
An alternative is the @test_approx_eq macro (which differs from
isapprox in that it treats NaN values as equal and has a smaller
default tolerance) or @test_approx_eq_eps (which takes an extra
argument indicating the relative tolerance):
julia>@test1≈0.999999999julia>@test1≈0.999999ERROR:testfailed:1isapprox0.999999inexpression:1≈0.999999inerroraterror.jl:21indefault_handlerattest.jl:30indo_testattest.jl:53julia>@test_approx_eq1.0.999999999ERROR:assertionfailed:|1.0-0.999999999|<2.220446049250313e-121.0=1.00.999999999=0.999999999intest_approx_eqattest.jl:75intest_approx_eqattest.jl:80julia>@test_approx_eq1.0.9999999999999julia>@test_approx_eq_eps1.0.9991e-2julia>@test_approx_eq_eps1.0.9991e-3ERROR:assertionfailed:|1.0-0.999|<=0.0011.0=1.00.999=0.999difference=0.0010000000000000009>0.001inerroraterror.jl:22intest_approx_eqattest.jl:68Note that these macros will fail immediately, and are not compatible
with @testset(), so using @testisapprox is encouraged when
writing new tests.
@test_approx_eq(a, b)¶Test two floating point numbers
aandbfor equality taking into account small numerical errors.
@test_approx_eq_eps(a, b, tol)¶Test two floating point numbers
aandbfor equality taking into account a margin of tolerance given bytol.
@inferred 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 off(x)if the types match, and anErrorResultif it finds different types.julia>usingBase.Testjulia>f(a,b,c)=b>1?1:1.0f(genericfunction with1method)julia>typeof(f(1,2,3))Int64julia>@code_warntypef(1,2,3)...Body:beginunless(Base.slt_int)(1,b::Int64)::Boolgoto3return13:return1.0end::UNION{FLOAT64,INT64}julia>@inferredf(1,2,3)ERROR:returntype Int64doesnotmatchinferredreturntype Union{Float64,Int64}inerror(::String)at./error.jl:21...julia>@inferredmax(1,2)2
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_broken exIndicates a test that should pass but currently consistently fails. Tests that the expression
exevaluates tofalseor causes an exception. Returns aBrokenResultif it does, or anErrorResultif the expression evaluates totrue.
@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 BrokenResult.
@test_skip exMarks 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.
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.
record(ts::AbstractTestSet, res::Result)¶Record a result to a testset. This function is called by the
@testsetinfrastructure each time a contained@testmacro completes, and is given the test result (which could be anError). This will also be called with anErrorif an exception is thrown inside the test block but outside of a@testcontext.
finish(ts::AbstractTestSet)¶Do any final processing necessary for the given testset. This is called by the
@testsetinfrastructure after a test block executes. One common use for this function is to record the testset to the parent’s results list, usingget_testset.
Base.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.
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.
get_testset_depth()¶Returns the number of active test sets, not including the defaut test set
Base.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 Base.Test
provides.
Defining a basic AbstractTestSet subtype might look like:
importBase.Test:record,finishusingBase.Test:AbstractTestSet,Result,Pass,Fail,ErrorusingBase.Test:get_testset_depth,get_testsetimmutableCustomTestSet<:Base.Test.AbstractTestSetdescription::AbstractStringfoo::Intresults::Vector# constructor takes a description string and options keyword argumentsCustomTestSet(desc;foo=1)=new(desc,foo,[])endrecord(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 parentifget_testset_depth()>0record(get_testset(),ts)endtsendAnd using that testset looks like:
@testsetCustomTestSetfoo=4"custom testset inner 2"begin# this testset should inherit the type, but not the argument.@testset"custom testset inner"begin@testtrueendend