Unit Testing
Testing Base Julia
Julia befindet sich in einer schnellen Entwicklung und verfügt über eine umfangreiche Test-Suite, um die Funktionalität auf mehreren Plattformen zu überprüfen. Wenn Sie Julia aus dem Quellcode erstellen, können Sie diese Test-Suite mit make test
ausführen. Bei einer Binärinstallation können Sie die Test-Suite mit Base.runtests()
ausführen.
Base.runtests
— FunctionBase.runtests(tests=["all"]; ncores=ceil(Int, Sys.CPU_THREADS / 2),
exit_on_error=false, revise=false, [seed])
Führen Sie die in tests
aufgeführten Julia-Einheitstests aus, die entweder eine Zeichenfolge oder ein Array von Zeichenfolgen sein können, unter Verwendung von ncores
Prozessoren. Wenn exit_on_error
auf false
gesetzt ist, werden bei einem fehlgeschlagenen Test alle verbleibenden Tests in anderen Dateien weiterhin ausgeführt; andernfalls werden sie verworfen, wenn exit_on_error == true
. Wenn revise
auf true
gesetzt ist, wird das Revise
-Paket verwendet, um alle Änderungen an Base
oder an den Standardbibliotheken vor der Ausführung der Tests zu laden. Wenn ein Seed über das Schlüsselwort-Argument bereitgestellt wird, wird er verwendet, um den globalen RNG im Kontext, in dem die Tests ausgeführt werden, zu initialisieren; andernfalls wird der Seed zufällig gewählt.
Basic Unit Tests
Das Test
-Modul bietet einfache Unit-Testing-Funktionalität. Unit-Testing ist eine Möglichkeit, um zu überprüfen, ob Ihr Code korrekt ist, indem Sie sicherstellen, dass die Ergebnisse Ihren Erwartungen entsprechen. Es kann hilfreich sein, um sicherzustellen, dass Ihr Code nach Änderungen weiterhin funktioniert, und kann beim Entwickeln verwendet werden, um die Verhaltensweisen zu spezifizieren, die Ihr Code beim Abschluss haben sollte. Sie möchten möglicherweise auch die Dokumentation für adding tests to your Julia Package ansehen.
Einfache Unit-Tests können mit den @test
und @test_throws
Makros durchgeführt werden:
Test.@test
— Macro@test ex
@test f(args...) key=val ...
@test ex broken=true
@test ex skip=true
Testen Sie, ob der Ausdruck ex
zu true
evaluiert. Wenn er innerhalb eines @testset
ausgeführt wird, geben Sie ein Pass
Result
zurück, wenn dies der Fall ist, ein Fail
Result
, wenn es false
ist, und ein Error
Result
, wenn es nicht evaluiert werden konnte. Wenn es außerhalb eines @testset
ausgeführt wird, werfen Sie stattdessen eine Ausnahme, anstatt Fail
oder Error
zurückzugeben.
Beispiele
julia> @test true
Test Bestanden
julia> @test [1, 2] + [2, 1] == [3, 3]
Test Bestanden
Die Form @test f(args...) key=val...
ist äquivalent zu @test f(args..., key=val...)
, was nützlich sein kann, wenn der Ausdruck ein Aufruf mit Infix-Syntax ist, wie z. B. ungefähre Vergleiche:
julia> @test π ≈ 3.14 atol=0.01
Test Bestanden
Dies ist äquivalent zu dem unansehnlicheren Test @test ≈(π, 3.14, atol=0.01)
. Es ist ein Fehler, mehr als einen Ausdruck anzugeben, es sei denn, der erste ist ein Aufrufausdruck und der Rest sind Zuweisungen (k=v
).
Sie können jeden Schlüssel für die key=val
Argumente verwenden, außer für broken
und skip
, die im Kontext von @test
spezielle Bedeutungen haben:
broken=cond
zeigt einen Test an, der bestehen sollte, aber derzeit konsequent fehlschlägt, wenncond==true
. Tests, dass der Ausdruckex
zufalse
evaluiert oder eine Ausnahme verursacht. Gibt einBroken
Result
zurück, wenn dies der Fall ist, oder einError
Result
, wenn der Ausdruck zutrue
evaluiert. Der reguläre@test ex
wird evaluiert, wenncond==false
.skip=cond
kennzeichnet einen Test, der nicht ausgeführt werden sollte, aber in der Testzusammenfassungsberichterstattung alsBroken
enthalten sein sollte, wenncond==true
. Dies kann nützlich sein für Tests, die intermittierend fehlschlagen, oder Tests von noch nicht implementierten Funktionen. Der reguläre@test ex
wird evaluiert, wenncond==false
.
Beispiele
julia> @test 2 + 2 ≈ 6 atol=1 broken=true
Test Broken
Ausdruck: ≈(2 + 2, 6, atol = 1)
julia> @test 2 + 2 ≈ 5 atol=1 broken=false
Test Bestanden
julia> @test 2 + 2 == 5 skip=true
Test Broken
Übersprungen: 2 + 2 == 5
julia> @test 2 + 2 == 4 skip=false
Test Bestanden
Die Schlüsselwortargumente broken
und skip
erfordern mindestens Julia 1.7.
Test.@test_throws
— Macro@test_throws exception expr
Testet, dass der Ausdruck expr
exception
auslöst. Die Ausnahme kann entweder einen Typ, einen String, einen regulären Ausdruck oder eine Liste von Strings, die in der angezeigten Fehlermeldung vorkommen, eine Übereinstimmungsfunktion oder einen Wert angeben (der auf Gleichheit durch den Vergleich von Feldern getestet wird). Beachten Sie, dass @test_throws
keine abschließende Schlüsselwortform unterstützt.
Die Möglichkeit, etwas anderes als einen Typ oder einen Wert als exception
anzugeben, erfordert Julia v1.8 oder höher.
Beispiele
julia> @test_throws BoundsError [1, 2, 3][4]
Test bestanden
Ausgelöst: BoundsError
julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
Test bestanden
Ausgelöst: DimensionMismatch
julia> @test_throws "Versuchen Sie sqrt(Complex" sqrt(-1)
Test bestanden
Nachricht: "DomainError mit -1.0:\nsqrt wurde mit einem negativen reellen Argument aufgerufen, gibt jedoch nur ein komplexes Ergebnis zurück, wenn es mit einem komplexen Argument aufgerufen wird. Versuchen Sie sqrt(Complex(x))."
Im letzten Beispiel hätte anstelle der Übereinstimmung mit einem einzelnen String alternativ auch Folgendes verwendet werden können:
["Versuchen", "Complex"]
(eine Liste von Strings)r"Versuchen sqrt\([Cc]omplex"
(ein regulärer Ausdruck)str -> occursin("complex", str)
(eine Übereinstimmungsfunktion)
Zum Beispiel, nehmen wir an, wir möchten überprüfen, ob unsere neue Funktion foo(x)
wie erwartet funktioniert:
julia> using Test
julia> foo(x) = length(x)^2
foo (generic function with 1 method)
Wenn die Bedingung wahr ist, wird ein Pass
zurückgegeben:
julia> @test foo("bar") == 9
Test Passed
julia> @test foo("fizz") >= 10
Test Passed
Wenn die Bedingung falsch ist, wird ein Fail
zurückgegeben und eine Ausnahme ausgelöst:
julia> @test foo("f") == 20
Test Failed at none:1
Expression: foo("f") == 20
Evaluated: 1 == 20
ERROR: There was an error during testing
Wenn die Bedingung nicht ausgewertet werden konnte, weil eine Ausnahme ausgelöst wurde, was in diesem Fall geschieht, weil length
für Symbole nicht definiert ist, wird ein Error
-Objekt zurückgegeben und eine Ausnahme ausgelöst:
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
Wenn wir erwarten, dass die Auswertung eines Ausdrucks eine Ausnahme auslösen sollte, können wir @test_throws
verwenden, um zu überprüfen, dass dies geschieht:
julia> @test_throws MethodError foo(:cat)
Test Passed
Thrown: MethodError
Working with Test Sets
Typischerweise wird eine große Anzahl von Tests verwendet, um sicherzustellen, dass Funktionen über eine Reihe von Eingaben korrekt arbeiten. Im Falle eines fehlgeschlagenen Tests ist das Standardverhalten, sofort eine Ausnahme auszulösen. Es ist jedoch normalerweise vorzuziehen, zuerst die restlichen Tests auszuführen, um ein besseres Bild davon zu bekommen, wie viele Fehler im getesteten Code vorhanden sind.
Der @testset
erstellt einen eigenen lokalen Geltungsbereich, wenn die Tests darin ausgeführt werden.
Das @testset
-Makro kann verwendet werden, um Tests in Sätze zu gruppieren. Alle Tests in einem Testsatz werden ausgeführt, und am Ende des Testsatzes wird eine Zusammenfassung gedruckt. Wenn einer der Tests fehlgeschlagen ist oder aufgrund eines Fehlers nicht ausgewertet werden konnte, wird der Testsatz dann eine TestSetException
auslösen.
Test.@testset
— Macro@testset [CustomTestSet] [options...] ["Beschreibung"] begin test_ex end
@testset [CustomTestSet] [options...] ["Beschreibung $v"] for v in itr test_ex end
@testset [CustomTestSet] [options...] ["Beschreibung $v, $w"] for v in itrv, w in itrw test_ex end
@testset [CustomTestSet] [options...] ["Beschreibung"] test_func()
@testset let v = v, w = w; test_ex; end
Mit begin/end oder Funktionsaufruf
Wenn @testset verwendet wird, mit begin/end oder einem einzelnen Funktionsaufruf, startet das Makro ein neues Testset, in dem der gegebene Ausdruck ausgewertet wird.
Wenn kein benutzerdefinierter Testset-Typ angegeben ist, wird standardmäßig ein DefaultTestSet
erstellt. DefaultTestSet
zeichnet alle Ergebnisse auf und wirft, falls es Fail
s oder Error
s gibt, am Ende des obersten (nicht geschachtelten) Testsets eine Ausnahme, zusammen mit einer Zusammenfassung der Testergebnisse.
Jeder benutzerdefinierte Testset-Typ (Untertyp von AbstractTestSet
) kann angegeben werden und wird auch für alle geschachtelten @testset
-Aufrufe verwendet. Die angegebenen Optionen werden nur auf das Testset angewendet, in dem sie angegeben sind. Der Standard-Testset-Typ akzeptiert drei boolesche Optionen:
verbose
: wenntrue
, wird die Ergebnisszusammenfassung der geschachtelten Testsets angezeigt, selbst wenn sie alle bestehen (der Standardwert istfalse
).showtiming
: wenntrue
, wird die Dauer jedes angezeigten Testsets angezeigt (der Standardwert isttrue
).failfast
: wenntrue
, führt jeder Testfehler oder Fehler dazu, dass das Testset und alle untergeordneten Testsets sofort zurückgegeben werden (der Standardwert istfalse
). Dies kann auch global über die UmgebungsvariableJULIA_TEST_FAILFAST
festgelegt werden.
@testset test_func()
erfordert mindestens Julia 1.8.
failfast
erfordert mindestens Julia 1.9.
Der Beschreibungsstring akzeptiert Interpolation von den Schleifenindizes. Wenn keine Beschreibung angegeben ist, wird eine basierend auf den Variablen erstellt. Wenn ein Funktionsaufruf angegeben ist, wird dessen Name verwendet. Explizite Beschreibungsstrings überschreiben dieses Verhalten.
Standardmäßig gibt das @testset
-Makro das Testset-Objekt selbst zurück, obwohl dieses Verhalten in anderen Testset-Typen angepasst werden kann. Wenn eine for
-Schleife verwendet wird, sammelt und gibt das Makro eine Liste der Rückgabewerte der finish
-Methode zurück, die standardmäßig eine Liste der in jeder Iteration verwendeten Testset-Objekte zurückgibt.
Vor der Ausführung des Körpers eines @testset
erfolgt ein impliziter Aufruf von Random.seed!(seed)
, wobei seed
der aktuelle Seed des globalen RNG ist. Darüber hinaus wird nach der Ausführung des Körpers der Zustand des globalen RNG auf den Zustand vor dem @testset
zurückgesetzt. Dies soll die Reproduzierbarkeit im Falle eines Fehlers erleichtern und nahtlose Umstellungen von @testset
s unabhängig von ihren Nebenwirkungen auf den Zustand des globalen RNG ermöglichen.
Beispiele
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
Wenn @testset for
verwendet wird, startet das Makro einen neuen Test für jede Iteration der bereitgestellten Schleife. Die Semantik jedes Testsets ist ansonsten identisch mit der des begin/end
-Falls (als ob es für jede Schleifeniteration verwendet wird).
@testset let
Wenn @testset let
verwendet wird, startet das Makro ein transparentes Testset, wobei das gegebene Objekt als Kontextobjekt zu jedem fehlerhaften Test hinzugefügt wird, der darin enthalten ist. Dies ist nützlich, wenn eine Reihe verwandter Tests an einem größeren Objekt durchgeführt wird und es wünschenswert ist, dieses größere Objekt anzuzeigen, wenn einer der einzelnen Tests fehlschlägt. Transparente Testsets führen nicht zu zusätzlichen Ebenen der Verschachtelung in der Testset-Hierarchie und werden direkt an das übergeordnete Testset weitergegeben (mit dem Kontextobjekt, das an alle fehlerhaften Tests angehängt wird).
@testset let
erfordert mindestens Julia 1.9.
Mehrere let
-Zuweisungen werden seit Julia 1.10 unterstützt.
Beispiele
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: Es gab einen Fehler während des Tests
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: Es gab einen Fehler während des Tests
Test.TestSetException
— TypeTestSetException
Wird ausgelöst, wenn ein Testset abgeschlossen ist und nicht alle Tests bestanden wurden.
Wir können unsere Tests für die foo(x)
-Funktion in einem Testset unterbringen:
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
Test-Sets können auch geschachtelt werden:
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
Sowie Funktionen aufrufen:
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
Dies kann verwendet werden, um die Faktorisierung von Testsets zu ermöglichen, was es einfacher macht, einzelne Testsets auszuführen, indem stattdessen die zugehörigen Funktionen aufgerufen werden. Beachten Sie, dass im Fall von Funktionen das Testset den Namen der aufgerufenen Funktion erhält. Falls ein geschachteltes Testset keine Fehler aufweist, wie hier geschehen, wird es in der Zusammenfassung ausgeblendet, es sei denn, die Option verbose=true
wird übergeben:
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
Wenn wir einen Testfehler haben, werden nur die Details für die fehlgeschlagenen Testsets angezeigt:
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
Man kann das @test_logs
Makro verwenden, um Protokollanweisungen zu testen, oder ein TestLogger
verwenden.
Test.@test_logs
— Macro@test_logs [log_patterns...] [keywords] Ausdruck
Sammeln Sie eine Liste von Protokolleinträgen, die durch Ausdruck
generiert werden, indem Sie collect_test_logs
verwenden, überprüfen Sie, ob sie mit der Sequenz log_patterns
übereinstimmen, und geben Sie den Wert von Ausdruck
zurück. Die keywords
bieten eine einfache Filterung der Protokolleinträge: das min_level
-Keyword steuert das minimale Protokollniveau, das für den Test gesammelt wird, das match_mode
-Keyword definiert, wie das Matching durchgeführt wird (das Standard-:all
überprüft, ob alle Protokolle und Muster paarweise übereinstimmen; verwenden Sie :any
, um zu überprüfen, ob das Muster irgendwo in der Sequenz mindestens einmal übereinstimmt.)
Das nützlichste Protokollmuster ist ein einfaches Tupel der Form (level,message)
. Eine andere Anzahl von Tupel-Elementen kann verwendet werden, um andere Protokollmetadaten abzugleichen, die den Argumenten entsprechen, die an AbstractLogger
über die Funktion handle_message
übergeben werden: (level,message,module,group,id,file,line)
. Elemente, die vorhanden sind, werden standardmäßig paarweise mit den Feldern des Protokolleintrags unter Verwendung von ==
abgeglichen, wobei die Sonderfälle, dass Symbol
s für die Standardprotokollniveaus verwendet werden können, und Regex
s im Muster die String- oder Symbolfelder mit occursin
abgleichen.
Beispiele
Betrachten Sie eine Funktion, die eine Warnung protokolliert und mehrere Debug-Nachrichten:
function foo(n)
@info "Doing foo with n=$n"
for i=1:n
@debug "Iteration $i"
end
42
end
Wir können die Info-Nachricht testen mit
@test_logs (:info,"Doing foo with n=2") foo(2)
Wenn wir auch die Debug-Nachrichten testen möchten, müssen diese mit dem min_level
-Keyword aktiviert werden:
using Logging
@test_logs (:info,"Doing foo with n=2") (:debug,"Iteration 1") (:debug,"Iteration 2") min_level=Logging.Debug foo(2)
Wenn Sie testen möchten, dass bestimmte Nachrichten generiert werden, während der Rest ignoriert wird, können Sie das Keyword match_mode=:any
setzen:
using Logging
@test_logs (:info,) (:debug,"Iteration 42") min_level=Logging.Debug match_mode=:any foo(100)
Das Makro kann mit @test
verkettet werden, um auch den zurückgegebenen Wert zu testen:
@test (@test_logs (:info,"Doing foo with n=2") foo(2)) == 42
Wenn Sie auf das Fehlen von Warnungen testen möchten, können Sie die Angabe von Protokollmustern weglassen und das min_level
entsprechend setzen:
# testen, dass der Ausdruck keine Nachrichten protokolliert, wenn das Logger-Niveau warn ist:
@test_logs min_level=Logging.Warn @info("Some information") # besteht
@test_logs min_level=Logging.Warn @warn("Some information") # schlägt fehl
Wenn Sie das Fehlen von Warnungen (oder Fehlermeldungen) in stderr
testen möchten, die nicht durch @warn
generiert werden, siehe @test_nowarn
. ```
Test.TestLogger
— TypeTestLogger(; min_level=Info, catch_exceptions=false)
Erstellen Sie einen TestLogger
, der protokollierte Nachrichten in seinem Feld logs::Vector{LogRecord}
erfasst.
Setzen Sie min_level
, um das LogLevel
zu steuern, catch_exceptions
, um festzulegen, ob Ausnahmen, die im Rahmen der Protokollereigniserzeugung ausgelöst werden, erfasst werden sollen, und respect_maxlog
, um festzulegen, ob die Konvention befolgt werden soll, Nachrichten mit maxlog=n
für eine ganze Zahl n
höchstens n
Mal zu protokollieren.
Siehe auch: LogRecord
.
Beispiele
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
Speichert die Ergebnisse eines einzelnen Protokollereignisses. Felder:
level
: dasLogLevel
der Protokollnachrichtmessage
: der textliche Inhalt der Protokollnachricht_module
: das Modul des Protokollereignissesgroup
: die Protokollgruppe (standardmäßig der Name der Datei, die das Protokollereignis enthält)id
: die ID des Protokollereignissesfile
: die Datei, die das Protokollereignis enthältline
: die Zeile innerhalb der Datei des Protokollereignisseskwargs
: alle Schlüsselwortargumente, die an das Protokollereignis übergeben wurden
Other Test Macros
Da Berechnungen mit Fließkommawerten ungenau sein können, können Sie ungefähre Gleichheitsprüfungen entweder mit @test a ≈ b
durchführen (wobei ≈
, eingegeben über die Tab-Vervollständigung von \approx
, die isapprox
-Funktion ist) oder verwenden Sie 4d61726b646f776e2e436f64652822222c20226973617070726f782229_40726566
direkt.
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
Sie können relative und absolute Toleranzen festlegen, indem Sie die Schlüsselwortargumente rtol
und atol
von isapprox
entsprechend nach dem ≈
-Vergleich einstellen:
julia> @test 1 ≈ 0.999999 rtol=1e-5
Test Passed
Beachten Sie, dass dies kein spezifisches Merkmal von ≈
ist, sondern vielmehr ein allgemeines Merkmal des @test
-Makros: @test a <op> b key=val
wird vom Makro in @test op(a, b, key=val)
umgewandelt. Es ist jedoch besonders nützlich für ≈
-Tests.
Test.@inferred
— Macro@inferred [ErlaubterTyp] f(x)
Testet, ob der Aufrufausdruck f(x)
einen Wert des gleichen Typs zurückgibt, der vom Compiler abgeleitet wurde. Es ist nützlich, um die Typstabilität zu überprüfen.
f(x)
kann jeder Aufrufausdruck sein. Gibt das Ergebnis von f(x)
zurück, wenn die Typen übereinstimmen, und ein Error
Result
, wenn unterschiedliche Typen gefunden werden.
Optional lockert ErlaubterTyp
den Test, indem er besteht, wenn entweder der Typ von f(x)
mit dem abgeleiteten Typ modulo ErlaubterTyp
übereinstimmt oder wenn der Rückgabetyp ein Subtyp von ErlaubterTyp
ist. Dies ist nützlich, wenn man die Typstabilität von Funktionen testet, die eine kleine Vereinigung wie Union{Nothing, T}
oder Union{Missing, T}
zurückgeben.
julia> f(a) = a > 1 ? 1 : 1.0
f (generische Funktion mit 1 Methode)
julia> typeof(f(2))
Int64
julia> @code_warntype f(2)
MethodInstance für f(::Int64)
von f(a) @ Main none:1
Argumente
#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)
FEHLER: Rückgabetyp Int64 stimmt nicht mit abgeleitetem Rückgabetyp Union{Float64, Int64} überein
[...]
julia> @inferred max(1, 2)
2
julia> g(a) = a < 10 ? missing : 1.0
g (generische Funktion mit 1 Methode)
julia> @inferred g(20)
FEHLER: Rückgabetyp Float64 stimmt nicht mit abgeleitetem Rückgabetyp Union{Missing, Float64} überein
[...]
julia> @inferred Missing g(20)
1.0
julia> h(a) = a < 10 ? missing : f(a)
h (generische Funktion mit 1 Methode)
julia> @inferred Missing h(20)
FEHLER: Rückgabetyp Int64 stimmt nicht mit abgeleitetem Rückgabetyp Union{Missing, Float64, Int64} überein
[...]
Test.@test_deprecated
— Macro@test_deprecated [pattern] expression
Wenn --depwarn=yes
gesetzt ist, teste, dass expression
eine Abwertungswarnung ausgibt und gib den Wert von expression
zurück. Der Lognachricht-String wird mit pattern
verglichen, das standardmäßig auf r"deprecated"i
gesetzt ist.
Wenn --depwarn=no
gesetzt ist, gib einfach das Ergebnis der Ausführung von expression
zurück. Wenn --depwarn=error
gesetzt ist, überprüfe, dass eine ErrorException ausgelöst wird.
Beispiele
# Abgewertet in julia 0.7
@test_deprecated num2hex(1)
# Der zurückgegebene Wert kann durch Verketten mit @test getestet werden:
@test (@test_deprecated num2hex(1)) == "0000000000000001"
Test.@test_warn
— Macro@test_warn msg expr
Testen Sie, ob die Auswertung von expr
zu einer stderr
-Ausgabe führt, die den msg
-String enthält oder mit dem msg
-Regulären Ausdruck übereinstimmt. Wenn msg
eine boolesche Funktion ist, wird getestet, ob msg(output)
true
zurückgibt. Wenn msg
ein Tupel oder Array ist, wird überprüft, ob die Fehlermeldung jedes Element in msg
enthält/übereinstimmt. Gibt das Ergebnis der Auswertung von expr
zurück.
Siehe auch @test_nowarn
, um das Fehlen von Fehlermeldungen zu überprüfen.
Hinweis: Warnungen, die von @warn
erzeugt werden, können mit diesem Makro nicht getestet werden. Verwenden Sie stattdessen @test_logs
.
Test.@test_nowarn
— Macro@test_nowarn expr
Testen Sie, ob die Auswertung von expr
zu leerem stderr
Ausgabe (keine Warnungen oder andere Nachrichten) führt. Gibt das Ergebnis der Auswertung von expr
zurück.
Hinweis: Das Fehlen von Warnungen, die von @warn
erzeugt werden, kann mit diesem Makro nicht getestet werden. Verwenden Sie stattdessen @test_logs
.
Broken Tests
Wenn ein Test konstant fehlschlägt, kann er so geändert werden, dass das @test_broken
-Makro verwendet wird. Dies kennzeichnet den Test als Broken
, wenn der Test weiterhin fehlschlägt, und warnt den Benutzer über einen Error
, wenn der Test erfolgreich ist.
Test.@test_broken
— Macro@test_broken ex
@test_broken f(args...) key=val ...
Kennzeichnet einen Test, der bestehen sollte, aber derzeit konstant fehlschlägt. Testet, ob der Ausdruck ex
zu false
evaluiert oder eine Ausnahme verursacht. Gibt ein Broken
Result
zurück, wenn dies der Fall ist, oder ein Error
Result
, wenn der Ausdruck zu true
evaluiert. Dies entspricht @test ex broken=true
.
Die Form @test_broken f(args...) key=val...
funktioniert wie bei dem @test
-Makro.
Beispiele
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
ist ebenfalls verfügbar, um einen Test ohne Auswertung zu überspringen, zählt jedoch den übersprungenen Test in der Testberichterstattung. Der Test wird nicht ausgeführt, gibt jedoch ein Broken
Result
zurück.
Test.@test_skip
— Macro@test_skip ex
@test_skip f(args...) key=val ...
Markiert einen Test, der nicht ausgeführt werden soll, aber in der Testzusammenfassung als Broken
aufgeführt werden sollte. Dies kann nützlich sein für Tests, die sporadisch fehlschlagen, oder Tests von noch nicht implementierten Funktionen. Dies entspricht @test ex skip=true
.
Die Form @test_skip f(args...) key=val...
funktioniert wie bei dem @test
-Makro.
Beispiele
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 types
Test.Result
— TypeTest.Result
Alle Tests erzeugen ein Ergebnisobjekt. Dieses Objekt kann gespeichert werden oder auch nicht, abhängig davon, ob der Test Teil eines Testsets ist.
Test.Pass
— TypeTest.Pass <: Test.Result
Die Testbedingung war wahr, d.h. der Ausdruck wurde als wahr ausgewertet oder die korrekte Ausnahme wurde ausgelöst.
Test.Fail
— TypeTest.Fail <: Test.Result
Die Testbedingung war falsch, d.h. der Ausdruck wurde als falsch ausgewertet oder die korrekte Ausnahme wurde nicht ausgelöst.
Test.Error
— TypeTest.Error <: Test.Result
Die Testbedingung konnte aufgrund einer Ausnahme nicht ausgewertet werden oder sie wurde auf etwas anderes als ein Bool
ausgewertet. Im Fall von @test_broken
wird es verwendet, um anzuzeigen, dass ein unerwartetes Pass
Result
aufgetreten ist.
Test.Broken
— TypeTest.Broken <: Test.Result
Die Testbedingung ist das erwartete (fehlgeschlagene) Ergebnis eines fehlerhaften Tests oder wurde ausdrücklich mit @test_skip
übersprungen.
Creating Custom AbstractTestSet
Types
Pakete können ihre eigenen AbstractTestSet
-Untertypen erstellen, indem sie die Methoden record
und finish
implementieren. Der Untertyp sollte einen Konstruktor mit einem Argument haben, der einen Beschreibungsstring entgegennimmt, wobei alle Optionen als Schlüsselwortargumente übergeben werden.
Test.record
— Functionrecord(ts::AbstractTestSet, res::Result)
Ein Ergebnis zu einem Testset aufzeichnen. Diese Funktion wird von der @testset
-Infrastruktur jedes Mal aufgerufen, wenn ein enthaltenes @test
-Makro abgeschlossen ist, und erhält das Testergebnis (das ein Error
sein könnte). Dies wird auch mit einem Error
aufgerufen, wenn eine Ausnahme innerhalb des Testblocks, aber außerhalb eines @test
-Kontexts ausgelöst wird.
Test.finish
— Functionfinish(ts::AbstractTestSet)
Führen Sie alle erforderlichen Abschlussverarbeitungen für das gegebene Testset durch. Dies wird von der @testset
-Infrastruktur aufgerufen, nachdem ein Testblock ausgeführt wurde.
Benutzerdefinierte AbstractTestSet
-Untertypen sollten record
auf ihrem übergeordneten Element (sofern vorhanden) aufrufen, um sich dem Baum der Testergebnisse hinzuzufügen. Dies könnte wie folgt implementiert werden:
if get_testset_depth() != 0
# Fügen Sie dieses Testset dem übergeordneten Testset hinzu
parent_ts = get_testset()
record(parent_ts, self)
return self
end
Test
übernimmt die Verantwortung für die Verwaltung eines Stapels von geschachtelten Testsets während ihrer Ausführung, aber die Ergebnisakkumulation liegt in der Verantwortung des AbstractTestSet
-Subtyps. Sie können auf diesen Stapel mit den Methoden get_testset
und get_testset_depth
zugreifen. Beachten Sie, dass diese Funktionen nicht exportiert sind.
Test.get_testset
— Functionget_testset()
Rufen Sie das aktive Testset aus dem lokalen Speicher des Tasks ab. Wenn kein aktives Testset vorhanden ist, verwenden Sie das Standard-Testset als Fallback.
Test.get_testset_depth
— Functionget_testset_depth()
Gibt die Anzahl der aktiven Testsets zurück, ohne das Standard-Testset einzuschließen.
Test
stellt auch sicher, dass geschachtelte @testset
-Aufrufe denselben AbstractTestSet
-Untertyp wie ihr übergeordnetes Element verwenden, es sei denn, dies wird ausdrücklich festgelegt. Es propagiert keine Eigenschaften des Testsets. Das Verhalten der Optionenvererbung kann von Paketen mithilfe der Stapelinfrastruktur, die Test
bereitstellt, implementiert werden.
Die Definition eines grundlegenden AbstractTestSet
-Subtyps könnte folgendermaßen aussehen:
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
Und die Verwendung dieses Testsets sieht folgendermaßen aus:
@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
Um ein benutzerdefiniertes Testset zu verwenden und die aufgezeichneten Ergebnisse als Teil eines äußeren Standard-Testsets auszugeben, definieren Sie auch Test.get_test_counts
. Das könnte so aussehen:
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
Hält den Zustand für das rekursive Sammeln der Ergebnisse eines Testsets zu Anzeigezwecken.
Felder:
customized
: Ob die Funktionget_test_counts
für dasAbstractTestSet
angepasst wurde, für das dieses Zählobjekt gedacht ist. Wenn eine benutzerdefinierte Methode definiert wurde, immertrue
an den Konstruktor übergeben.passes
: Die Anzahl der bestandenen@test
-Aufrufe.fails
: Die Anzahl der fehlgeschlagenen@test
-Aufrufe.errors
: Die Anzahl der fehlerhaften@test
-Aufrufe.broken
: Die Anzahl der defekten@test
-Aufrufe.passes
: Die kumulierte Anzahl der bestandenen@test
-Aufrufe.fails
: Die kumulierte Anzahl der fehlgeschlagenen@test
-Aufrufe.errors
: Die kumulierte Anzahl der fehlerhaften@test
-Aufrufe.broken
: Die kumulierte Anzahl der defekten@test
-Aufrufe.duration
: Die Gesamtdauer, die das betreffendeAbstractTestSet
lief, als formatierterString
.
Test.get_test_counts
— Function" gettestcounts(::AbstractTestSet) -> TestCounts
Rekursive Funktion, die die Anzahl der Testergebnisse jedes Typs direkt im Testset zählt und die Gesamtzahlen über die untergeordneten Testsets summiert.
Benutzerdefinierte AbstractTestSet
sollten diese Funktion implementieren, um ihre Gesamtzahlen zu zählen und zusammen mit DefaultTestSet
anzuzeigen.
Wenn dies für ein benutzerdefiniertes TestSet
nicht implementiert ist, fällt die Ausgabe auf die Berichterstattung von x
für Fehler und ?s
für die Dauer zurück.
Test.format_duration
— Functionformat_duration(::AbstractTestSet)
Gibt einen formatierten String zurück, um die Dauer anzuzeigen, für die das Testset ausgeführt wurde.
Wenn nicht definiert, fällt es auf "?s"
zurück.
Test.print_test_results
— Functionprint_test_results(ts::AbstractTestSet, depth_pad=0)
Druckt die Ergebnisse eines AbstractTestSet
als formatierte Tabelle aus.
depth_pad
bezieht sich darauf, wie viel Padding vor allen Ausgaben hinzugefügt werden soll.
Wird innerhalb von Test.finish
aufgerufen, wenn das finish
te Testset das oberste Testset ist.
Test utilities
Test.GenericArray
— TypeDas GenericArray
kann verwendet werden, um generische Array-APIs zu testen, die auf das AbstractArray
-Interface abzielen, um sicherzustellen, dass Funktionen auch mit Array-Typen neben dem Standard-Array
-Typ arbeiten können.
Test.GenericDict
— TypeDer GenericDict
kann verwendet werden, um generische Dict-APIs zu testen, die auf das AbstractDict
-Interface abzielen, um sicherzustellen, dass Funktionen auch mit assoziativen Typen neben dem Standardtyp Dict
arbeiten können.
Test.GenericOrder
— TypeDer GenericOrder
kann verwendet werden, um APIs auf ihre Unterstützung für generische geordnete Typen zu testen.
Test.GenericSet
— TypeDas GenericSet
kann verwendet werden, um generische Set-APIs zu testen, die auf das AbstractSet
-Interface abzielen, um sicherzustellen, dass Funktionen auch mit Set-Typen neben den Standardtypen Set
und BitSet
funktionieren.
Test.GenericString
— TypeDer GenericString
kann verwendet werden, um generische String-APIs zu testen, die auf das AbstractString
-Interface abzielen, um sicherzustellen, dass Funktionen mit String-Typen neben dem Standard-String
-Typ arbeiten können.
Test.detect_ambiguities
— Functiondetect_ambiguities(mod1, mod2...; recursive=false,
ambiguous_bottom=false,
allowed_undefineds=nothing)
Gibt ein Vektor von (Method,Method)
-Paaren von mehrdeutigen Methoden zurück, die in den angegebenen Modulen definiert sind. Verwenden Sie recursive=true
, um in allen Untermodulen zu testen.
ambiguous_bottom
steuert, ob Mehrdeutigkeiten, die nur durch Union{}
-Typparameter ausgelöst werden, einbezogen werden; in den meisten Fällen möchten Sie dies wahrscheinlich auf false
setzen. Siehe Base.isambiguous
.
Siehe Test.detect_unbound_args
für eine Erklärung von allowed_undefineds
.
allowed_undefineds
erfordert mindestens Julia 1.8.
Test.detect_unbound_args
— Functiondetect_unbound_args(mod1, mod2...; recursive=false, allowed_undefineds=nothing)
Gibt einen Vektor von Method
s zurück, die möglicherweise ungebundene Typparameter haben. Verwenden Sie recursive=true
, um in allen Untermodulen zu testen.
Standardmäßig lösen alle undefinierten Symbole eine Warnung aus. Diese Warnung kann unterdrückt werden, indem eine Sammlung von GlobalRef
s bereitgestellt wird, für die die Warnung übersprungen werden kann. Zum Beispiel, wenn Sie
allowed_undefineds = Set([GlobalRef(Base, :active_repl),
GlobalRef(Base, :active_repl_backend)])
setzen, würden Warnungen über Base.active_repl
und Base.active_repl_backend
unterdrückt.
allowed_undefineds
erfordert mindestens Julia 1.8.
Workflow for Testing Packages
Mit den uns in den vorherigen Abschnitten zur Verfügung stehenden Werkzeugen, hier ist ein möglicher Arbeitsablauf zur Erstellung eines Pakets und zum Hinzufügen von Tests dazu.
Generating an Example Package
Für diesen Workflow werden wir ein Paket namens Example
erstellen:
pkg> generate Example
shell> cd Example
shell> mkdir test
pkg> activate .
Creating Sample Functions
Die wichtigste Voraussetzung für das Testen eines Pakets ist, dass es Funktionalität zum Testen gibt. Dazu werden wir einige einfache Funktionen zu Example
hinzufügen, die wir testen können. Fügen Sie Folgendes zu src/Example.jl
hinzu:
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
Navigieren Sie im Stammverzeichnis des Example
-Pakets zum test
-Verzeichnis, aktivieren Sie dort eine neue Umgebung und fügen Sie das Test
-Paket zur Umgebung hinzu:
shell> cd test
pkg> activate .
(test) pkg> add Test
Testing Our Package
Jetzt sind wir bereit, Tests zu Example
hinzuzufügen. Es ist gängige Praxis, eine Datei im test
-Verzeichnis mit dem Namen runtests.jl
zu erstellen, die die Testsets enthält, die wir ausführen möchten. Legen Sie diese Datei im test
-Verzeichnis an und fügen Sie den folgenden Code hinzu:
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
Wir müssen die beiden enthaltenen Dateien math_tests.jl
und greeting_tests.jl
erstellen und einige Tests hinzufügen.
Hinweis: Beachten Sie, dass wir nicht angeben mussten,
Example
in dietest
-Umgebung vonProject.toml
aufzunehmen. Dies ist ein Vorteil des Testsystems von Julia, dass Sie read about more here hinzufügen konnten.
Writing Tests for math_tests.jl
Mit unserem Wissen über Test.jl
sind hier einige Beispieltests, die wir zu math_tests.jl
hinzufügen könnten:
@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
Mit unserem Wissen über Test.jl
sind hier einige Beispieltests, die wir zu greeting_tests.jl
hinzufügen könnten:
@testset "Testset 3" begin
@test "Hello world!" == greet()
@test_throws MethodError greet("Antonia")
end
Testing Our Package
Jetzt, da wir unsere Tests und unser runtests.jl
-Skript im test
-Verzeichnis hinzugefügt haben, können wir unser Example
-Paket testen, indem wir zum Stammverzeichnis der Example
-Paketumgebung zurückkehren und die Example
-Umgebung reaktivieren:
shell> cd ..
pkg> activate .
Von dort aus können wir schließlich unser Testpaket wie folgt ausführen:
(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
Und wenn alles korrekt verlief, sollten Sie eine ähnliche Ausgabe wie oben sehen. Mit Test.jl
können kompliziertere Tests für Pakete hinzugefügt werden, aber dies sollte idealerweise die Entwickler in die Richtung weisen, wie sie mit dem Testen ihrer eigenen erstellten Pakete beginnen können.
Code Coverage
Die Verfolgung der Codeabdeckung während der Tests kann mit dem pkg> test --coverage
-Flag aktiviert werden (oder auf einer niedrigeren Ebene mit dem --code-coverage
Julia-Argument). Dies ist standardmäßig im julia-runtest GitHub-Action aktiviert.
To evaluate coverage either manually inspect the .cov
files that are generated beside the source files locally, or in CI use the julia-processcoverage GitHub action.
Seit Julia 1.11 wird die Abdeckung während der Phase der Paketvorkompilierung nicht mehr erfasst.