Модульное тестирование
Тестирование Base Julia
Julia находится в процессе быстрой разработки и располагает обширным набором тестов для проверки функциональности на различных платформах. Если вы выполняете сборку Julia из исходного кода, этот набор тестов можно запустить с помощью make test
. При бинарной установке набор тестов можно запустить с помощью Base.runtests()
.
#
Base.runtests
— Function
Base.runtests(tests=["all"]; ncores=ceil(Int, Sys.CPU_THREADS / 2),
exit_on_error=false, revise=false, [seed])
Выполняет модульные тесты Julia, перечисленные в tests
, который может быть как строкой, так и массивом строк, используя процессоры ncores
. Если exit_on_error
имеет значение false
, когда один тест завершается сбоем, все остальные тесты в других файлах по-прежнему будут выполняться. Они будут удалены, если exit_on_error == true
. Если revise
имеет значение true
, пакет Revise
используется для загрузки любых изменений Base
или стандартных библиотек перед запуском тестов. Если с помощью именованного аргумента указано начальное значение, оно используется для заполнения глобального генератора случайных чисел в контексте выполнения тестов. В противном случае значение выбирается случайным образом.
Базовые модульные тесты
Модуль Test
обеспечивает простую функциональность модульного тестирования. С помощью модульного тестирования можно убедиться в правильности кода, проверив соответствие результатов ожиданиям. Этот вид тестирования может быть полезен для обеспечения работоспособности кода после внесения в него изменений, а также использоваться при разработке как способ указания поведения, которым в итоге должен обладать код. Возможно, вам также будет интересно ознакомиться с документацией по добавлению тестов в пакет Julia.
Простейшее модульное тестирование можно выполнить с помощью макросов @test
и @test_throws
:
#
Test.@test
— Macro
@test ex
@test f(args...) key=val ...
@test ex broken=true
@test ex skip=true
Проверяет, равно ли выражение ex
true
. Если выполняется внутри макроса @testset
, возвращает Result
Pass
, если проверка пройдена успешно, Result
Fail
, если результат равен false
, и Result
Error
, если выражение не удалось вычислить. При выполнении вне макроса @testset
вызывает исключение вместо возврата Fail
или Error
.
Примеры
julia> @test true
Test Passed
julia> @test [1, 2] + [2, 1] == [3, 3]
Test Passed
Форма @test f(args...) key=val...
эквивалентна написанию @test f(args..., key=val...)
, что может быть полезно, когда выражение является вызовом, использующим инфиксный суффикс, например при приближенном сравнении:
julia> @test π ≈ 3.14 atol=0.01
Test Passed
Это эквивалентно неуклюжему тесту @test ≈(π, 3.14, atol=0.01)
. Предоставление более одного выражения является ошибкой, если только первое из них не является выражением вызова, а остальные — присваиваниями (k=v
).
Для аргументов key=val
можно использовать любой ключ, кроме broken
и skip
, которые имеют особое значение в контексте @test
:
-
broken=cond
указывает на тест, который должен быть выполнен, но на данный момент постоянно завершается сбоем приcond==true
. Проверяет, равно ли выражениеex
false
, или вызывает исключение. ВозвращаетResult
Broken
, если это так, илиResult
Error
, если выражение равноtrue
. Регулярное выражение@test ex
вычисляется, когдаcond==false
. -
skip=cond
помечает тест, который не должен выполняться, но должен быть включен в сводный отчет по тестам, какBroken
, когдаcond==true
. Это может быть полезно для тестов, которые периодически завершаются сбоем, или тестов еще не реализованной функциональности. Регулярное выражение@test ex
вычисляется, когдаcond==false
.
Примеры
julia> @test 2 + 2 ≈ 6 atol=1 broken=true
Test Broken
Expression: ≈(2 + 2, 6, atol = 1)
julia> @test 2 + 2 ≈ 5 atol=1 broken=false
Test Passed
julia> @test 2 + 2 == 5 skip=true
Test Broken
Skipped: 2 + 2 == 5
julia> @test 2 + 2 == 4 skip=false
Test Passed
Совместимость: Julia 1.7
Для именованных аргументов |
#
Test.@test_throws
— Macro
@test_throws exception expr
Проверяет, вызывает ли выражение expr
исключение exception
. В исключении может указываться тип, строка, регулярное выражение, список строк в выводимом сообщении об ошибке или значение (которое будет проверяться на равенство путем сравнения полей). Обратите внимание, что @test_throws
не поддерживает форму с ключевым словом в конце.
Совместимость: Julia 1.8
Для возможности указания в качестве |
Примеры
julia> @test_throws BoundsError [1, 2, 3][4]
Test Passed
Thrown: BoundsError
julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
Test Passed
Thrown: DimensionMismatch
julia> @test_throws "Try sqrt(Complex" sqrt(-1)
Test Passed
Message: "DomainError with -1.0:\nsqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try 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
Работа с наборами тестов
Обычно используется большое количество тестов, позволяющих убедиться в корректной работе функций с различными входными данными. В случае сбоя теста по умолчанию немедленно возникает исключение. Однако обычно сначала рекомендуется выполнить остальные тесты, чтобы получить более полное представление о том, сколько ошибок содержится в тестируемом коде.
Макрос |
Макрос @testset
можно использовать для группировки тестов в наборы. Будут выполнены все тесты из набора тестов, а в конце набора тестов будет выведена сводка. В случае сбоя какого-либо теста или невозможности выполнить оценку из-за ошибки набор тестов выдаст исключение TestSetException
.
#
Test.@testset
— Macro
@testset [CustomTestSet] [options...] ["description"] begin test_ex end
@testset [CustomTestSet] [options...] ["description $v"] for v in itr test_ex end
@testset [CustomTestSet] [options...] ["description $v, $w"] for v in itrv, w in itrw test_ex end
@testset [CustomTestSet] [options...] ["description"] 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
.
Совместимость: Julia 1.8
Для |
Совместимость: Julia 1.9
Для |
Строка описания принимает интерполяцию от индексов цикла. Если описание отсутствует, оно формируется на основе переменных. Если указан вызов функции, будет использоваться имя функции. Это поведение можно переопределить строками явного описания.
По умолчанию макрос @testset
возвратит сам объект набора тестов, хотя это поведение можно настроить в других типах набора тестов. Если используется цикл for
, макрос собирает и возвращает список возвращаемых значений метода finish
, который по умолчанию возвращает список объектов набора тестов, используемых в каждой итерации.
Перед выполнением тела @testset
происходит неявный вызов Random.seed!(seed)
, где seed
является текущим начальным значением глобального генератора случайных чисел. Более того, после выполнения тела состояние глобального RNG восстанавливается до того, каким оно было до @testset
. Это сделано для того, чтобы упростить воспроизводимость в случае сбоя и обеспечить беспрепятственную перестановку @testset
независимо от их побочного влияния на состояние глобального RNG.
Примеры
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
При использовании @testset for
макрос запускает новый тест в каждой итерации предоставленного цикла. В остальном семантика каждого набора тестов идентична случаю begin/end
(как если бы он имел место в каждой итерации цикла).
@testset let
При использовании @testset let
макрос запускает прозрачный набор тестов с добавлением указанного объекта в качестве объекта контекста для каждого непройденного теста в наборе. Это полезно при выполнении набора связанных тестов для одного большого объекта, если нужно, чтобы этот объект выводился при непрохождении каждого отдельного теста. Прозрачные наборы тестов не вводят дополнительные уровни вложения в иерархию наборов тестов и передаются напрямую в родительский набор тестов (с добавлением объекта контекста для каждого непройденного теста).
Совместимость: Julia 1.9
Для |
Совместимость: Julia 1.10
Несколько присваиваний |
Примеры
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: There was an error during testing
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: There was an error during testing
#
Test.TestSetException
— Type
TestSetException
Возникает, когда набор тестов завершается и не все тесты пройдены.
Мы можем поместить тесты для функции 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.
Тестирование операторов журналов
Для тестирования операторов журналов можно использовать макрос @test_logs
или воспользоваться средством TestLogger
.
#
Test.@test_logs
— Macro
@test_logs [log_patterns...] [keywords] expression
Собирает список записей журнала, созданных expression
с помощью collect_test_logs
, проверяет их соответствие последовательности log_patterns
и возвращает значение выражения expression
. Именованные аргументы keywords
обеспечивают простую фильтрацию записей журнала: min_level
управляет минимальным уровнем ведения журнала, который будет собираться для теста, match_mode
определяет, как будет осуществляться сопоставление (:all
по умолчанию проверяет попарное соответствие всех журналов и шаблонов; используйте :any
для проверки того, что шаблон совпадает хотя бы один раз в последовательности).
Наиболее полезным шаблоном журнала является простой кортеж вида (level,message)
. Для соответствия другим метаданным журнала может использоваться другое количество элементов кортежа, соответствующее аргументам, передаваемым в AbstractLogger
с помощью функции handle_message
: (level,message,module,group,id,file,line)
. Имеющиеся элементы будут попарно сопоставляться с полями записи журнала с использованием ==
по умолчанию, с теми особыми случаями, когда для стандартных уровней ведения журнала могут использоваться символы (Symbol
), а регулярные выражения (Regex
) в шаблоне будут соответствовать строковым или символьным полям, используя occursin
.
Примеры
Рассмотрим функцию, которая записывает в журнал предупреждение и несколько отладочных сообщений.
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") # passes @test_logs min_level=Logging.Warn @warn("Some information") # fails
Если требуется протестировать отсутствие предупреждений (или сообщений об ошибках) в stderr
, которые не генерируются макросом @warn
, см. описание @test_nowarn
.
#
Test.TestLogger
— Type
TestLogger(; 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
— Type
LogRecord
Хранит результаты одного события журнала. Поля:
-
level
:LogLevel
сообщения журнала -
message
: текстовое содержимое сообщения журнала -
_module
: модуль события журнала -
group
: группа ведения журнала (по умолчанию — имя файла, содержащего событие журнала) -
id
: ИД события журнала -
file
: файл, содержащий событие журнала -
line
: строка в файле или событии журнала -
kwargs
: любые именованные аргументы, переданные событию журнала
Другие макросы тестирования
Поскольку вычисления со значениями с плавающей запятой могут быть неточными, можно выполнить приближенную проверку равенства, используя либо @test a ≈ b
(где ≈
, введенный путем автозаполнения по нажатию клавиши TAB \approx
, является функцией isapprox
, либо непосредственно isapprox
.
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
Вы можете задать относительные и абсолютные допуски, задав именованные аргументы rtol
и atol
функции isapprox
, соответственно, после сравнения ≈
:
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 [AllowedType] f(x)
Проверяет, возвращает ли выражение вызова f(x)
значение того же типа, что и выведенный компилятором. Это полезно для проверки стабильности типов.
f(x)
может быть любым выражением вызова. Возвращает результат f(x)
, если типы совпадают, и Result
Error
, если типы разные.
Кроме того, AllowedType
ослабляет тест, делая его выполненным, если либо тип f(x)
совпадает с выведенным типом по модулю AllowedType
, либо возвращаемый тип является подтипом AllowedType
. Это удобно при тестировании устойчивости типов функций, возвращающих небольшое объединение, например 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, если не %1
2 ─ return 1
3 ─ return 1.0
julia> @inferred f(2)
ERROR: return type Int64 does not match inferred return type 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)
ERROR: return type Float64 does not match inferred return type 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)
ERROR: return type Int64 does not match inferred return type Union{Missing, Float64, Int64}
[...]
#
Test.@test_deprecated
— Macro
@test_deprecated [pattern] expression
Когда --depwarn=yes
, проверяет, выдает ли выражение expression
предупреждение о выводе из использования, и возвращает значение expression
. Строка сообщения журнала будет сопоставлена с шаблоном pattern
, имеющим по умолчанию значение r"deprecated"i
.
Когда --depwarn=no
, просто возвращает результат выполнения выражения expression
. Когда --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
выходные данные stderr
, содержащие строку msg
или соответствующие регулярному выражению msg
. Если 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
.
Неработающие тесты
Если тест постоянно завершается сбоем, его можно изменить так, чтобы в нем использовался макрос @test_broken
. При этом тест будет обозначен как Broken
, если он продолжает завершаться ошибкой, и будет указан как Error
, если он выполнен успешно.
#
Test.@test_broken
— Macro
@test_broken ex
@test_broken f(args...) key=val ...
Указывает на тест, который должен быть пройден, но на данный момент постоянно завершается неудачно. Проверяет, равно ли выражение ex
false
, или вызывает исключение. Возвращает Result
Broken
, если это так, или Result
Error
, если выражение равно true
. Это эквивалентно @test ex broken=true
.
Форма @test_broken f(args...) key=val...
работает так же, как и для макроса @test
.
Примеры
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
позволяет пропускать тест без оценки, но пропущенный тест будет упомянут в отчете о наборе тестов. Тест не выполняется, а выдает 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
Test Broken
Skipped: 1 == 2
julia> @test_skip 1 == 2 atol=0.1
Test Broken
Skipped: ==(1, 2, atol = 0.1)
Типы результатов тестов
#
Test.Result
— Type
Test.Result
Все тесты выдают объект результата. Этот объект может сохраняться или нет в зависимости от того, является ли тест частью тестового набора.
#
Test.Pass
— Type
Test.Pass <: Test.Result
Условие теста было выполнено, т. е. выражение было вычислено как true или было выдано правильное исключение.
#
Test.Fail
— Type
Test.Fail <: Test.Result
Условие теста не было выполнено, т. е. выражение было вычислено как false или не было выдано правильное исключение.
#
Test.Broken
— Type
Test.Broken <: Test.Result
Для условия теста получен ожидаемый (неудачный) результат, либо оно было явно пропущено с помощью @test_skip
.
Создание пользовательских типов AbstractTestSet
Пакеты могут создавать собственные подтипы AbstractTestSet
, реализуя методы record
и finish
. Подтип должен иметь одноаргументный конструктор, принимающий строку описания, а любые параметры передаются в качестве именованных аргументов.
#
Test.record
— Function
record(ts::AbstractTestSet, res::Result)
Записывает результат в набор тестов. Эта функция вызывается инфраструктурой @testset
при каждом завершении вложенного макроса @test
и получает результат тестирования (которым может быть ошибка Error
). Эта функция также будет вызвана с ошибкой (Error
), если исключение возникло внутри тестового блока, но вне контекста @test
.
#
Test.finish
— Function
finish(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
— Function
get_testset()
Получает активный набор тестов из локального хранилища задачи. Если активный набор тестов отсутствует, использует резервный набор тестов.
#
Test.get_testset_depth
— Function
get_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
— Type
TestCounts
Сохраняет состояние для рекурсивного сбора результатов набора тестов для отображения.
Поля:
-
customized
: была ли функцияget_test_counts
настроена для набораAbstractTestSet
, для которого предназначен этот объект counts. Если определен пользовательский метод, всегда передавайте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
— Function
format_duration(::AbstractTestSet)
Возвращает форматированную строку для вывода продолжительности выполнения набора тестов.
Если эта функция не определена, в качестве резервного варианта используется "?s"
.
#
Test.print_test_results
— Function
print_test_results(ts::AbstractTestSet, depth_pad=0)
Выводит результаты AbstractTestSet
в виде форматированной таблицы.
depth_pad
указывает размер отступа перед выходными данными.
Вызывается внутри Test.finish
, если завершенный (finish
) набор тестов является самым верхним.
Тестовые служебные программы
#
Test.GenericArray
— Type
GenericArray
можно использовать для тестирования универсальных API массивов, которые пишутся для интерфейса AbstractArray
, с целью убедиться в том, что функции могут работать с типами массивов помимо стандартного типа Array
.
#
Test.GenericDict
— Type
GenericDict
Можно использовать для тестирования универсальных API словарей, которые пишутся для интерфейса AbstractDict
, с целью убедиться в том, что функции могут работать с типами словарей помимо стандартного типа Dict
.
#
Test.GenericOrder
— Type
GenericOrder
можно использовать для тестирования поддержки универсальных упорядоченных типов в API.
#
Test.GenericSet
— Type
GenericSet
Можно использовать для тестирования универсальных API множеств, которые пишутся для интерфейса AbstractSet
, с целью убедиться в том, что функции могут работать с типами множеств помимо стандартных типов Set
и BitSet
.
#
Test.GenericString
— Type
GenericString
Можно использовать для тестирования универсальных API строк, которые пишутся для интерфейса AbstractString
, с целью убедиться в том, что функции могут работать с типами строк помимо стандартного типа String
.
#
Test.detect_ambiguities
— Function
detect_ambiguities(mod1, mod2...; recursive=false,
ambiguous_bottom=false,
allowed_undefineds=nothing)
Возвращает вектор пар (Method,Method)
неоднозначных методов, определенных в указанных модулях. Используйте recursive=true
для тестирования во всех подмодулях.
ambiguous_bottom
контролирует, будут ли включены неоднозначности, вызываемые только параметрами типа Union{}
. В большинстве случаев, скорее всего, лучше будет установить значение false
. См. описание Base.isambiguous
.
См. описание Test.detect_unbound_args
с пояснением, касающимся функции allowed_undefineds
.
Совместимость: Julia 1.8
Для |
#
Test.detect_unbound_args
— Function
detect_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
.
Совместимость: Julia 1.8
Для |
Рабочий процесс тестирования пакетов
Ниже описан возможный рабочий процесс создания пакета и добавления в него тестов с использованием инструментов, упомянутых в предыдущих разделах.
Создание примера пакета
Для этого рабочего процесса мы создадим пакет с именем 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
Создание тестовой среды
Из корня пакета Example
перейдите в каталог test
, активируйте в нем новую среду и добавьте в нее пакет Test
:
shell> cd test
pkg> activate .
(test) pkg> add Test
Тестирование пакета
Теперь мы готовы добавить тесты в пакет Example
. Для выполняемых наборов тестов принято создавать файл с именем runtests.jl
в каталоге test
. Создайте такой файл в каталоге 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
, и добавить в них ряд тестов.
Примечание. Обратите внимание, что нам не пришлось добавлять
Example
в файлеProject.toml
средыtest
. Это преимущество системы тестирования Julia, о котором можно узнать больше здесь.
Написание тестов для 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
Тестирование пакета
Добавив тесты и скрипт runtests.jl
в среду test
, мы можем протестировать пакет 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
можно добавлять и более сложные тесты для пакетов, но эти инструкции являются хорошей отправной точкой для разработчиков, желающих приступить к тестированию собственных пакетов.
Покрытие кода
Отслеживание покрытия кода во время тестирования можно включить с помощью флага pkg> test --coverage
(или на более низком уровне с помощью аргумента Julia --code-coverage
). Оно включено по умолчанию в действии GitHub julia-runtest.
Для вычисления покрытия либо вручную проверьте файлы .cov
, которые генерируются локально рядом с файлами исходного кода, либо в CI используйте действие GitHub julia-processcoverage.
Совместимость: Julia 1.11
Начиная с Julia 1.11 покрытие не собирается на этапе предварительной компиляции пакета. |