Spec-Zone.ru › Julia 1.11

Тестирование модулей

Тестирование базовой Julia

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

Base.runtestsФункция

Base.runtests(tests=["all"]; ncores=ceil(Int, Sys.CPU_THREADS / 2),
              exit_on_error=false, revise=false, [seed])

Запустите модульные тесты Julia, перечисленные в tests, которые могут быть строкой или массивом строк, с использованием ncores процессоров. Если exit_on_error равно false, при неудаче одного теста, все оставшиеся тесты в других файлах всё равно будут запущены; в противном случае они будут отброшены, когда exit_on_error == true. Если revise равно true, пакет Revise используется для загрузки любых изменений в Base или в стандартных библиотеках перед запуском тестов. Если значение для семени предоставляется в качестве ключевого аргумента, оно используется для инициализации глобального генератора случайных чисел в контексте, где выполняются тесты; в противном случае семя выбирается случайным образом.

исходный код

Базовые модульные тесты

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

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

Test.@testМакрос

@test ex
@test f(args...) key=val ...
@test ex broken=true
@test ex skip=true

Проверка того, что выражение ex вычисляется до true. Если выполняется внутри @testset, возвращает Pass Result, если это так, Fail Result, если это false, и Error Result, если оно не может быть вычислено. Если выполняется вне @testset, вместо возврата Fail или Error выбрасывается исключение.

Примеры

julia> @test true
Test Passed

julia> @test [1, 2] + [2, 1] == [3, 3]
Test Passed

Форма @test f(args...) key=val... эквивалентна записи @test f(args..., key=val...), что может быть полезно, когда выражение является вызовом с использованием инфиксного синтаксиса, такого как приблизительные сравнения:

julia> @test π ≈ 3.14 atol=0.01
Test Passed

Это эквивалентно более громоздкому тесту @test ≈(π, 3.14, atol=0.01). Недопустимо передавать более одного выражения, если первое не является выражением-вызовом, а остальные — операторами присваивания (k=v).

Вы можете использовать любые ключи для аргументов key=val, кроме broken и skip, которые имеют специальное значение в контексте @test:

  • broken=cond указывает на тест, который должен пройти, но в настоящее время постоянно терпит неудачу, когда cond==true. Проверяет, что выражение ex вычисляется до false или вызывает исключение. Возвращает 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

Ключевые аргументы broken и skip требуют, как минимум, Julia 1.7.

Test.@test_throwsМакрос

@test_throws exception expr

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

Возможность указывать что-либо, кроме типа или значения, как exception требует Julia v1.8 или более поздней версии.

Примеры

julia> @test_throws BoundsError [1, 2, 3][4]
Test Passed
      Thrown: BoundsError

julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
Test Passed
      Thrown: DimensionMismatch

julia> @test_throws "Try sqrt(Complex" sqrt(-1)
Test Passed
     Message: "DomainError with -1.0:\nsqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x))."

В последнем примере, вместо соответствия одной строке, это можно было бы выполнить с помощью:

  • ["Try", "Complex"] (список строк)
  • r"Try sqrt\([Cc]omplex" (регулярное выражение)
  • str -> occursin("complex", str) (функция сопоставления)

Например, предположим, что мы хотим проверить, что наша новая функция foo(x) работает как ожидается:

julia> using Test

julia> foo(x) = length(x)^2
foo (generic function with 1 method)

Если условие истинно, возвращается Pass:

julia> @test foo("bar") == 9
Test Passed

julia> @test foo("fizz") >= 10
Test Passed

Если условие ложно, возвращается Fail и выбрасывается исключение:

julia> @test foo("f") == 20
Test Failed at none:1
  Expression: foo("f") == 20
   Evaluated: 1 == 20

ERROR: There was an error during testing

Если условие не может быть вычислено из-за исключения, которое в этом случае происходит, потому что length не определено для символов, возвращается объект Error и выбрасывается исключение:

