Документация Engee

Модульное тестирование

Тестирование Base Julia

Julia находится в процессе быстрой разработки и располагает обширным набором тестов для проверки функциональности на различных платформах. Если вы выполняете сборку Julia из исходного кода, этот набор тестов можно запустить с помощью make test. При бинарной установке набор тестов можно запустить с помощью 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 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

Для именованных аргументов broken и skip требуется версия Julia не ниже 1.7.

@test_throws exception expr

Проверяет, вызывает ли выражение expr исключение exception. В исключении может указываться тип, строка, регулярное выражение, список строк в выводимом сообщении об ошибке или значение (которое будет проверяться на равенство путем сравнения полей). Обратите внимание, что @test_throws не поддерживает форму с ключевым словом в конце.

Совместимость: Julia 1.8

Для возможности указания в качестве exception чего-либо, отличного от типа или значения, требуется версия 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 создаст собственную локальную область при выполнении в ней тестов.

Макрос @testset можно использовать для группировки тестов в наборы. Будут выполнены все тесты из набора тестов, а в конце набора тестов будет выведена сводка. В случае сбоя какого-либо теста или невозможности выполнить оценку из-за ошибки набор тестов выдаст исключение TestSetException.

@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

Для @testset test_func() требуется версия Julia не ниже 1.8.

Совместимость: Julia 1.9

Для failfast требуется версия 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

Для @testset let требуется версия Julia не ниже 1.9.

Совместимость: Julia 1.10

Несколько присваиваний 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
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_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.

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
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). Однако это особенно полезно для тестов .

@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_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_warn msg expr

Проверяет, возвращаются ли в результате вычисления выражения expr выходные данные stderr, содержащие строку msg или соответствующие регулярному выражению msg. Если msg — это логическая функция, проверяет, возвращает ли msg(output) значение true. Если msg — это кортеж или массив, проверяет, содержит ли вывод ошибки каждый элемент в msg или соответствует ли он каждому элементу. Возвращает результат вычисления выражения expr.

См. также описание макроса @test_nowarn для проверки на отсутствие вывода ошибки.

Примечание. С помощью этого макроса нельзя протестировать предупреждения, генерируемые макросом @warn. Используйте вместо него @test_logs.

@test_nowarn expr

Проверяет, возвращаются ли в результате вычисления выражения expr пустые выходные данные stderr (без предупреждений или других сообщений). Возвращает результат вычисления выражения expr.

Примечание. С помощью этого макроса нельзя протестировать отсутствие предупреждений, сгенерированных макросом @warn. Используйте вместо него @test_logs.

Неработающие тесты

Если тест постоянно завершается сбоем, его можно изменить так, чтобы в нем использовался макрос @test_broken. При этом тест будет обозначен как Broken, если он продолжает завершаться ошибкой, и будет указан как Error, если он выполнен успешно.

@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_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.Pass <: Test.Result

Условие теста было выполнено, т. е. выражение было вычислено как true или было выдано правильное исключение.

Test.Fail <: Test.Result

Условие теста не было выполнено, т. е. выражение было вычислено как false или не было выдано правильное исключение.

Test.Error <: Test.Result

Условие теста не удалось оценить из-за исключения, либо в результате его оценки было получено значение, отличное от Bool. В случае с @test_broken используется для указания на то, что получен непредвиденный Result Pass.

Test.Broken <: Test.Result

Для условия теста получен ожидаемый (неудачный) результат, либо оно было явно пропущено с помощью @test_skip.

Создание пользовательских типов AbstractTestSet

Пакеты могут создавать собственные подтипы AbstractTestSet, реализуя методы record и finish. Подтип должен иметь одноаргументный конструктор, принимающий строку описания, а любые параметры передаются в качестве именованных аргументов.

record(ts::AbstractTestSet, res::Result)

Записывает результат в набор тестов. Эта функция вызывается инфраструктурой @testset при каждом завершении вложенного макроса @test и получает результат тестирования (которым может быть ошибка Error). Эта функция также будет вызвана с ошибкой (Error), если исключение возникло внутри тестового блока, но вне контекста @test.

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. Обратите внимание, что эти функции не экспортируются.

get_testset()

Получает активный набор тестов из локального хранилища задачи. Если активный набор тестов отсутствует, использует резервный набор тестов.

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
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.

" gettestcounts(::AbstractTestSet) -> TestCounts

Рекурсивная функция, которая подсчитывает количество результатов тестов каждого типа непосредственно в наборе тестов и суммирует их по дочерним наборам тестов.

Пользовательский тип AbstractTestSet должен реализовывать эту функцию для подсчета и отображения итоговых количеств вместе с DefaultTestSet.

Если она не реализована для пользовательского TestSet, по умолчанию выводится x для сбоев и ?s для продолжительности.

format_duration(::AbstractTestSet)

Возвращает форматированную строку для вывода продолжительности выполнения набора тестов.

Если эта функция не определена, в качестве резервного варианта используется "?s".

print_test_results(ts::AbstractTestSet, depth_pad=0)

Выводит результаты AbstractTestSet в виде форматированной таблицы.

depth_pad указывает размер отступа перед выходными данными.

Вызывается внутри Test.finish, если завершенный (finish) набор тестов является самым верхним.

Тестовые служебные программы

GenericArray можно использовать для тестирования универсальных API массивов, которые пишутся для интерфейса AbstractArray, с целью убедиться в том, что функции могут работать с типами массивов помимо стандартного типа Array.

GenericDict Можно использовать для тестирования универсальных API словарей, которые пишутся для интерфейса AbstractDict, с целью убедиться в том, что функции могут работать с типами словарей помимо стандартного типа Dict.

GenericOrder можно использовать для тестирования поддержки универсальных упорядоченных типов в API.

GenericSet Можно использовать для тестирования универсальных API множеств, которые пишутся для интерфейса AbstractSet, с целью убедиться в том, что функции могут работать с типами множеств помимо стандартных типов Set и BitSet.

GenericString Можно использовать для тестирования универсальных API строк, которые пишутся для интерфейса AbstractString, с целью убедиться в том, что функции могут работать с типами строк помимо стандартного типа String.

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

Для allowed_undefineds требуется версия Julia не ниже 1.8.

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

Для allowed_undefineds требуется версия 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

Написание тестов для 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

Начиная с Julia 1.11 покрытие не собирается на этапе предварительной компиляции пакета.