Unit Testing
Testing Base Julia
Julia находится на стадии активной разработки и имеет обширный набор тестов для проверки функциональности на различных платформах. Если вы собираете Julia из исходников, вы можете запустить этот набор тестов с помощью make test
. В бинарной установке вы можете запустить набор тестов, используя Base.runtests()
.
Base.runtests
— FunctionBase.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
или в стандартные библиотеки перед запуском тестов. Если семя предоставлено через аргумент ключевого слова, оно используется для инициализации глобального RNG в контексте, где выполняются тесты; в противном случае семя выбирается случайным образом.
Basic Unit Tests
Модуль Test
предоставляет простую функциональность модульного тестирования. Модульное тестирование — это способ проверить, правильен ли ваш код, проверяя, соответствуют ли результаты вашим ожиданиям. Это может быть полезно, чтобы убедиться, что ваш код по-прежнему работает после внесения изменений, и может использоваться при разработке как способ указания поведения, которое ваш код должен иметь по завершении. Вы также можете ознакомиться с документацией по adding tests to your Julia Package.
Простое модульное тестирование можно выполнить с помощью макросов @test
и @test_throws
:
Test.@test
— Macro@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
Тест пройден
julia> @test [1, 2] + [2, 1] == [3, 3]
Тест пройден
Форма @test f(args...) key=val...
эквивалентна записи @test f(args..., key=val...)
, что может быть полезно, когда выражение является вызовом с использованием инфиксного синтаксиса, такого как приближенные сравнения:
julia> @test π ≈ 3.14 atol=0.01
Тест пройден
Это эквивалентно более громоздкому тесту @test ≈(π, 3.14, atol=0.01)
. Ошибка возникает, если предоставить более одного выражения, если только первое не является выражением вызова, а остальные — присваиваниями (k=v
).
Вы можете использовать любой ключ для аргументов key=val
, кроме broken
и skip
, которые имеют особые значения в контексте @test
:
broken=cond
указывает на тест, который должен пройти, но в настоящее время постоянно терпит неудачу, когдаcond==true
. Тестирует, что выражениеex
оценивается какfalse
или вызывает исключение. ВозвращаетBroken
Result
, если это так, илиError
Result
, если выражение оценивается какtrue
. Обычный@test ex
оценивается, когдаcond==false
.skip=cond
помечает тест, который не должен выполняться, но должен быть включен в отчет о тестовом резюме какBroken
, когдаcond==true
. Это может быть полезно для тестов, которые периодически терпят неудачу, или для тестов еще не реализованной функциональности. Обычный@test ex
оценивается, когдаcond==false
.
Примеры
julia> @test 2 + 2 ≈ 6 atol=1 broken=true
Тест сломан
Выражение: ≈(2 + 2, 6, atol = 1)
julia> @test 2 + 2 ≈ 5 atol=1 broken=false
Тест пройден
julia> @test 2 + 2 == 5 skip=true
Тест сломан
Пропущено: 2 + 2 == 5
julia> @test 2 + 2 == 4 skip=false
Тест пройден
Аргументы ключевых слов broken
и skip
требуют как минимум Julia 1.7.
Test.@test_throws
— Macro@test_throws exception expr
Тестирует, что выражение expr
выбрасывает exception
. Исключение может указывать либо на тип, строку, регулярное выражение, либо список строк, которые встречаются в отображаемом сообщении об ошибке, функцию сопоставления или значение (которое будет проверяться на равенство путем сравнения полей). Обратите внимание, что @test_throws
не поддерживает завершающую форму ключевого слова.
Возможность указывать что-либо, кроме типа или значения, в качестве exception
требует Julia v1.8 или более поздней версии.
Примеры
julia> @test_throws BoundsError [1, 2, 3][4]
Тест пройден
Выброшено: BoundsError
julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
Тест пройден
Выброшено: DimensionMismatch
julia> @test_throws "Попробуйте sqrt(Complex" sqrt(-1)
Тест пройден
Сообщение: "DomainError with -1.0:\nsqrt был вызван с отрицательным действительным аргументом, но вернет комплексный результат только в том случае, если будет вызван с комплексным аргументом. Попробуйте sqrt(Complex(x))."
В последнем примере, вместо сопоставления с одной строкой, это также могло быть выполнено с:
["Попробуйте", "Complex"]
(список строк)r"Попробуйте 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
Working with Test Sets
Обычно используется большое количество тестов, чтобы убедиться, что функции работают правильно с различными входными данными. В случае, если тест не проходит, поведение по умолчанию — немедленно выбросить исключение. Однако обычно предпочтительнее сначала запустить остальные тесты, чтобы получить более полное представление о том, сколько ошибок в тестируемом коде.
@testset
создаст собственную локальную область при выполнении тестов в ней.
Макрос @testset
можно использовать для группировки тестов в наборы. Все тесты в наборе тестов будут выполнены, и в конце набора тестов будет напечатано резюме. Если какой-либо из тестов не прошел или не мог быть оценен из-за ошибки, набор тестов выбросит TestSetException
.
Test.@testset
— Macro@testset [CustomTestSet] [options...] ["описание"] begin test_ex end
@testset [CustomTestSet] [options...] ["описание $v"] for v in itr test_ex end
@testset [CustomTestSet] [options...] ["описание $v, $w"] for v in itrv, w in itrw test_ex end
@testset [CustomTestSet] [options...] ["описание"] 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
.
@testset test_func()
требует как минимум Julia 1.8.
failfast
требует как минимум Julia 1.9.
Строка описания принимает интерполяцию от индексов цикла. Если описание не предоставлено, оно создается на основе переменных. Если предоставлен вызов функции, будет использовано ее имя. Явные строки описания переопределяют это поведение.
По умолчанию макрос @testset
вернет сам объект тестового набора, хотя это поведение может быть настроено в других типах тестовых наборов. Если используется цикл for
, то макрос собирает и возвращает список значений, возвращаемых методом finish
, который по умолчанию вернет список объектов тестового набора, использованных в каждой итерации.
Перед выполнением тела @testset
происходит неявный вызов Random.seed!(seed)
, где seed
— это текущее значение семени глобального генератора случайных чисел. Более того, после выполнения тела состояние глобального генератора случайных чисел восстанавливается до того, каким оно было до @testset
. Это предназначено для облегчения воспроизводимости в случае сбоя и для обеспечения бесшовной перестановки @testset
, независимо от их побочного эффекта на состояние глобального генератора случайных чисел.
Примеры
julia> @testset "тригонометрические тождества" begin
θ = 2/3*π
@test sin(-θ) ≈ -sin(θ)
@test cos(-θ) ≈ cos(θ)
@test sin(2θ) ≈ 2*sin(θ)*cos(θ)
@test cos(2θ) ≈ cos(θ)^2 - sin(θ)^2
end;
Сводка тестов: | Прошло Всего Время
тригонометрические тождества | 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
Тест не прошел на none:3
Выражение: !(iszero(real(logi)))
Контекст: logi = 0.0 + 1.5707963267948966im
ОШИБКА: Произошла ошибка во время тестирования
julia> @testset let logi = log(im), op = !iszero
@test imag(logi) == π/2
@test op(real(logi))
end
Тест не прошел на none:3
Выражение: op(real(logi))
Контекст: logi = 0.0 + 1.5707963267948966im
op = !iszero
ОШИБКА: Произошла ошибка во время тестирования
Test.TestSetException
— TypeTestSetException
Выбрасывается, когда набор тестов завершается, и не все тесты прошли.
Мы можем поместить наши тесты для функции 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.
Testing Log Statements
Можно использовать макрос @test_logs
для тестирования логов, или использовать TestLogger
.
Test.@test_logs
— Macro@test_logs [log_patterns...] [keywords] выражение
Соберите список записей журналов, сгенерированных выражением
, используя collect_test_logs
, проверьте, что они соответствуют последовательности log_patterns
, и верните значение выражения
. Ключевые слова keywords
обеспечивают простую фильтрацию записей журналов: ключевое слово min_level
контролирует минимальный уровень журнала, который будет собран для теста, ключевое слово match_mode
определяет, как будет выполняться сопоставление (по умолчанию :all
проверяет, что все журналы и шаблоны соответствуют парно; используйте :any
, чтобы проверить, что шаблон соответствует хотя бы один раз где-то в последовательности.)
Самый полезный шаблон журнала — это простой кортеж вида (level,message)
. Разное количество элементов кортежа может быть использовано для сопоставления другой метаданных журнала, соответствующих аргументам, переданным в AbstractLogger
через функцию handle_message
: (level,message,module,group,id,file,line)
. Элементы, которые присутствуют, будут сопоставлены парно с полями записи журнала, используя ==
по умолчанию, с особыми случаями, что Symbol
s могут быть использованы для стандартных уровней журнала, а Regex
s в шаблоне будут соответствовать строковым или символьным полям, используя 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
соответствующим образом:
# тестируем, что выражение не записывает сообщений, когда уровень журнала warn:
@test_logs min_level=Logging.Warn @info("Некоторая информация") # проходит
@test_logs min_level=Logging.Warn @warn("Некоторая информация") # не проходит
Если вы хотите протестировать отсутствие предупреждений (или сообщений об ошибках) в stderr
, которые не генерируются @warn
, смотрите @test_nowarn
.
Test.TestLogger
— TypeTestLogger(; 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
— TypeLogRecord
Хранит результаты одного события журнала. Поля:
level
:LogLevel
сообщения журналаmessage
: текстовое содержимое сообщения журнала_module
: модуль события журналаgroup
: группа журналирования (по умолчанию, имя файла, содержащего событие журнала)id
: ID события журналаfile
: файл, содержащий событие журналаline
: строка в файле события журналаkwargs
: любые именованные аргументы, переданные событию журнала
Other Test Macros
Поскольку вычисления с плавающей запятой могут быть неточными, вы можете выполнять проверки на приближенное равенство, используя либо @test a ≈ b
(где ≈
, вводимое с помощью автозаполнения \approx
, является функцией isapprox
), либо использовать 4d61726b646f776e2e436f64652822222c20226973617070726f782229_40726566
напрямую.
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)
, если типы совпадают, и 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
— Macro@test_deprecated [pattern] выражение
Когда --depwarn=yes
, проверьте, что выражение
вызывает предупреждение о устаревании и верните значение выражения
. Строка сообщения журнала будет сопоставлена с pattern
, который по умолчанию равен r"deprecated"i
.
Когда --depwarn=no
, просто верните результат выполнения выражения
. Когда --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
.
Broken Tests
Если тест постоянно не проходит, его можно изменить, чтобы использовать макрос @test_broken
. Это будет обозначать тест как Сломанный
, если тест продолжает не проходить, и уведомлять пользователя через Ошибка
, если тест проходит.
Test.@test_broken
— Macro@test_broken ex
@test_broken f(args...) key=val ...
Указывает на тест, который должен пройти, но в настоящее время постоянно терпит неудачу. Проверяет, что выражение ex
оценивается в false
или вызывает исключение. Возвращает Broken
Result
, если это так, или Error
Result
, если выражение оценивается в true
. Это эквивалентно @test ex broken=true
.
Форма @test_broken f(args...) key=val...
работает так же, как и для макроса @test
.
Примеры
julia> @test_broken 1 == 2
Тест сломан
Выражение: 1 == 2
julia> @test_broken 1 == 2 atol=0.1
Тест сломан
Выражение: ==(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
Тест сломан
Пропущено: 1 == 2
julia> @test_skip 1 == 2 atol=0.1
Тест сломан
Пропущено: ==(1, 2, atol = 0.1)
Test result types
Test.Result
— TypeTest.Result
Все тесты производят объект результата. Этот объект может быть сохранен или не сохранен, в зависимости от того, является ли тест частью набора тестов.
Test.Pass
— TypeTest.Pass <: Test.Result
Условие теста было истинным, т.е. выражение оценивалось как истинное или было выброшено правильное исключение.
Test.Fail
— TypeTest.Fail <: Test.Result
Условие теста было ложным, т.е. выражение оценивалось как ложное или правильное исключение не было выброшено.
Test.Error
— TypeTest.Error <: Test.Result
Условие теста не могло быть оценено из-за исключения или оно оценивалось в нечто иное, чем Bool
. В случае @test_broken
это используется для указания на то, что произошел неожиданный Pass
Result
.
Test.Broken
— TypeTest.Broken <: Test.Result
Условие теста — это ожидаемый (неудачный) результат сломанного теста или он был явно пропущен с помощью @test_skip
.
Creating Custom AbstractTestSet
Types
Пакеты могут создавать свои собственные подтипы AbstractTestSet
, реализуя методы record
и finish
. Подтип должен иметь конструктор с одним аргументом, принимающим строку описания, с любыми параметрами, переданными в виде именованных аргументов.
Test.record
— Functionrecord(ts::AbstractTestSet, res::Result)
Записывает результат в тестовый набор. Эта функция вызывается инфраструктурой @testset
каждый раз, когда завершается содержащийся макрос @test
, и получает результат теста (который может быть Error
). Это также будет вызвано с Error
, если исключение выбрасывается внутри блока теста, но вне контекста @test
.
Test.finish
— Functionfinish(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
— Functionget_testset()
Получите активный тестовый набор из локального хранилища задачи. Если активный тестовый набор отсутствует, используйте запасной тестовый набор по умолчанию.
Test.get_testset_depth
— Functionget_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
— TypeTestCounts
Содержит состояние для рекурсивного сбора результатов набора тестов для целей отображения.
Поля:
customized
: Указывает, была ли функцияget_test_counts
настроена дляAbstractTestSet
, для которого предназначен этот объект подсчета. Если был определен пользовательский метод, всегда передавайте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
— Functionformat_duration(::AbstractTestSet)
Возвращает отформатированную строку для вывода продолжительности, в течение которой выполнялся тестовый набор.
Если не определено, используется "?s"
.
Test.print_test_results
— Functionprint_test_results(ts::AbstractTestSet, depth_pad=0)
Выводит результаты AbstractTestSet
в виде отформатированной таблицы.
depth_pad
указывает, сколько отступа должно быть добавлено перед всем выводом.
Вызывается внутри Test.finish
, если завершенный тестовый набор является верхним тестовым набором.
Test utilities
Test.GenericArray
— TypeGenericArray
можно использовать для тестирования универсальных API массивов, которые работают с интерфейсом AbstractArray
, чтобы убедиться, что функции могут работать с типами массивов, помимо стандартного типа Array
.
Test.GenericDict
— TypeGenericDict
можно использовать для тестирования общих API словарей, которые работают с интерфейсом AbstractDict
, чтобы гарантировать, что функции могут работать с ассоциативными типами, помимо стандартного типа Dict
.
Test.GenericOrder
— TypeGenericOrder
можно использовать для тестирования API на предмет их поддержки обобщенных упорядоченных типов.
Test.GenericSet
— TypeGenericSet
можно использовать для тестирования универсальных API наборов, которые работают с интерфейсом AbstractSet
, чтобы гарантировать, что функции могут работать с типами наборов, помимо стандартных типов Set
и BitSet
.
Test.GenericString
— TypeGenericString
можно использовать для тестирования универсальных строковых API, которые работают с интерфейсом AbstractString
, чтобы гарантировать, что функции могут работать с типами строк, помимо стандартного типа String
.
Test.detect_ambiguities
— Functiondetect_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
— Functiondetect_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.
Workflow for Testing Packages
Используя инструменты, доступные нам в предыдущих разделах, вот потенциальный рабочий процесс создания пакета и добавления тестов к нему.
Generating an Example Package
Для этого рабочего процесса мы создадим пакет под названием 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
Creating a Test Environment
Из корня пакета Example
перейдите в директорию test
, активируйте там новую среду и добавьте пакет Test
в среду:
shell> cd test
pkg> activate .
(test) pkg> add Test
Testing Our Package
Теперь мы готовы добавить тесты к 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
вProject.toml
окруженияtest
. Это преимущество системы тестирования Julia, что вы могли бы read about more here.
Writing Tests for 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
Writing Tests for greeting_tests.jl
Используя наши знания о Test.jl
, вот несколько примеров тестов, которые мы могли бы добавить в greeting_tests.jl
:
@testset "Testset 3" begin
@test "Hello world!" == greet()
@test_throws MethodError greet("Antonia")
end
Testing Our Package
Теперь, когда мы добавили наши тесты и наш скрипт 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
, можно добавить более сложные тесты для пакетов, но это должно в идеале направить разработчиков в сторону того, как начать тестирование своих собственных созданных пакетов.
Code Coverage
Отслеживание покрытия кода во время тестов можно включить с помощью флага pkg> test --coverage
(или на более низком уровне, используя аргумент --code-coverage
julia). Это включено по умолчанию в GitHub-экшене julia-runtest.
Чтобы оценить покрытие, либо вручную проверьте файлы .cov
, которые генерируются рядом с исходными файлами локально, либо в CI используйте julia-processcoverage действие GitHub.
С тех пор как Julia 1.11, покрытие не собирается во время фазы предкомпиляции пакета.