Документация 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, возвращает 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 или вызывает исключение. Возвращает 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
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 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)
  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] [option=val  ...] ["description"] begin ... end
@testset [CustomTestSet] [option=val  ...] ["description $v"] for v in (...) ... end
@testset [CustomTestSet] [option=val  ...] ["description $v, $w"] for v in (...), w in (...) ... end
@testset [CustomTestSet] [option=val  ...] ["description"] foo()
@testset let v = (...) ... 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 foo() требуется версия не ниже 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> @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
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") # выполняется
@test_logs min_level=Logging.Warn @warn("Some information") # завершается сбоем

Если требуется протестировать отсутствие предупреждений (или сообщений об ошибках) в 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), если типы совпадают, и 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, если не %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, или вызывает исключение. Возвращает Broken Result, если это так, или Error Result, если выражение равно 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)

Создание пользовательских типов 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
    # конструктор принимает строку описания и именованные аргументы параметров
    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)
    # просто запись, если мы не являемся родительским элементом верхнего уровня
    if get_testset_depth() > 0
        record(get_testset(), ts)
    end
    ts
end

Использование этого набора тестов выглядит следующим образом:

@testset CustomTestSet foo=4 "custom testset inner 2" begin
    # этот набор тестов должен наследовать тип, но не аргумент.
    @testset "custom testset inner" begin
        @test true
    end
end

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

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, для которых предупреждение может быть пропущено. Например, при задании

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

Создание примеров функций

Главным требованием при тестировании пакета является наличие тестируемых функций. Поэтому мы добавим в пакет 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

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