julia> @test foo(:cat) == 1
Error During Test
  Test threw an exception of type MethodError
  Expression: foo(:cat) == 1
  MethodError: no method matching length(::Symbol)
  The function `length` exists, but no method is defined for this combination of argument types.

  Closest candidates are:
    length(::SimpleVector) at essentials.jl:256
    length(::Base.MethodList) at reflection.jl:521
    length(::MethodTable) at reflection.jl:597
    ...
  Stacktrace:
  [...]
ERROR: There was an error during testing

Если мы ожидаем, что вычисление выражения должно вызвать исключение, мы можем использовать @test_throws для проверки этого:

julia> @test_throws MethodError foo(:cat)
Test Passed
      Thrown: MethodError

Работа с наборами тестов

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

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

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

Test.@testsetМакрос

@testset [CustomTestSet] [options...] ["description"] begin test_ex end
@testset [CustomTestSet] [options...] ["description $v"] for v in itr test_ex end
@testset [CustomTestSet] [options...] ["description $v, $w"] for v in itrv, w in itrw test_ex end
@testset [CustomTestSet] [options...] ["description"] test_func()
@testset let v = v, w = w; test_ex; end

С началом/концом или вызовом функции

Когда используется @testset, с началом/концом или одиночным вызовом функции, макрос запускает новый набор тестов, в котором нужно оценить заданное выражение.

Если тип настраиваемого набора тестов не указан, по умолчанию создается DefaultTestSet. DefaultTestSet записывает все результаты и, если есть какие-либо Fail или Error, выбрасывает исключение в конце набора тестов верхнего уровня (не вложенного), вместе с сводкой результатов тестов.

Любой пользовательский тип набора тестов (подтип AbstractTestSet) может быть задан, и он также будет использоваться для всех вложенных вызовов @testset. Указанные параметры применяются только к набору тестов, где они указаны. Тип набора тестов по умолчанию принимает три логических параметра:

  • verbose: если true, сводка результатов вложенных наборов тестов отображается даже при их успешном выполнении (по умолчанию false).
  • showtiming: если true, отображается продолжительность выполнения каждого набора тестов (по умолчанию true).
  • failfast: если true, любой сбой или ошибка теста заставят набор тестов и все дочерние наборы тестов вернуть значение немедленно (по умолчанию false). Это также может быть установлено глобально через переменную окружения JULIA_TEST_FAILFAST.

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

failfast требует как минимум Julia 1.9.

Строка описания принимает интерполяцию из индексов цикла. Если описание не указано, оно строится на основе переменных. Если указан вызов функции, его имя будет использоваться. Явные строки описания переопределяют это поведение.

По умолчанию макрос @testset вернёт сам объект набора тестов, хотя это поведение можно настроить в других типах наборов тестов. Если используется цикл for, макрос собирает и возвращает список возвращаемых значений метода finish, который по умолчанию вернет список объектов наборов тестов, используемых в каждой итерации.

Перед выполнением тела @testset, происходит неявный вызов Random.seed!(seed), где seed — текущее значение генератора глобальных случайных чисел. Кроме того, после выполнения тела состояние глобального генератора случайных чисел восстанавливается до состояния, которое было до вызова @testset. Это призвано упростить воспроизводимость в случае сбоя и обеспечить бесшовную перестановку @testset независимо от их влияния на состояние глобального генератора случайных чисел.

Примеры

julia> @testset "trigonometric identities" begin
           θ = 2/3*π
           @test sin(-θ) ≈ -sin(θ)
           @test cos(-θ) ≈ cos(θ)
           @test sin(2θ) ≈ 2*sin(θ)*cos(θ)
           @test cos(2θ) ≈ cos(θ)^2 - sin(θ)^2
       end;
Test Summary:            | Pass  Total  Time
trigonometric identities |    4      4  0.2s

@testset for

Когда используется @testset for, макрос запускает новый тест для каждой итерации предоставленного цикла. Семантика каждого набора тестов в остальном идентична той, что и в случае begin/end (как если бы она использовалась для каждой итерации цикла).

