Unit Testing

Testing Base Julia

Julia находится на стадии активной разработки и имеет обширный набор тестов для проверки функциональности на различных платформах. Если вы собираете Julia из исходников, вы можете запустить этот набор тестов с помощью make test. В бинарной установке вы можете запустить набор тестов, используя Base.runtests().

Base.runtestsFunction
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 или в стандартные библиотеки перед запуском тестов. Если семя предоставлено через аргумент ключевого слова, оно используется для инициализации глобального RNG в контексте, где выполняются тесты; в противном случае семя выбирается случайным образом.

source

Basic Unit Tests

Модуль Test предоставляет простую функциональность модульного тестирования. Модульное тестирование — это способ проверить, правильен ли ваш код, проверяя, соответствуют ли результаты вашим ожиданиям. Это может быть полезно, чтобы убедиться, что ваш код по-прежнему работает после внесения изменений, и может использоваться при разработке как способ указания поведения, которое ваш код должен иметь по завершении. Вы также можете ознакомиться с документацией по adding tests to your Julia Package.

Простое модульное тестирование можно выполнить с помощью макросов @test и @test_throws:

Test.@testMacro
@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
Тест пройден
Julia 1.7

Аргументы ключевых слов broken и skip требуют как минимум Julia 1.7.

source
Test.@test_throwsMacro
@test_throws exception expr

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

Julia 1.8

Возможность указывать что-либо, кроме типа или значения, в качестве 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) (функция сопоставления)
source

Например, предположим, что мы хотим проверить, что наша новая функция 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

Обычно используется большое количество тестов, чтобы убедиться, что функции работают правильно с различными входными данными. В случае, если тест не проходит, поведение по умолчанию — немедленно выбросить исключение. Однако обычно предпочтительнее сначала запустить остальные тесты, чтобы получить более полное представление о том, сколько ошибок в тестируемом коде.

Note

@testset создаст собственную локальную область при выполнении тестов в ней.

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

Test.@testsetMacro
@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.
Julia 1.8

@testset test_func() требует как минимум Julia 1.8.

Julia 1.9

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

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
Тест не прошел на 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

ОШИБКА: Произошла ошибка во время тестирования
source
Test.TestSetExceptionType
TestSetException

Выбрасывается, когда набор тестов завершается, и не все тесты прошли.

source

Мы можем поместить наши тесты для функции 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_logsMacro
@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). Элементы, которые присутствуют, будут сопоставлены парно с полями записи журнала, используя == по умолчанию, с особыми случаями, что Symbols могут быть использованы для стандартных уровней журнала, а Regexs в шаблоне будут соответствовать строковым или символьным полям, используя 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.

source
Test.TestLoggerType
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
source
Test.LogRecordType
LogRecord

Хранит результаты одного события журнала. Поля:

  • level: LogLevel сообщения журнала
  • message: текстовое содержимое сообщения журнала
  • _module: модуль события журнала
  • group: группа журналирования (по умолчанию, имя файла, содержащего событие журнала)
  • id: ID события журнала
  • file: файл, содержащий событие журнала
  • line: строка в файле события журнала
  • kwargs: любые именованные аргументы, переданные событию журнала
source

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.@inferredMacro
@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}
[...]
source
Test.@test_deprecatedMacro
@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"
source
Test.@test_warnMacro
@test_warn msg expr

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

Смотрите также @test_nowarn, чтобы проверить отсутствие вывода ошибок.

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

source
Test.@test_nowarnMacro
@test_nowarn expr

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

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

source

Broken Tests

Если тест постоянно не проходит, его можно изменить, чтобы использовать макрос @test_broken. Это будет обозначать тест как Сломанный, если тест продолжает не проходить, и уведомлять пользователя через Ошибка, если тест проходит.

Test.@test_brokenMacro
@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)
source

@test_skip также доступен для пропуска теста без оценки, но с учетом пропущенного теста в отчетности набора тестов. Тест не будет выполнен, но выдаст Broken Result.

Test.@test_skipMacro
@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)
source

Test result types

Test.ResultType
Test.Result

Все тесты производят объект результата. Этот объект может быть сохранен или не сохранен, в зависимости от того, является ли тест частью набора тестов.

source
Test.PassType
Test.Pass <: Test.Result

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

source
Test.FailType
Test.Fail <: Test.Result

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

source
Test.ErrorType
Test.Error <: Test.Result

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

source
Test.BrokenType
Test.Broken <: Test.Result

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

source

Creating Custom AbstractTestSet Types

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

Test.recordFunction
record(ts::AbstractTestSet, res::Result)

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

source
Test.finishFunction
finish(ts::AbstractTestSet)

Выполните все необходимые финальные обработки для данного набора тестов. Это вызывается инфраструктурой @testset после выполнения блока тестов.

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

if get_testset_depth() != 0
    # Присоедините этот набор тестов к родительскому набору тестов
    parent_ts = get_testset()
    record(parent_ts, self)
    return self
end
source

Test берет на себя ответственность за поддержание стека вложенных тестовых наборов по мере их выполнения, но любое накопление результатов является ответственностью подтипа AbstractTestSet. Вы можете получить доступ к этому стеку с помощью методов get_testset и get_testset_depth. Обратите внимание, что эти функции не экспортируются.

Test.get_testsetFunction
get_testset()

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

source
Test.get_testset_depthFunction
get_testset_depth()

Возвращает количество активных тестовых наборов, не включая набор тестов по умолчанию

source

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

Содержит состояние для рекурсивного сбора результатов набора тестов для целей отображения.

Поля:

  • 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.
source
Test.get_test_countsFunction

" gettestcounts(::AbstractTestSet) -> TestCounts

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

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

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

source
Test.format_durationFunction
format_duration(::AbstractTestSet)

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

Если не определено, используется "?s".

source
Test.print_test_resultsFunction
print_test_results(ts::AbstractTestSet, depth_pad=0)

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

depth_pad указывает, сколько отступа должно быть добавлено перед всем выводом.

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

source

Test utilities

Test.GenericArrayType

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

source
Test.GenericDictType

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

source
Test.GenericOrderType

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

source
Test.GenericSetType

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

source
Test.GenericStringType

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

source
Test.detect_ambiguitiesFunction
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.

source
Test.detect_unbound_argsFunction
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.

source

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

С тех пор как Julia 1.11, покрытие не собирается во время фазы предкомпиляции пакета.