На WWDC 2024 одним из самых интересных инструментов был Swift Testing, который делает тестирование Swift-кода более мощным, чем когда-либо. С его помощью разработчики могут уверенно создавать высококачественные продукты с минимальным количеством кода.
Swift Testing включает в себя такие инновационные функции, как встроенная поддержка параллелизма, параллельное выполнение тестов и внедрение макросов тестирования.
Предварительные условия
Прежде чем использовать Swift Testing, убедитесь, что у вас есть Xcode 16.0+ и Swift 6.0+.
Как добавить цель Swift Testing?
Чтобы начать работу с Swift Testing, необходимо выбрать Swift Testing with XCTest UI Tests при создании нового проекта.
Это позволяет писать тестовые случаи в соответствующих тестовых таргетах.
Строительные блоки
Очень важно убедиться в том, что ваши тесты читабельны, особенно по мере усложнения кодовой базы. В Swift Testing добавлены некоторые функции, помогающие в этом, поэтому вам будет проще писать выразительные тесты.
В общей сложности в нем присутствует 4 строительных блока. Ниже вы найдете подробную информацию о каждом из них.
1. Функции тестирования
Функция, аннотированная @Test
, обозначает тестовую функцию. Где бы вы ни использовали эту аннотацию, Xcode покажет вам бриллиантовую кнопку.
- Это может быть глобальная функция или метод класса.
- Она может быть помечена как async или throws.
2. Ожидания (Expectations)
Макрос #expect
Макрос #expect
проверяет, истинно ли ожидаемое условие или нет, с помощью операторов и выражений. Он может обрабатывать сложные проверки.
Если произошел сбой, будет выведена подробная информация о нем и это позволит вам понять, что пошло не так.
Макрос #expect(.throws: (any Error).self) { }
Вместо того чтобы писать оператор catch
вручную, используйте макросы expect throws
, которые делают обработку исключений проще и эффективнее.
Если этот блок не выбросит ни одной ошибки, он завершится неудачей. В противном случае он будет успешным.
/// This snippet provides functions to demonstrate testing scenarios with the #expect(throws:) macros. enum APIError: Error { case resourceNotFound } func throwingAnError() throws { print("A function that throw an error") throw APIError.resourceNotFound } func notThrowingAnError() throws { print("A function that does not throw an error") }
Вы можете взглянуть на тестовый пример ниже:
В приведенном выше примере функция, выбрасывающая ошибку, успешно прошла тест, а функция, не выбрасывающая ошибок, не прошла тест.
Макрос #require
Его можно использовать, если вы хотите завершить тест раньше времени, если ожидание не сработало. Он останавливает тест, если заданное условие ложно, или разворачивает необязательные значения и прекращает тестирование, если выражение оказывается равным nil
.
В нем есть ключевой оператор try
, который выбрасывает ошибку, если выражение ложно или содержит значение nil
, останавливая дальнейшее выполнение.
В данном случае ожидание полагается на требуемое значение, которое оказалось равным нулю, что привело к остановке выполнения теста.
3. Трейты (Traits)
Трейты помогают добавить описательную информацию о тестах и позволяют настраивать, когда или будет ли вообще выполняться тест.
Выберите .tags()
вместо @Test("test name")
, чтобы включить или исключить тесты.
Используйте трейты с умом, не в каждой ситуации нужны теги. Например, если вы имеете дело с условиями рантайма, лучше использовать .enable()
вместо .tag()
.
4. Тестовые наборы (Test Suites)
Наборы тестов группируют тестовые функции и другие наборы.
Логика установки и завершения работы реализована с помощью init()
и deinit()
, соответственно. init()
вызывается перед запуском теста, а deinit()
— сразу после его завершения.
Каждая тестовая функция запускается независимо на своем экземпляре, поэтому они никогда не обмениваются данными по ошибке.
Пример тестового набора:
@Suite("Basic calculator operations") struct TestCalculation { var calc = Calculator() @Test func testAddition() async throws { #expect(calc.addition(valA: 10, valB: 20) == 30) } @Test func testSubtraction() async throws { #expect(calc.subtraction(valA: 20, valB: 10) == 10) } @Suite("Advance calculator operations") struct AdvanceTestCalculation { var calc = Calculator() @Test func testPower() async throws { #expect(calc.power(radix: 2, power: 3) == 8) } } }
Calculator.swift используется в TestCalculation.swift:
struct Calculator { func addition(valA: Int, valB: Int) -> Int { return valA + valB } func subtraction(valA: Int, valB: Int) -> Int { return valA - valB } func power(radix: Int, power: Int) -> Int { var tempBase = radix var tempPower = power var result: Int = 1 while (tempPower != 0) { if (tempPower % 2 == 1) { result *= tempBase } tempPower = tempPower >> 1 tempBase *= tempBase } return result } }
Здесь вы можете увидеть оба набора в навигаторе тестов с указанными именами тестов. Поднабор отображается иерархически под родительским набором.
Параметризованное тестирование
Swift Testing предлагает параметризованные тесты, которые позволяют запускать одну тестовую функцию с несколькими различными аргументами. Это делает тестирование более эффективным и управляемым.
Параметризованное тестирование помогает обеспечить тщательное покрытие тестами без избыточного кода или создания чрезмерного количества отдельных тестовых случаев.
Давайте получим практический опыт работы с параметризованными тестами.
Вышеописанная ошибка указывает на неправильный синтаксис параметризованных тестов в Swift-тестировании. Мы должны использовать аргументы в атрибуте @Test
.
import Testing struct ParameterizedTests { @Test(arguments: [ "abc123@gmail.com", "abc123gmail.com", "abc123@.com" ]) func testValidEmail(email: String) { #expect(email.contains("@") && email.contains("."), "Invalid email format") } }
В приведенном выше примере указано три аргумента. Если два из них верны, а один неверен, то весь тестовый пример будет помечен как неудачный. Однако в навигаторе тестов можно увидеть, какие аргументы привели к неудаче.
Вместе с аргументами можно указать дополнительные трейты или имена отображения.
@Test("Email validation", arguments: [ "abc123@gmail.com", "abc123gmail.com", "abc123@.com" ]) func testValidEmail(email: String) { #expect(email.contains("@") && email.contains("."), "Invalid email format") }
Положительные аспекты:
- Просмотр подробной информации о каждом аргументе в результатах.
- Повторное выполнение определенных аргументов при необходимости отладки функции.
- Тесты могут выполняться более эффективно за счет параллельного выполнения каждого аргумента.
Сравнение XCTest и Swift Test
Если вы знакомы с XCTest, вам может быть интересно, чем XCTest отличается от Swift Test.
Сравнение строительных блоков в тестах Swift и XCTest.
1. Тестовая функция
Ожидания в обоих фреймворках сильно отличаются.
XCTest использует специальные проверки, начиная с XCTAssert, чтобы убедиться, что все работает так, как ожидается.
Однако в Swift Test есть два основных макроса — #expect
и #require
.
Советы по миграции с XCTest
- XCTest и Swift Test могут быть написаны в одной цели. Нет необходимости создавать новую цель.
- Если несколько методов имеют схожую структуру, их можно объединить в один тест и сделать эту функцию параметризованной.
- Объедините отдельные классы XCTest в одну глобальную функцию
@Test
. - Префикс
test
в именах функций больше в Swift Test не нужен. Вы можете удалить его из функций. - Избегайте использования XCTAssertion в Swift-тестировании и макросов тестирования типа
#expect
и#require
в XCTest.
Продолжайте использовать XCTest
Отказаться от XCTest сложно, если:
- Тесты используют API автоматизации работы с UI, например XCUIApplication, или API метрик производительности, например XCTMetric, которые не поддерживаются в Swift тестировании.
- Тесты могут быть написаны только на Objective-C.
Заключение
Swift Testing предлагает выразительный API с макросами, позволяющий объявлять поведение тестов с минимальным количеством кода. API #expect использует выражения и операторы Swift для проверки предоставленных выражений. Параметризованные тесты помогают сократить количество избыточного кода, позволяя использовать код с похожей структурой. Кроме того, Swift Testing поддерживает параллельное тестирование, что повышает общую эффективность и производительность тестирования. Однако если вы хотите включить сериализованное тестирование, вам необходимо использовать трейт, чтобы сделать его сериализованным.