@testset let

Когда используется @testset let, макрос запускает прозрачный набор тестов, к которому добавляется указанный объект как контекстный объект для любого неудачного теста внутри него. Это полезно при выполнении набора связанных тестов на одном большом объекте, и желательно напечатать этот большой объект, когда любой из отдельных тестов завершается неудачно. Прозрачные наборы тестов не вводят дополнительные уровни вложенности в иерархии наборов тестов и передаются напрямую в родительский набор тестов (с добавленным контекстным объектом к любым неудачным тестам).

@testset let требует как минимум Julia 1.9.

Несколько let назначений поддерживаются начиная с Julia 1.10.

Примеры

julia> @testset let logi = log(im)
           @test imag(logi) == π/2
           @test !iszero(real(logi))
       end
Test Failed at none:3
  Expression: !(iszero(real(logi)))
     Context: logi = 0.0 + 1.5707963267948966im

ERROR: There was an error during testing

julia> @testset let logi = log(im), op = !iszero
           @test imag(logi) == π/2
           @test op(real(logi))
       end
Test Failed at none:3
  Expression: op(real(logi))
     Context: logi = 0.0 + 1.5707963267948966im
              op = !iszero

ERROR: There was an error during testing

Test.TestSetExceptionТип

TestSetException

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

Мы можем поместить наши тесты для функции foo(x) в набор тестов:

julia> @testset "Foo Tests" begin
           @test foo("a")   == 1
           @test foo("ab")  == 4
           @test foo("abc") == 9
       end;
Test Summary: | Pass  Total  Time
Foo Tests     |    3      3  0.0s

Наборы тестов также могут быть вложены:

julia> @testset "Foo Tests" begin
           @testset "Animals" begin
               @test foo("cat") == 9
               @test foo("dog") == foo("cat")
           end
           @testset "Arrays $i" for i in 1:3
               @test foo(zeros(i)) == i^2
               @test foo(fill(1.0, i)) == i^2
           end
       end;
Test Summary: | Pass  Total  Time
Foo Tests     |    8      8  0.0s

А также вызывать функции:

julia> f(x) = @test isone(x)
f (generic function with 1 method)

julia> @testset f(1);
Test Summary: | Pass  Total  Time
f             |    1      1  0.0s

Это может быть использовано для факторизации наборов тестов, что упрощает запуск отдельных наборов тестов путём запуска соответствующих функций. Обратите внимание, что в случае функций набор тестов получит имя вызываемой функции. В случае, если вложенный набор тестов не имеет ошибок, как произошло здесь, он будет скрыт в сводке, если не передан параметр verbose=true:

julia> @testset verbose = true "Foo Tests" begin
           @testset "Animals" begin
               @test foo("cat") == 9
               @test foo("dog") == foo("cat")
           end
           @testset "Arrays $i" for i in 1:3
               @test foo(zeros(i)) == i^2
               @test foo(fill(1.0, i)) == i^2
           end
       end;
Test Summary: | Pass  Total  Time
Foo Tests     |    8      8  0.0s
  Animals     |    2      2  0.0s
  Arrays 1    |    2      2  0.0s
  Arrays 2    |    2      2  0.0s
  Arrays 3    |    2      2  0.0s

Если у нас есть ошибка теста, будут показаны только подробности о неудачных наборах тестов:

julia> @testset "Foo Tests" begin
           @testset "Animals" begin
               @testset "Felines" begin
                   @test foo("cat") == 9
               end
               @testset "Canines" begin
                   @test foo("dog") == 9
               end
           end
           @testset "Arrays" begin
               @test foo(zeros(2)) == 4
               @test foo(fill(1.0, 4)) == 15
           end
       end

Arrays: Test Failed
  Expression: foo(fill(1.0, 4)) == 15
   Evaluated: 16 == 15
