Julia Language
Testów jednostkowych
Szukaj…
Składnia
- @test [expr]
- @test_throws [Exception] [expr]
- @testset „[name]” start; [testy]; koniec
- Pkg.test ([pakiet])
Uwagi
Standardowa dokumentacja biblioteki dla Base.Test
obejmuje dodatkowe materiały poza tymi pokazanymi w tych przykładach.
Testowanie pakietu
Aby uruchomić testy jednostkowe dla pakietu, użyj funkcji Pkg.test
. W przypadku pakietu o nazwie MyPackage
będzie to polecenie
julia> Pkg.test("MyPackage")
Oczekiwany wynik byłby podobny do
INFO: Computing test dependencies for MyPackage...
INFO: Installing BaseTestNext v0.2.2
INFO: Testing MyPackage
Test Summary: | Pass Total
Data | 66 66
Test Summary: | Pass Total
Monetary | 107 107
Test Summary: | Pass Total
Basket | 47 47
Test Summary: | Pass Total
Mixed | 13 13
Test Summary: | Pass Total
Data Access | 35 35
INFO: MyPackage tests passed
INFO: Removing BaseTestNext v0.2.2
choć oczywiście nie można oczekiwać, że będzie dokładnie pasować do powyższego, ponieważ różne pakiety używają różnych frameworków.
To polecenie uruchamia plik test/runtests.jl
w czystym środowisku.
Można przetestować wszystkie zainstalowane pakiety jednocześnie
julia> Pkg.test()
ale zwykle zajmuje to bardzo dużo czasu.
Pisanie prostego testu
Testy jednostkowe są deklarowane w pliku test/runtests.jl
w pakiecie. Zazwyczaj ten plik się zaczyna
using MyModule
using Base.Test
Podstawową jednostką testowania jest makro @test
. To makro jest swego rodzaju stwierdzeniem. Dowolne wyrażenie logiczne można przetestować w makrze @test
:
@test 1 + 1 == 2
@test iseven(10)
@test 9 < 10 || 10 < 9
Możemy wypróbować makro @test
w REPL:
julia> using Base.Test
julia> @test 1 + 1 == 2
Test Passed
Expression: 1 + 1 == 2
Evaluated: 2 == 2
julia> @test 1 + 1 == 3
Test Failed
Expression: 1 + 1 == 3
Evaluated: 2 == 3
ERROR: There was an error during testing
in record(::Base.Test.FallbackTestSet, ::Base.Test.Fail) at ./test.jl:397
in do_test(::Base.Test.Returned, ::Expr) at ./test.jl:281
Makra testowego można używać niemal wszędzie, na przykład w pętlach lub funkcjach:
# For positive integers, a number's square is at least as large as the number
for i in 1:10
@test i^2 ≥ i
end
# Test that no two of a, b, or c share a prime factor
function check_pairwise_coprime(a, b, c)
@test gcd(a, b) == 1
@test gcd(a, c) == 1
@test gcd(b, c) == 1
end
check_pairwise_coprime(10, 23, 119)
Pisanie zestawu testowego
W wersji v0.5 zestawy testowe są wbudowane w standardowy moduł biblioteki Base.Test
i nie trzeba nic specjalnego robić (oprócz using Base.Test
), aby z nich korzystać.
Zestawy testowe nie są częścią biblioteki Base.Test
Julii Base.Test
. Zamiast tego, trzeba REQUIRE
się BaseTestNext
moduł i dodać using BaseTestNext
do pliku. Aby obsługiwać zarówno wersję 0.4, jak i 0.5, możesz użyć
if VERSION ≥ v"0.5.0-dev+7720"
using Base.Test
else
using BaseTestNext
const Test = BaseTestNext
end
Pomocne jest grupowanie powiązanych @test
w zestawie testowym. Oprócz bardziej przejrzystej organizacji testów, zestawy testowe oferują lepszą wydajność i więcej możliwości dostosowywania.
Aby zdefiniować zestaw testowy, po prostu @test
dowolną liczbę @test
@testset
blokiem @testset
:
@testset "+" begin
@test 1 + 1 == 2
@test 2 + 2 == 4
end
@testset "*" begin
@test 1 * 1 == 1
@test 2 * 2 == 4
end
Uruchomienie tych zestawów testowych powoduje wydrukowanie następujących danych wyjściowych:
Test Summary: | Pass Total
+ | 2 2
Test Summary: | Pass Total
* | 2 2
Nawet jeśli zestaw testowy zawiera test zakończony niepowodzeniem, cały zestaw testowy zostanie uruchomiony do końca, a awarie zostaną zarejestrowane i zgłoszone:
@testset "-" begin
@test 1 - 1 == 0
@test 2 - 2 == 1
@test 3 - () == 3
@test 4 - 4 == 0
end
Uruchomienie tego zestawu testowego powoduje
-: Test Failed
Expression: 2 - 2 == 1
Evaluated: 0 == 1
in record(::Base.Test.DefaultTestSet, ::Base.Test.Fail) at ./test.jl:428
...
-: Error During Test
Test threw an exception of type MethodError
Expression: 3 - () == 3
MethodError: no method matching -(::Int64, ::Tuple{})
...
Test Summary: | Pass Fail Error Total
- | 2 1 1 4
ERROR: Some tests did not pass: 2 passed, 1 failed, 1 errored, 0 broken.
...
Zestawy testowe można zagnieżdżać, co pozwala na dowolną głęboką organizację
@testset "Int" begin
@testset "+" begin
@test 1 + 1 == 2
@test 2 + 2 == 4
end
@testset "-" begin
@test 1 - 1 == 0
end
end
Jeśli testy przejdą pomyślnie, zostaną wyświetlone tylko wyniki dla najbardziej zewnętrznego zestawu testowego:
Test Summary: | Pass Total
Int | 3 3
Ale jeśli testy zakończą się niepowodzeniem, zgłaszane jest zebranie dokładnego zestawu testów i testu powodującego błąd.
@testset
można używać z pętlą for
do tworzenia wielu zestawów testowych jednocześnie:
@testset for i in 1:5
@test 2i == i + i
@test i^2 == i * i
@test i ÷ i == 1
end
które raporty
Test Summary: | Pass Total
i = 1 | 3 3
Test Summary: | Pass Total
i = 2 | 3 3
Test Summary: | Pass Total
i = 3 | 3 3
Test Summary: | Pass Total
i = 4 | 3 3
Test Summary: | Pass Total
i = 5 | 3 3
Powszechną strukturą jest posiadanie zewnętrznych zestawów testowych komponentów lub typów testowych. W ramach tych zewnętrznych zestawów testowych testy zachowania wewnętrznych zestawów testowych. Załóżmy na przykład, że stworzyliśmy typ UniversalSet
z instancją singleton, która zawiera wszystko. Zanim jeszcze zaimplementujemy ten typ, możemy zastosować zasady programistyczne oparte na testach i zaimplementować testy:
@testset "UniversalSet" begin
U = UniversalSet.instance
@testset "egal/equal" begin
@test U === U
@test U == U
end
@testset "in" begin
@test 1 in U
@test "Hello World" in U
@test Int in U
@test U in U
end
@testset "subset" begin
@test Set() ⊆ U
@test Set(["Hello World"]) ⊆ U
@test Set(1:10) ⊆ U
@test Set([:a, 2.0, "w", Set()]) ⊆ U
@test U ⊆ U
end
end
Następnie możemy rozpocząć wdrażanie naszej funkcjonalności, dopóki nie przejdzie ona pomyślnie naszych testów. Pierwszym krokiem jest zdefiniowanie typu:
immutable UniversalSet <: Base.AbstractSet end
Tylko dwa z naszych testów przeszły teraz. Możemy wdrożyć in
:
immutable UniversalSet <: Base.AbstractSet end
Base.in(x, ::UniversalSet) = true
To sprawia, że niektóre z naszych testów podzestawów muszą przejść pomyślnie. issubset
( ⊆
) nie działa jednak w UniversalSet
, ponieważ zastępowanie próbuje iterować elementy, czego nie możemy zrobić. Możemy po prostu zdefiniować specjalizację, która sprawi, że issubset
zwróci wartość true
dla dowolnego zestawu:
immutable UniversalSet <: Base.AbstractSet end
Base.in(x, ::UniversalSet) = true
Base.issubset(x::Base.AbstractSet, ::UniversalSet) = true
A teraz wszystkie nasze testy przeszły pomyślnie!
Testowanie wyjątków
Wyjątki napotkane podczas uruchamiania testu zakończą się niepowodzeniem, a jeśli test nie znajduje się w zestawie testowym, zakończ silnik testowy. Zwykle jest to dobra rzecz, ponieważ w większości sytuacji wyjątki nie są pożądanym rezultatem. Ale czasami chce się konkretnie przetestować, czy powstaje pewien wyjątek. @test_throws
makro @test_throws
.
julia> @test_throws BoundsError [1, 2, 3][4]
Test Passed
Expression: ([1,2,3])[4]
Thrown: BoundsError
Jeśli @test_throws
zostanie niewłaściwy wyjątek, @test_throws
nadal się nie powiedzie:
julia> @test_throws TypeError [1, 2, 3][4]
Test Failed
Expression: ([1,2,3])[4]
Expected: TypeError
Thrown: BoundsError
ERROR: There was an error during testing
in record(::Base.Test.FallbackTestSet, ::Base.Test.Fail) at ./test.jl:397
in do_test_throws(::Base.Test.Threw, ::Expr, ::Type{T}) at ./test.jl:329
a jeśli nie zostanie @test_throws
żaden wyjątek, @test_throws
również się nie powiedzie:
julia> @test_throws BoundsError [1, 2, 3, 4][4]
Test Failed
Expression: ([1,2,3,4])[4]
Expected: BoundsError
No exception thrown
ERROR: There was an error during testing
in record(::Base.Test.FallbackTestSet, ::Base.Test.Fail) at ./test.jl:397
in do_test_throws(::Base.Test.Returned, ::Expr, ::Type{T}) at ./test.jl:329
Testowanie przybliżonej równości zmiennoprzecinkowej
O co chodzi z następującymi?
julia> @test 0.1 + 0.2 == 0.3
Test Failed
Expression: 0.1 + 0.2 == 0.3
Evaluated: 0.30000000000000004 == 0.3
ERROR: There was an error during testing
in record(::Base.Test.FallbackTestSet, ::Base.Test.Fail) at ./test.jl:397
in do_test(::Base.Test.Returned, ::Expr) at ./test.jl:281
Błąd jest spowodowany faktem, że żadne z 0.1
, 0.2
i 0.3
są reprezentowane w komputerze jako dokładnie te wartości - 1//10
, 2//10
i 3//10
. Zamiast tego są one aproksymowane przez wartości, które są bardzo zbliżone. Ale jak widać w powyższym niepowodzeniu testu, po dodaniu dwóch przybliżeń razem wynik może być nieco gorszy niż to możliwe. Jest o wiele więcej w tym temacie , czego nie można tutaj omówić.
Ale nie brakuje nam szczęścia! Aby sprawdzić, czy połączenie zaokrąglania do liczby zmiennoprzecinkowej i arytmetyki zmiennoprzecinkowej jest w przybliżeniu prawidłowe, nawet jeśli nie jest dokładne, możemy użyć funkcji isapprox
(która odpowiada operatorowi ≈
). Możemy więc przepisać nasz test jako
julia> @test 0.1 + 0.2 ≈ 0.3
Test Passed
Expression: 0.1 + 0.2 ≈ 0.3
Evaluated: 0.30000000000000004 isapprox 0.3
Oczywiście, jeśli nasz kod był całkowicie niepoprawny, test nadal wykryje, że:
julia> @test 0.1 + 0.2 ≈ 0.4
Test Failed
Expression: 0.1 + 0.2 ≈ 0.4
Evaluated: 0.30000000000000004 isapprox 0.4
ERROR: There was an error during testing
in record(::Base.Test.FallbackTestSet, ::Base.Test.Fail) at ./test.jl:397
in do_test(::Base.Test.Returned, ::Expr) at ./test.jl:281
Funkcja isapprox
wykorzystuje heurystykę opartą na wielkości liczb i precyzji typu zmiennoprzecinkowego, aby określić wielkość błędu do tolerowania. Nie jest odpowiedni dla wszystkich sytuacji, ale działa w większości i oszczędza dużo wysiłku przy wdrażaniu własnej wersji isapprox
.