Кто управляет тестами?
Возможно ты задавал себе вопрос: «Кто-то же запускает написанные мной тесты, но кто именно» ? В этой главе я дам более точный ответ на заданный вопрос.
Глава про управление тестами разделена на 3 сценария:
Введение в типы данных
Runner — тип данных отвечающий за выполнение тестов в соответствии с заданой конфигурацией и планом. Основная задача — организовать и запустить массив тестов, управлять выполнением и обрабатывать результаты выполнения тестов.
Каждый запуск теста состоит из плана, массива тестов и конфигурации. Об этом ты прочитаешь ниже, но в начале рассмотрим все свойства и инициализаторы:
public struct Runner: Sendable {
public var plan: Plan
public var tests: [Test] { plan.steps.map(\.test) }
public var configuration: Configuration
// 1-ый инит
public init(testing tests: [Test], configuration: Configuration = .init()) async {
let plan = await Plan(tests: tests, configuration: configuration)
self.init(plan: plan, configuration: configuration)
}
// 2-ой инит
public init(plan: Plan, configuration: Configuration = .init()) {
self.plan = plan
self.configuration = configuration
}
// 3-ий инит
public init(configuration: Configuration = .init()) async {
let plan = await Plan(configuration: configuration)
self.init(plan: plan, configuration: configuration)
}
}
Runner представляет собой структуру данных c 3 свойствами:
- Свойство
plan
отвечает за план действий при запуске тестов. - Свойство
tests
хранящее массив тестов, которые будут выполнены. - Свойство
configuration
отвечающее за настройку для подготовки и запуска тестов.
Помимо этого существует 3 инициализатора, каждый из которых не обходится без обязательной конфигурации и плана.
Во всех случаях используется конфигурация по умолчанию: Configuration = .init()
.
important
Ты не можешь создавать собственный план или конфигурацию для передачи в Runner.
План
Plan — тип данных описывающий план для Runner
, а именно какие тесты будут запущены, в каком порядке и с каким действием (Action
):
extension Runner {
public struct Plan: Sendable {
public enum Action: Sendable {
public struct RunOptions: Sendable, Codable {
// ...
}
// ...
}
// ...
public struct Step: Sendable {
public var test: Test
public var action: Action
}
}
}
Структура Plan
представляет собой:
Step
: представляет собой шаг в ходе выполнения. Каждый шаг состоит из самого теста, который должен быть выполнен и действия, которое должно быть выполнено для этого теста.Graph
: создает иерархию шагов (Step)- Другие вспомогательные методы, например: метод отвечающий за рекурсивное применение действий (
Action
).
Тип данных Plan
структурирует тесты в виде графа:
var stepGraph: Graph<String, Step?>
public var steps: [Step] {
stepGraph
.compactMap(\.value)
.sorted { $0.test.sourceLocation < $1.test.sourceLocation }
}
И как я ранее сказал, в каждом плане есть действие, которое требуется сделать с тестом:
enum Action: Sendable {
case run(options: RunOptions)
indirect case skip(_ skipInfo: SkipInfo)
indirect case recordIssue(_ issue: Issue)
}
Run
: тест будет выполнен.Skip
: тест будет пропущен по причинеSkipInfo
.RecordIssue
: запись проблемы по которой тест не был выполнен.
Конфигурация
Configuration — отвечает за настройку и управление поведением тестов. Данная структура отвечает за конкурентное или последовательное выполнение тестов, устанавливает ограничение по времени для выполнения тестов, отвечает за политику повторого повторения и другое:
public struct Configuration: Sendable {
public init() {}
public var isParallelizationEnabled: Bool = true
public var repetitionPolicy: RepetitionPolicy = .once
public var defaultSynchronousIsolationContext: (any Actor)? = nil
public var maximumTestTimeLimit: Duration? { /* ... */ }
public var exitTestHandler: ExitTest.Handler = { exitTest in
// ...
}
// ...
Инициализатор пустой, т.е. не содержит свойств для начальной настройки. Вручную у тебя нет возможности создать собственную конфигурация для тестов, как я написал ранее.
Запуск теста
Наконец-то ты добрался до секции запуска.
Как ты знаешь, в любых язык программирования существует EntryPoint
.
Запуск любой программы начинается с точки входа. Это касается и Swift Testing, поскольку
это тоже программа.
Точка входа (EntryPoint) — адрес в оперативной памяти, с которого начинается выполнение программы.
После того, как ты написал свой первый тест и запустил его, происходит следующее:
- Создается конфигурация по умолчанию в методе
configurationForEntryPoint(from:)
- Запускается
Runner
с указанной конфигурацией - Значение
runner.tests
присвается константеtests
- Происходит запуск тестов с помощью метода
await runner.run()
public func configurationForEntryPoint(from args: __CommandLineArguments_v0) throws -> Configuration {
var configuration = Configuration()
configuration.isParallelizationEnabled = args.parallel ?? true
// ...
}
func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Handler?) async -> CInt {
// ...
let tests: [Test]
var configuration = try configurationForEntryPoint(from: args)
let runner = await Runner(configuration: configuration)
tests = runner.tests
await runner.run()
}
public func __swiftPMEntryPoint(passing args: __CommandLineArguments_v0? = nil) async -> CInt {
#if !SWT_NO_FILE_IO
// Ensure that stdout is line- rather than block-buffered. Swift Package
// Manager reroutes standard I/O through pipes, so we tend to end up with
// block-buffered streams.
FileHandle.stdout.withUnsafeCFILEHandle { stdout in
_ = setvbuf(stdout, nil, _IOLBF, Int(BUFSIZ))
}
#endif
return await entryPoint(passing: args, eventHandler: nil)
}
await Testing.__swiftPMEntryPoint() as Never
Конечно, на деле запуск тестов не такой простой, как я описал.
Помимо этого происходят различные проверки и глава о запуске превратится в главу о проверках перед запуском.
Именно поэтому я намеренно упростил объяснение запуска тестов с помощью EntryPoint
.
note
Если есть желание ознакомится более подробно, ты всегда можешь прочитать исходный код Swift Testing.
С первым шагом справились и тесты запущены, но возникает следующий вопрос: «Как завершить тест упехом или неудачей»?
Завершение теста и выход
После того, как ты запустил тест, было бы славно его завершить. Для такого случая, существует специальный тип данных.
ExitTest — тип данных отвечающий за завершение теста.
public typealias ExitTest = __ExitTest
public struct __ExitTest: Sendable, ~Copyable {
// ...
static func handlerForEntryPoint() -> Handler {
// ...
}
}
В методе конфигурация вызывает метод для выхода из тестов: handlerForEntryPoint()
configuration.exitTestHandler = ExitTest.handlerForEntryPoint()
Таким образом, каждый из тестов завершается каким-то результатом. И сразу следует спросить, а какой из результатов завершения существует?
Для ответа на этот вопрос посмотри на перечисление ExitCondition
:
public enum ExitCondition: Sendable {
public static var success: Self {
.exitCode(EXIT_SUCCESS)
}
case failure
case exitCode(_ exitCode: CInt)
case signal(_ signal: CInt)
}
В самых простых вариантах тест завершается успехом .success
или неудачей .failure
.
Существуют другие варианты выхода из теста один из которых выход с помощью кода выхода, а другой с помощью сигнала.
Код выхода (возврата) — целое число, возвращаемое программой после ее завершения.
Статическое свойство .success
завершает тест с помощью кода выхода .exitCode(EXIT_SUCCESS)
.
В свою очередь EXIT_SUCCESS
равен нулю:
#define EXIT_FAILURE 1
#define EXIT_SUCCESS 0
#expect(EXIT_SUCCESS == .zero)