[...]
Test Summary: | Pass  Fail  Total  Time
Foo Tests     |    3     1      4  0.0s
  Animals     |    2            2  0.0s
  Arrays      |    1     1      2  0.0s
ERROR: Some tests did not pass: 3 passed, 1 failed, 0 errored, 0 broken.

Тестирование заявлений протокола

Можно использовать макрос @test_logs для проверки заявлений протокола или использовать TestLogger.

Test.@test_logsМакрос

@test_logs [log_patterns...] [keywords] expression

Собирает список записей протокола, сгенерированных expression с помощью collect_test_logs, проверяет, что они соответствуют последовательности log_patterns, и возвращает значение expression. keywords обеспечивают некоторое простое фильтрование записей протокола: ключевое слово min_level управляет минимальным уровнем протокола, который будет собираться для теста, ключевое слово match_mode определяет, как будет выполняться соответствие (по умолчанию :all проверяет, что все логи и шаблоны соответствуют друг другу попарно; используйте :any для проверки того, что шаблон соответствует хотя бы один раз в последовательности).

Самый полезный шаблон протокола — это простой кортеж вида (level,message). Можно использовать разное количество элементов кортежа для сопоставления других метаданных протокола, соответствующих аргументам, переданным AbstractLogger через функцию handle_message: (level,message,module,group,id,file,line). Настоящие элементы будут сопоставляться попарно с полями записей протокола с использованием == по умолчанию, со специальными случаями, что Symbol могут использоваться для стандартных уровней протокола, и Regex в шаблоне будут соответствовать строковым или символьным полям с использованием occursin.

Примеры

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

function foo(n)
    @info "Doing foo with n=$n"
    for i=1:n
        @debug "Iteration $i"
    end
    42
end

Мы можем проверить сообщение info с помощью

@test_logs (:info,"Doing foo with n=2") foo(2)

Если мы также хотели проверить сообщения отладки, их нужно включить с помощью ключевого слова min_level:

using Logging
@test_logs (:info,"Doing foo with n=2") (:debug,"Iteration 1") (:debug,"Iteration 2") min_level=Logging.Debug foo(2)

Если вы хотите проверить, что сгенерированы некоторые конкретные сообщения, игнорируя остальные, вы можете установить ключевое слово match_mode=:any:

using Logging
@test_logs (:info,) (:debug,"Iteration 42") min_level=Logging.Debug match_mode=:any foo(100)

Макрос может быть объединен с @test для проверки возвращаемого значения:

@test (@test_logs (:info,"Doing foo with n=2") foo(2)) == 42

Если вы хотите проверить отсутствие предупреждений, вы можете опустить указание шаблонов протоколов и соответственно установить min_level:

# test that the expression logs no messages when the logger level is warn:
@test_logs min_level=Logging.Warn @info("Some information") # passes
@test_logs min_level=Logging.Warn @warn("Some information") # fails

Если вы хотите проверить отсутствие предупреждений (или сообщений об ошибках) в stderr, которые не генерируются @warn, см. @test_nowarn.

Test.TestLoggerТип

TestLogger(; min_level=Info, catch_exceptions=false)

Создаёт TestLogger, который захватывает сообщения протокола в поле logs::Vector{LogRecord}.

Установите min_level для управления LogLevel, catch_exceptions для того, чтобы исключения, выброшенные во время генерации событий протокола, ловились, и respect_maxlog для того, чтобы следовать соглашению о регистрации сообщений с maxlog=n для некоторого целого числа n максимум n раз.

См. также: LogRecord.

Примеры

julia> using Test, Logging

julia> f() = @info "Hi" number=5;

julia> test_logger = TestLogger();

julia> with_logger(test_logger) do
           f()
           @info "Bye!"
       end

julia> @test test_logger.logs[1].message == "Hi"
Test Passed

julia> @test test_logger.logs[1].kwargs[:number] == 5
Test Passed

julia> @test test_logger.logs[2].message == "Bye!"
Test Passed

Test.LogRecordТип

