Тестирование модулей
Тестирование базовой Julia
Julia активно развивается и имеет обширную тестовую базу для проверки функциональности на нескольких платформах. Если вы собираете Julia из исходного кода, вы можете запустить этот набор тестов с помощью make test. В бинарной установке вы можете запустить тестовую базу с помощью Base.runtests().
Base.runtestsФункция
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Макрос
@test ex @test f(args...) key=val ... @test ex broken=true @test ex skip=true
Проверка того, что выражение ex вычисляется до true. Если выполняется внутри @testset, возвращает Pass Result, если это так, Fail Result, если это false, и Error Result, если оно не может быть вычислено. Если выполняется вне @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или вызывает исключение. ВозвращаетBrokenResult, если это так, илиErrorResult, если выражение вычисляется до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
Ключевые аргументы broken и skip требуют, как минимум, Julia 1.7.
Test.@test_throwsМакрос
@test_throws exception expr
Проверяет, что выражение expr выбрасывает исключение exception. Исключение может указывать либо на тип, либо на строку, либо на регулярное выражение, либо на список строк, появляющихся в сообщении об ошибке, либо на функцию сопоставления, либо на значение (которое будет проверяться на равенство путём сравнения полей). Обратите внимание, что @test_throws не поддерживает заключительный ключевой формат.
Возможность указывать что-либо, кроме типа или значения, как exception требует Julia v1.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 создаёт локальную область видимости при выполнении тестов в ней.
Макрос @testset может использоваться для группировки тестов в наборы. Все тесты в наборе будут выполнены, и в конце набора будет напечатан сводный отчёт. Если какие-либо тесты потерпели неудачу или не могли быть вычислены из-за ошибки, набор тестов выбросит исключение TestSetException.
Test.@testsetМакрос
@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
С началом/концом или вызовом функции
Когда используется @testset, с началом/концом или одиночным вызовом функции, макрос запускает новый набор тестов, в котором нужно оценить заданное выражение.
Если тип настраиваемого набора тестов не указан, по умолчанию создается DefaultTestSet. DefaultTestSet записывает все результаты и, если есть какие-либо Fail или Error, выбрасывает исключение в конце набора тестов верхнего уровня (не вложенного), вместе с сводкой результатов тестов.
Любой пользовательский тип набора тестов (подтип AbstractTestSet) может быть задан, и он также будет использоваться для всех вложенных вызовов @testset. Указанные параметры применяются только к набору тестов, где они указаны. Тип набора тестов по умолчанию принимает три логических параметра:
-
verbose: еслиtrue, сводка результатов вложенных наборов тестов отображается даже при их успешном выполнении (по умолчаниюfalse). -
showtiming: еслиtrue, отображается продолжительность выполнения каждого набора тестов (по умолчаниюtrue). -
failfast: еслиtrue, любой сбой или ошибка теста заставят набор тестов и все дочерние наборы тестов вернуть значение немедленно (по умолчаниюfalse). Это также может быть установлено глобально через переменную окруженияJULIA_TEST_FAILFAST.
@testset test_func() требует как минимум Julia 1.8.
failfast требует как минимум Julia 1.9.
Строка описания принимает интерполяцию из индексов цикла. Если описание не указано, оно строится на основе переменных. Если указан вызов функции, его имя будет использоваться. Явные строки описания переопределяют это поведение.
По умолчанию макрос @testset вернёт сам объект набора тестов, хотя это поведение можно настроить в других типах наборов тестов. Если используется цикл for, макрос собирает и возвращает список возвращаемых значений метода finish, который по умолчанию вернет список объектов наборов тестов, используемых в каждой итерации.
Перед выполнением тела @testset, происходит неявный вызов Random.seed!(seed), где seed — текущее значение генератора глобальных случайных чисел. Кроме того, после выполнения тела состояние глобального генератора случайных чисел восстанавливается до состояния, которое было до вызова @testset. Это призвано упростить воспроизводимость в случае сбоя и обеспечить бесшовную перестановку @testset независимо от их влияния на состояние глобального генератора случайных чисел.
Примеры
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, макрос запускает прозрачный набор тестов, к которому добавляется указанный объект как контекстный объект для любого неудачного теста внутри него. Это полезно при выполнении набора связанных тестов на одном большом объекте, и желательно напечатать этот большой объект, когда любой из отдельных тестов завершается неудачно. Прозрачные наборы тестов не вводят дополнительные уровни вложенности в иерархии наборов тестов и передаются напрямую в родительский набор тестов (с добавленным контекстным объектом к любым неудачным тестам).
@testset let требует как минимум Julia 1.9.
Несколько let назначений поддерживаются начиная с 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Тип
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Макрос
@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
Мы можем проверить сообщение info с помощью
@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 that the expression logs no messages when the logger level is warn:
@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Тип
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Тип
LogRecord
Хранит результаты одного события протокола. Поля:
-
level:LogLevelсообщения протокола -
message: текстовое содержимое сообщения протокола -
_module: модуль события протокола -
group: группа протоколирования (по умолчанию, имя файла, содержащего событие протокола) -
id: идентификатор события протокола -
file: файл, содержащий событие протокола -
line: строка в файле события протокола -
kwargs: любые ключевые аргументы, переданные событию протокола
Другие макросы тестов
Так как вычисления с плавающей точкой могут быть неточными, вы можете выполнить проверку приблизительного равенства, используя либо @test a ≈ b (где ≈, введённый с помощью автодополнения \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Макрос
@inferred [AllowedType] f(x)
Тесты, проверяющие, что выражение вызова f(x) возвращает значение того же типа, который определил компилятор. Это полезно для проверки стабильности типов.
f(x) может быть любым выражением вызова. Возвращает результат f(x), если типы совпадают, и Error Result, если обнаруживаются разные типы.
Необязательно, 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 if not %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Макрос
@test_deprecated [pattern] expression
Когда --depwarn=yes, проверяет, что expression генерирует предупреждение об устаревании и возвращает значение expression. Строка сообщения журнала будет сравниваться с pattern, по умолчанию r"deprecated"i.
Когда --depwarn=no, просто возвращает результат выполнения expression. Когда --depwarn=error, проверяет, что выбрасывается исключение ErrorException.
Примеры
# Deprecated in julia 0.7 @test_deprecated num2hex(1) # The returned value can be tested by chaining with @test: @test (@test_deprecated num2hex(1)) == "0000000000000001"
Test.@test_warnМакрос
@test_warn msg expr
Проверяет, приводит ли вычисление expr к выводу в stderr, содержащему строку msg или соответствующему регулярному выражению msg. Если msg — булева функция, проверяет, возвращает ли msg(output) значение true. Если msg — кортеж или массив, проверяет, содержит ли вывод ошибки каждый элемент в msg. Возвращает результат вычисления expr.
См. также @test_nowarn для проверки отсутствия вывода об ошибках.
Примечание: Предупреждения, генерируемые @warn, не могут быть проверены с помощью этого макроса. Используйте @test_logs вместо этого.
Test.@test_nowarnМакрос
@test_nowarn expr
Проверяет, приводит ли вычисление expr к пустому выводу в stderr (без предупреждений или других сообщений). Возвращает результат вычисления expr.
Примечание: Отсутствие предупреждений, генерируемых @warn, не может быть проверено с помощью этого макроса. Используйте @test_logs вместо этого.
Разбитые тесты
Если тест постоянно терпит неудачу, его можно изменить, используя макрос @test_broken. Это обозначит тест как Broken, если он продолжает терпит неудачу, и уведомит пользователя через Error , если тест успешно пройден.
Test.@test_brokenМакрос
@test_broken ex @test_broken f(args...) key=val ...
Указывает тест, который должен пройти, но в настоящее время постоянно терпит неудачу. Проверяет, что выражение ex вычисляется до false или вызывает исключение. Возвращает Broken Result в случае успеха или Error Result в случае неудачи. Это эквивалентно @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Макрос
@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Тип
Test.Result
Все тесты производят объект результата. Этот объект может или не может быть сохранён, в зависимости от того, является ли тест частью набора тестов.
Test.PassТип
Test.Pass <: Test.Result
Условие теста было истинным, т. е. выражение вычислялось как истинное или было выброшено правильное исключение.
Test.FailТип
Test.Fail <: Test.Result
Условие теста было ложным, т. е. выражение вычислялось как ложное или не было выброшено правильное исключение.
Test.ErrorТип
Test.Error <: Test.Result
Условие теста не удалось вычислить из-за исключения или оно вычислялось как что-то другое, кроме Bool. В случае @test_broken он используется для указания возникшего неожиданного Pass Result.
Test.BrokenТип
Test.Broken <: Test.Result
Условие теста является ожидаемым (неуспешным) результатом разбитого теста или было явно пропущено с помощью @test_skip.
Создание настраиваемых типов AbstractTestSet
Пакеты могут создавать свои собственные подтипы AbstractTestSet путём реализации методов record и finish. Подтип должен иметь одноаргументный конструктор, принимающий строку описания, а любые параметры передаются в качестве ключевых аргументов.
Test.recordФункция
record(ts::AbstractTestSet, res::Result)
Записывает результат в набор тестов. Эта функция вызывается инфраструктурой @testset каждый раз, когда завершается содержащийся в нём @test макрос, и получает результат теста (который может быть Error). Она также вызывается с Error , если исключение было выброшено внутри блока теста, но вне контекста @test.
Test.finishФункция
finish(ts::AbstractTestSet)
Выполняет все необходимые заключительные обработки для данного набора тестов. Это вызывается инфраструктурой @testset после выполнения блока теста.
Пользовательские подтипы AbstractTestSet должны вызывать record у своего родительского элемента (если он есть), чтобы добавить себя в дерево результатов тестов. Это можно реализовать следующим образом:
if get_testset_depth() != 0
# Attach this test set to the parent test set
parent_ts = get_testset()
record(parent_ts, self)
return self
endTest отвечает за поддержание стека вложенных наборов тестов во время их выполнения, но любая накапливаемая информация о результатах является обязанностью подтипа AbstractTestSet. Вы можете получить доступ к этому стеку с помощью методов get_testset и get_testset_depth . Обратите внимание, что эти функции не экспортируются.
Test.get_testsetФункция
get_testset()
Возвращает активный набор тестов из локального хранилища задачи. Если активного набора тестов нет, использует набор тестов по умолчанию.
Test.get_testset_depthФункция
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Тип
TestCounts
Содержит состояние для рекурсивного сбора результатов набора тестов для отображения.
Поля:
-
customized: Признак того, была ли функцияget_test_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Функция
" gettestcounts(::AbstractTestSet) -> TestCounts
Рекурсивная функция, которая подсчитывает количество результатов тестов каждого типа непосредственно в наборе тестов и суммирует результаты по дочерним наборам тестов.
Пользовательский AbstractTestSet должен реализовать эту функцию, чтобы подсчитать и отобразить свои итоги вместе с DefaultTestSet.
Если эта функция не реализована для пользовательского TestSet, отображение результатов будет возвращено к сообщению о x для ошибок и ?s для продолжительности.
Test.format_durationФункция
format_duration(::AbstractTestSet)
Возвращает отформатированную строку для отображения продолжительности выполнения набора тестов.
Если не определена, используется "?s".
Test.print_test_resultsФункция
print_test_results(ts::AbstractTestSet, depth_pad=0)
Выводит результаты AbstractTestSet в виде отформатированной таблицы.
depth_pad определяет, сколько отступов должно быть добавлено перед всем выводом.
Вызывается внутри Test.finish, если набор тестов finish является верхним.
Утилиты для тестирования
Test.GenericArrayТип
GenericArray можно использовать для тестирования универсальных API массивов, которые работают с интерфейсом AbstractArray, чтобы убедиться, что функции могут работать с типами массивов помимо стандартного типа Array.
Test.GenericDictТип
GenericDict можно использовать для тестирования универсальных API словарей, которые работают с интерфейсом AbstractDict, чтобы убедиться, что функции могут работать с ассоциативными типами помимо стандартного типа Dict.
Test.GenericOrderТип
GenericOrder можно использовать для тестирования API на предмет поддержки универсальных упорядоченных типов.
Test.GenericSetТип
GenericSet можно использовать для тестирования универсальных API множеств, которые работают с интерфейсом AbstractSet, чтобы убедиться, что функции могут работать с типами множеств помимо стандартных типов Set и BitSet.
Test.GenericStringТип
GenericString можно использовать для тестирования универсальных API строк, которые работают с интерфейсом AbstractString, чтобы убедиться, что функции могут работать с типами строк помимо стандартного типа String.
Test.detect_ambiguitiesФункция
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.
allowed_undefineds требует как минимум Julia 1.8.
Test.detect_unbound_argsФункция
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.
allowed_undefineds требует как минимум Julia 1.8.
Процесс тестирования пакетов
Используя инструменты, доступные в предыдущих разделах, вот потенциальный процесс создания пакета и добавления тестов к нему.
Создание примера пакета
Для этого процесса мы создадим пакет под названием Example.
pkg> generate Example shell> cd Example shell> mkdir test pkg> activate .
Создание образцовых функций
Основное требование для тестирования пакета — наличие функциональности для тестирования. Для этого мы добавим несколько простых функций в 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. Стандартной практикой является создание файла в каталоге test с именем runtests.jl, содержащего наборы тестов, которые мы хотим запустить. Создайте этот файл в каталоге 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в средуtest'sProject.toml. Это преимущество системы тестирования 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
Написание тестов для greeting_tests.jl
Используя наши знания о Test.jl, вот примеры тестов, которые мы можем добавить в greeting_tests.jl:
@testset "Testset 3" begin
@test "Hello world!" == greet()
@test_throws MethodError greet("Antonia")
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, покрытие не собирается во время предварительной компиляции пакета.
© 2009–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.11/stdlib/Test/