Трейты (признки, типажи, traits) позволяют настраивать или аннотировать тесты с помощью фреймворка Swift Testing, анонсированного на WWDC 2024. Они также позволяют настраивать общее поведение, например условия, оцениваемые в рантайме, для пропуска определенных тестов.
Прежде чем изучать эту особенность Swift Testing, я рекомендую вам прочитать статью о введении в Swift Testing. Давайте погрузимся!
Что такое трейты в Swift Testing?
Используя фреймворк Swift Testing, вы можете применять трейты для настройки или аннотирования тестов. Существуют различные встроенные трейты, а также вы можете генерировать собственные.
В этой статье мы подробнее рассмотрим встроенные трейты, но сначала давайте узнаем, как можно добавить трейты в тест. Мы можем добавить трейты с помощью инициализатора макроса @Test:
Приведенный выше пример представляет собой автозаполнение для включенного трейта. Этот признак представляет собой так называемый ConditionTrait
, который является структурой, соответствующей протоколу TestTrait
и SuiteTrait
. Это означает, что вы можете использовать enabled
признак как в тестах, так и в наборах.
Если вам интересно узнать, как реализованы эти трейты, я рекомендую вам изучить репозиторий с открытым исходным кодом swiftlang/swift-testing. Да, именно так: фреймворк Swift Testing с открытым исходным кодом, что позволяет вам вносить свой вклад и учиться на его реализации.
Включение и отключение тестов
Если тест нестабилен или требует выполнения только в определенных условиях, вы, скорее всего, захотите включить или отключить его на основе условий, оцениваемых во время выполнения. Мы можем сделать это с помощью enabled
или disabled
трейта:
@Test(.disabled("This test is flaky and will be solved later.")) func someFlakyTest() { /// ... }
Вы можете использовать инициализатор трейта, чтобы добавить комментарий, объясняющий, почему тест нестабилен и почему он отключен.
В приведенном выше примере мы отключили тест статически. Однако вы также можете решить отключить тест на основе условия:
struct TestHelper { static var isRunningOnSimulator: Bool { #if targetEnvironment(simulator) return false #else return true #endif } } @Test(.disabled(if: TestHelper.isRunningOnSimulator, "This test will only run on an actual device")) func testingDeviceOnlyFunctionality() { /// ... }
В данном случае мы хотим, чтобы тест выполнялся только в том случае, если мы проводим испытания на реальном устройстве.
Ассоциирование ошибок с тестами
По моему опыту, тесты часто отключают из-за нестабильности или конкретной ошибки. Помимо добавления комментария, объясняющего, почему вы отключили тест, полезно также ссылаться на открытый тикет ошибки для тех, кто обратится к коду в будущем. Для этого мы можем использовать трейт ошибки:
@Test( .disabled("This test is flaky and will be solved later."), .bug("https://github.com/AvdLee/RocketSimApp/issues/179") ) func someFlakyTest() { /// ... }
Другой распространенный вариант использования — ссылка на ошибку, проверяемую тестом:
@Test( .bug("https://github.com/AvdLee/RocketSimApp/issues/517", id: 517, "+0% shows in red for build insight") ) func zeroChangeTextColor() { /// Test logic to assert color is black for 0% }
В примере выше вы можете видеть, что я написал тест для проверки цвета метки в RocketSim. Я большой поклонник исправления ошибок с помощью тестов, когда я сначала воспроизвожу ошибку с помощью юнит-теста. Это предотвращает повторное появление ошибки в дальнейшем и облегчает проверку того, что исправление сработало.
Трейт ошибки позволяет вам ссылаться на тикет в вашей системе отслеживания ошибок, задавать идентификатор тикета и добавлять описательное название для тикета. Чтобы ошибка отображалась в отчете о тестировании, необходимо указать либо идентификатор тикета, либо его URL:
Swift Testing поддерживает несколько систем отслеживания ошибок, включая фидбек помощника Apple:
@Test( .disabled("No longer works on iOS 18"), .bug(id: "FB12345") ) func waitingForAppleToFixThis() { }
Более подробную информацию обо всех поддерживаемых системах можно найти в документации Apple Interpreting Bug Identifiers.
Ограничение времени выполнения тестов
В некоторых случаях вы можете захотеть ограничить время выполнения конкретного теста. Представьте, что у вас есть тест, который загружает определенные файлы, но вы хотите, чтобы он завершился не менее чем за 10 минут. Для этого можно использовать признак timeLimit
:
@Test(.timeLimit(.minutes(10))) func uploadFiles() { /// ... }
Здесь действует несколько правил:
- Установка ограничения времени для тестового набора автоматически распространяется на все отдельные тестовые функции и любые вложенные тестовые наборы внутри него.
- Если вы устанавливаете ограничение времени для параметризованной тестовой функции, оно применяется отдельно к каждому вызову. Таким образом, если определенные аргументы приводят к сбоям, успешные аргументы не будут ошибочно помечены как неудачные.
Последовательный запуск тестов
Наконец, у вас могут быть тесты, которые влияют друг на друга из-за общего ресурса. Например, я создал бэкенд с помощью Swift & Vapor, и некоторые из моих тестов работают с реальной базой данных. Если эти тесты выполняются параллельно, они будут манипулировать базой данных и влиять на результаты других тестов. В итоге это приводит к нестабильной работе тестов и ненадежному поведению.
Чтобы решить эту проблему, мы можем пометить набор тестов для последовательного выполнения:
@Suite(.serialized) struct SwiftBackendTests { /// ... }
Фреймворк Swift Testing по умолчанию запускает тесты параллельно, что обеспечивает гораздо более высокую производительность. Поэтому тщательно подумайте, стоит ли вам сериализовать набор тестов или можно переписать тесты для поддержки параллельного выполнения.
Заключение
Трейты в Swift Testing позволяют аннотировать и настраивать тестовые функции. Вы можете отключать тесты на основе условий, связывать определенные тикеты в баг-трекере или устанавливать ограничение по времени для длительных тестов.