LogRecord

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

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

Другие макросы тестов

Так как вычисления с плавающей точкой могут быть неточными, вы можете выполнить проверку приблизительного равенства, используя либо @test a ≈ b (где ≈, введённый с помощью автодополнения \approx, — функция isapprox) или использовать isapprox напрямую.

julia> @test 1 ≈ 0.999999999
Test Passed

julia> @test 1 ≈ 0.999999
Test Failed at none:1
  Expression: 1 ≈ 0.999999
   Evaluated: 1 ≈ 0.999999

ERROR: There was an error during testing

Вы можете указать относительные и абсолютные погрешности, установив ключевые аргументы rtol и atol макроса isapprox соответственно после сравнения ≈:

julia> @test 1 ≈ 0.999999  rtol=1e-5
Test Passed

Обратите внимание, что это не функция, специфичная для макроса ≈, а общая функция макроса @test: @test a <op> b key=val преобразуется макросом в @test op(a, b, key=val). Это, однако, особенно полезно для тестов ≈.

Test.@inferredМакрос

@inferred [AllowedType] f(x)

Тесты, проверяющие, что выражение вызова f(x) возвращает значение того же типа, который определил компилятор. Это полезно для проверки стабильности типов.

f(x) может быть любым выражением вызова. Возвращает результат f(x), если типы совпадают, и Error Result, если обнаруживаются разные типы.

Необязательно, AllowedType ослабляет тест, позволяя ему пройти, если тип f(x) соответствует предполагаемому типу с учётом AllowedType, или если возвращаемый тип является подтипом AllowedType. Это полезно для проверки стабильности типов функций, возвращающих небольшой союз, такой как Union{Nothing, T} или Union{Missing, T}.

julia> f(a) = a > 1 ? 1 : 1.0
f (generic function with 1 method)

julia> typeof(f(2))
Int64

julia> @code_warntype f(2)
MethodInstance for f(::Int64)
  from f(a) @ Main none:1
Arguments
  #self#::Core.Const(f)
  a::Int64
Body::UNION{FLOAT64, INT64}
1 ─ %1 = (a > 1)::Bool
└──      goto #3 if not %1
2 ─      return 1
3 ─      return 1.0

julia> @inferred f(2)
ERROR: return type Int64 does not match inferred return type Union{Float64, Int64}
[...]

julia> @inferred max(1, 2)
2

julia> g(a) = a < 10 ? missing : 1.0
g (generic function with 1 method)

julia> @inferred g(20)
ERROR: return type Float64 does not match inferred return type Union{Missing, Float64}
[...]

julia> @inferred Missing g(20)
1.0

julia> h(a) = a < 10 ? missing : f(a)
h (generic function with 1 method)

julia> @inferred Missing h(20)
ERROR: return type Int64 does not match inferred return type Union{Missing, Float64, Int64}
[...]

Test.@test_deprecatedМакрос

@test_deprecated [pattern] expression

Когда --depwarn=yes, проверяет, что expression генерирует предупреждение об устаревании и возвращает значение expression. Строка сообщения журнала будет сравниваться с pattern, по умолчанию r"deprecated"i.

Когда --depwarn=no, просто возвращает результат выполнения expression. Когда --depwarn=error, проверяет, что выбрасывается исключение ErrorException.

Примеры

# Deprecated in julia 0.7
@test_deprecated num2hex(1)

# The returned value can be tested by chaining with @test:
@test (@test_deprecated num2hex(1)) == "0000000000000001"

Test.@test_warnМакрос

@test_warn msg expr

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

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

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

Test.@test_nowarnМакрос

@test_nowarn expr

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

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

Разбитые тесты

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

Test.@test_brokenМакрос

@test_broken ex
@test_broken f(args...) key=val ...

Указывает тест, который должен пройти, но в настоящее время постоянно терпит неудачу. Проверяет, что выражение ex вычисляется до false или вызывает исключение. Возвращает Broken Result в случае успеха или Error Result в случае неудачи. Это эквивалентно @test ex broken=true.

Формат @test_broken f(args...) key=val... работает, как и для макроса @test.

Примеры

julia> @test_broken 1 == 2
Test Broken
  Expression: 1 == 2

julia> @test_broken 1 == 2 atol=0.1
Test Broken
  Expression: ==(1, 2, atol = 0.1)

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

Test.@test_skipМакрос

@test_skip ex
@test_skip f(args...) key=val ...

Помечает тест, который не должен выполняться, но должен быть включён в отчет о результатах тестов как Broken. Это может быть полезно для тестов, которые периодически терпят неудачу, или для тестов ещё не реализованной функциональности. Это эквивалентно @test ex skip=true.

Формат @test_skip f(args...) key=val... работает, как и для макроса @test.

Примеры

julia> @test_skip 1 == 2
Test Broken
  Skipped: 1 == 2

julia> @test_skip 1 == 2 atol=0.1
Test Broken
  Skipped: ==(1, 2, atol = 0.1)

Типы результатов тестов

Test.ResultТип

Test.Result

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

Test.PassТип

Test.Pass <: Test.Result

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

Test.FailТип

Test.Fail <: Test.Result

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

Test.ErrorТип

Test.Error <: Test.Result

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

Test.BrokenТип

Test.Broken <: Test.Result

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

Создание настраиваемых типов AbstractTestSet

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

Test.recordФункция

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

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

Test.finishФункция

finish(ts::AbstractTestSet)

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

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

if get_testset_depth() != 0
    # Attach this test set to the parent test set
    parent_ts = get_testset()
    record(parent_ts, self)
    return self
end

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

Test.get_testsetФункция

get_testset()

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

Test.get_testset_depthФункция

get_testset_depth()

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

Test также гарантирует, что вложенные вызовы @testset используют тот же подтип AbstractTestSet , что и их родительский элемент, если явно не задано иное. Он не распространяет какие-либо свойства набора тестов. Поведение наследования свойств можно реализовать в пакетах, использующих инфраструктуру стека, предоставляемую Test.

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

import Test: Test, record, finish
using Test: AbstractTestSet, Result, Pass, Fail, Error
using Test: get_testset_depth, get_testset
struct CustomTestSet <: Test.AbstractTestSet
    description::AbstractString
    foo::Int
    results::Vector
    # constructor takes a description string and options keyword arguments
    CustomTestSet(desc; foo=1) = new(desc, foo, [])
end

record(ts::CustomTestSet, child::AbstractTestSet) = push!(ts.results, child)
record(ts::CustomTestSet, res::Result) = push!(ts.results, res)
function finish(ts::CustomTestSet)
    # just record if we're not the top-level parent
    if get_testset_depth() > 0
        record(get_testset(), ts)
        return ts
    end

    # so the results are printed if we are at the top level
    Test.print_test_results(ts)
    return ts
end

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

@testset CustomTestSet foo=4 "custom testset inner 2" begin
    # this testset should inherit the type, but not the argument.
    @testset "custom testset inner" begin
        @test true
    end
end

Чтобы использовать настраиваемый набор тестов и отображать записанные результаты в качестве части любого внешнего набора тестов по умолчанию, также определите Test.get_test_counts . Это может выглядеть так:

using Test: AbstractTestSet, Pass, Fail, Error, Broken, get_test_counts, TestCounts, format_duration

function Test.get_test_counts(ts::CustomTestSet)
    passes, fails, errors, broken = 0, 0, 0, 0
    # cumulative results
    c_passes, c_fails, c_errors, c_broken = 0, 0, 0, 0

    for t in ts.results
        # count up results
        isa(t, Pass)   && (passes += 1)
        isa(t, Fail)   && (fails  += 1)
        isa(t, Error)  && (errors += 1)
        isa(t, Broken) && (broken += 1)
        # handle children
        if isa(t, AbstractTestSet)
            tc = get_test_counts(t)::TestCounts
            c_passes += tc.passes + tc.cumulative_passes
            c_fails  += tc.fails + tc.cumulative_fails
            c_errors += tc.errors + tc.cumulative_errors
            c_broken += tc.broken + tc.cumulative_broken
        end
    end
    # get a duration, if we have one
    duration = format_duration(ts)
    return TestCounts(true, passes, fails, errors, broken, c_passes, c_fails, c_errors, c_broken, duration)
end

Test.TestCountsТип

TestCounts

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

Поля:

  • customized: Признак того, была ли функция get_test_counts настраиваемой для набора тестов, для которого этот объект подсчета предназначен. Если пользовательский метод был определён, всегда передавайте true в конструктор.
  • passes: Количество пройденных вызовов @test.
  • fails: Количество неудачных вызовов @test.
  • errors: Количество вызовов @test с ошибками.
  • broken: Количество разбитых вызовов @test.
  • passes: Накопленное количество пройденных вызовов @test.
  • fails: Накопленное количество неудачных вызовов @test.
  • errors: Накопленное количество вызовов @test с ошибками.
  • broken: Накопленное количество разбитых вызовов @test.
  • duration: Общее время выполнения AbstractTestSet в формате String.

Test.get_test_countsФункция

" gettestcounts(::AbstractTestSet) -> TestCounts

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

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

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

Test.format_durationФункция

format_duration(::AbstractTestSet)

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

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

Test.print_test_resultsФункция

print_test_results(ts::AbstractTestSet, depth_pad=0)

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

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

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

Утилиты для тестирования

Test.GenericArrayТип

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

Test.GenericDictТип

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

Test.GenericOrderТип

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

Test.GenericSetТип

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

Test.GenericStringТип

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

Test.detect_ambiguitiesФункция

detect_ambiguities(mod1, mod2...; recursive=false,
                                  ambiguous_bottom=false,
                                  allowed_undefineds=nothing)

Возвращает вектор пар (Method,Method) неоднозначных методов, определённых в указанных модулях. Используйте recursive=true для тестирования во всех подмодулях.

ambiguous_bottom управляет включением неоднозначностей, вызванных только параметрами типа Union{}; в большинстве случаев вы, вероятно, захотите установить это значение в false. См. Base.isambiguous.

См. Test.detect_unbound_args для объяснения allowed_undefineds.

allowed_undefineds требует как минимум Julia 1.8.

Test.detect_unbound_argsФункция

detect_unbound_args(mod1, mod2...; recursive=false, allowed_undefineds=nothing)

Возвращает вектор Method которые могут иметь неопределённые параметры типа. Используйте recursive=true для тестирования во всех подмодулях.

По умолчанию любые неопределённые символы вызывают предупреждение. Это предупреждение можно подавить, предоставив коллекцию GlobalRef для которых предупреждение можно пропустить. Например,

allowed_undefineds = Set([GlobalRef(Base, :active_repl),
                          GlobalRef(Base, :active_repl_backend)])

подавятся предупреждения о Base.active_repl и Base.active_repl_backend.

allowed_undefineds требует как минимум Julia 1.8.

Процесс тестирования пакетов

Используя инструменты, доступные в предыдущих разделах, вот потенциальный процесс создания пакета и добавления тестов к нему.

Создание примера пакета

Для этого процесса мы создадим пакет под названием Example.

pkg> generate Example
shell> cd Example
shell> mkdir test
pkg> activate .

Создание образцовых функций

Основное требование для тестирования пакета — наличие функциональности для тестирования. Для этого мы добавим несколько простых функций в Example, которые можно протестировать. Добавьте следующее в src/Example.jl:

module Example

function greet()
    "Hello world!"
end

function simple_add(a, b)
    a + b
end

function type_multiply(a::Float64, b::Float64)
    a * b
end

export greet, simple_add, type_multiply

end

Создание тестовой среды

Из корневой директории пакета Example, перейдите в каталог test, активируйте там новую среду и добавьте пакет Test в эту среду:

shell> cd test
pkg> activate .
(test) pkg> add Test

Тестирование нашего пакета

Теперь мы готовы добавить тесты в Example. Стандартной практикой является создание файла в каталоге test с именем runtests.jl, содержащего наборы тестов, которые мы хотим запустить. Создайте этот файл в каталоге test и добавьте в него следующий код:

using Example
using Test

@testset "Example tests" begin

    @testset "Math tests" begin
        include("math_tests.jl")
    end

    @testset "Greeting tests" begin
        include("greeting_tests.jl")
    end
end

Нам нужно будет создать эти два файла, math_tests.jl и greeting_tests.jl, и добавить в них некоторые тесты.

Примечание: Обратите внимание, что нам не нужно было указывать добавить Example в среду test's Project.toml. Это преимущество системы тестирования Julia, о котором вы можете подробнее прочитать здесь.

Написание тестов для math_tests.jl

Используя наши знания о Test.jl, вот примеры тестов, которые мы можем добавить в math_tests.jl:

@testset "Testset 1" begin
    @test 2 == simple_add(1, 1)
    @test 3.5 == simple_add(1, 2.5)
        @test_throws MethodError simple_add(1, "A")
        @test_throws MethodError simple_add(1, 2, 3)
end

@testset "Testset 2" begin
    @test 1.0 == type_multiply(1.0, 1.0)
        @test isa(type_multiply(2.0, 2.0), Float64)
    @test_throws MethodError type_multiply(1, 2.5)
end

Написание тестов для greeting_tests.jl

Используя наши знания о Test.jl, вот примеры тестов, которые мы можем добавить в greeting_tests.jl:

@testset "Testset 3" begin
    @test "Hello world!" == greet()
    @test_throws MethodError greet("Antonia")
end

Тестирование нашего пакета

Теперь, когда мы добавили наши тесты и скрипт runtests.jl в test, мы можем протестировать наш пакет Example, вернувшись в корень среды пакета Example и повторно активировав среду Example:

shell> cd ..
pkg> activate .

Оттуда мы можем наконец запустить наш набор тестов следующим образом:

(Example) pkg> test
     Testing Example
      Status `/tmp/jl_Yngpvy/Project.toml`
  [fa318bd2] Example v0.1.0 `/home/src/Projects/tmp/errata/Example`
  [8dfed614] Test `@stdlib/Test`
      Status `/tmp/jl_Yngpvy/Manifest.toml`
  [fa318bd2] Example v0.1.0 `/home/src/Projects/tmp/errata/Example`
  [2a0f44e3] Base64 `@stdlib/Base64`
  [b77e0a4c] InteractiveUtils `@stdlib/InteractiveUtils`
  [56ddb016] Logging `@stdlib/Logging`
  [d6f4376e] Markdown `@stdlib/Markdown`
  [9a3f8284] Random `@stdlib/Random`
  [ea8e919c] SHA `@stdlib/SHA`
  [9e88b42a] Serialization `@stdlib/Serialization`
  [8dfed614] Test `@stdlib/Test`
     Testing Running tests...
Test Summary: | Pass  Total
Example tests |    9      9
     Testing Example tests passed

И если всё прошло правильно, вы должны увидеть похожий вывод, как выше. С помощью Test.jl, можно добавить более сложные тесты для пакетов, но это должно направить разработчиков к тому, как начать тестирование собственных созданных пакетов.

Покрытие кода

Отслеживание покрытия кода во время тестов можно включить, используя флаг pkg> test --coverage (или на более низком уровне, используя аргумент julia --code-coverage). По умолчанию это включено в GitHub действии julia-runtest.

Для оценки покрытия можно либо вручную проверить файлы .cov, которые генерируются рядом с исходными файлами локально, либо в CI использовать GitHub действие julia-processcoverage.

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

© 2009–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.11/stdlib/Test/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API