Как стать автором
Обновить

Введение в тестирование на Swift Testing

Уровень сложностиСредний
Время на прочтение17 мин
Количество просмотров1.9K

Обзор тестирования в Swift

Тестирование кода является важной и неотъемлемой частью процесса разработки программного обеспечения. Оно позволяет разработчикам проверять функциональность своих программ, выявлять ошибки и обеспечивать стабильную работу кода при внесении изменений. В экосистеме Swift разработчики могут воспользоваться несколькими инструментами для написания тестов, такими как XCTest, а также новыми библиотеками, такими как Testing.

XCTest является основным инструментом для тестирования в Swift и широко используется разработчиками. Однако, новая библиотека Testing предлагает дополнительные возможности и синтаксический сахар, который делает процесс тестирования еще более удобным и мощным. В данной статье мы рассмотрим основные аспекты тестирования на Swift, включая использование библиотеки Testing.

Зачем нужны тесты и какие виды тестов существуют

Тесты необходимы по нескольким причинам:

  • Обеспечение корректности работы кода: Тесты помогают убедиться, что ваш код работает так, как ожидается. Это особенно важно в больших проектах, где один неправильный шаг может привести к множеству проблем.

  • Защита от регрессий: Когда вы вносите изменения в код, тесты помогают убедиться, что новые изменения не сломали существующую функциональность.

  • Документация поведения системы: Тесты могут служить живой документацией для вашего кода, показывая, как именно он должен работать.

  • Повышение уверенности разработчиков: Наличие тестов позволяет разработчикам быть уверенными в том, что их изменения не приводят к неожиданным последствиям.

Существует несколько основных видов тестов:

  • Юнит-тесты (Unit Tests): Тестируют отдельные компоненты или функции кода. Эти тесты изолированы от других частей системы и проверяют работу конкретных функций или методов.

  • Интеграционные тесты (Integration Tests): Проверяют взаимодействие между различными частями системы. Эти тесты помогают убедиться, что модули системы работают вместе правильно.

  • Системные тесты (System Tests, E2E): Проверяют систему в целом. Эти тесты обычно включают проверку всех аспектов системы, включая пользовательский интерфейс.

  • Приемочные тесты (Acceptance Tests, QA): Валидируют систему с точки зрения требований пользователя. Эти тесты помогают убедиться, что система удовлетворяет требованиям конечного пользователя.

Основные понятия: test target, test function, test suite

Чтобы успешно писать тесты в Swift, необходимо понять несколько основных понятий:

  • Test Target: В Xcode для тестирования создается специальная цель (target), которая содержит все ваши тесты. Это позволяет отделить тесты от основной кода базы и управлять ими отдельно. Для добавления тестов в проект, необходимо создать новый test target в вашем Xcode проекте.

  • Test Function: Это отдельная функция, которая содержит проверочный код. В XCTest такие функции помечаются специальным префиксом test, например, func testExample(). В библиотеке Testing для этого используется атрибут @Test. Эти функции выполняются тестовым фреймворком и сообщают о результатах выполнения.

import Testing

@Test
func testExample() {
    let result = add(2, 3)
    #expect(result == 5)
}
  • Test Suite: Группа тестов, объединенных для совместного выполнения. В XCTest вы можете создавать тестовые наборы, наследуясь от XCTestCase. В библиотеке Testing тестовые наборы можно организовывать в типы с аннотацией @Suite. При работе с большим количеством тестовых функций организация их в тестовые наборы может значительно повысить ясность и управляемость. @Suite позволяет создавать тестовые наборы, которые представляют собой типы, содержащие несколько тестовых функций. Такой подход не только улучшает структуру, но и позволяет расширить возможности настройки и контроля над выполнением тестов.

import Testing

@Suite
struct ArithmeticTests {
    @Test
    func testAddition() {
        let result = add(2, 3)
        #expect(result == 5)
    }

    @Test
    func testSubtraction() {
        let result = subtract(5, 3)
        #expect(result == 2)
    }
}

Работа с библиотекой тестирования

Подключение библиотеки Testing

Для начала работы с библиотекой Testing необходимо импортировать её в файл с тестами. (Используйте XCode 16+)

import Testing

Важно: Импортируйте библиотеку тестирования только в тестовые цели. Импортирование её в целевые приложения, библиотеки или бинарные файлы не поддерживается и не рекомендуется.

Определение тестовой функции

Чтобы определить тестовую функцию, необходимо создать функцию Swift, которая не принимает аргументы и перед её именем поставить атрибут @Test:

@Test func foodTruckExists() {
  // Логика теста
}

Также можно задать пользовательское имя теста:

@Test("Food truck exists") func foodTruckExists() { ... }

Асинхронные и бросающие тесты

Тестовые функции могут быть асинхронными (async) и бросающими исключения (throws), что позволяет выполнять асинхронные операции и обрабатывать ошибки:

@Test @MainActor func foodTruckExists() async throws { ... }

Тестирование с использованием параметров

Тестовые функции могут принимать аргументы для проверки функции на разных данных без дублирования кода:

@Suite
struct MultiplicationTests {
    @Test(arguments: [(2, 3, 6), (4, 5, 20), (7, 8, 56)])
    func testMultiplication(_ a: Int, _ b: Int, _ expected: Int) {
        let result = multiply(a, b)
        #expect(result == expected)
    }
}
@Test(arguments: [
        FoodTruck(wheels: 4, name: "Ford"),
        FoodTruck(wheels: 3, name: "Ford"),
        FoodTruck(wheels: 5, name: "Ford")
])
func testAdditionParameterized(foodtruck: FoodTruck) {
    assert(foodtruck.wheels >= 4, "Ожидалось, что у фултрака минимум 4 колеса")
}

Ограничение доступности тестов

Если тест должен выполняться только на определенных версиях операционной системы или языка Swift, используйте атрибут @available:

@available(macOS 11.0, *)
@available(swift, introduced: 8.0, message: "Requires Swift 8.0 features to run")
@Test func foodTruckExists() { ... }

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

Для тестирования кода, который должен выполняться в основном потоке (например, обновление интерфейса), используйте атрибут @MainActor:

@Test @MainActor func mainThreadTestExample() async throws {
    // Логика теста в основном потоке
}

Примеры и пояснения

Пример 1: Тест асинхронной функции

struct NetworkManager {
    func fetchData() async throws -> Data {
        // Имитация сетевого запроса
        return Data()
    }
}

@Test
func testFetchData() async throws {
    let networkManager = NetworkManager()
    let data = try await networkManager.fetchData()
    #expect(data.isEmpty == false)
}

Этот пример демонстрирует тестирование асинхронной функции fetchData в классе NetworkManager. Функция testFetchData помечена атрибутами async и throws, что позволяет ей выполнять асинхронные операции и обрабатывать ошибки с помощью try. Утверждение #expect проверяет, что возвращенные данные не пусты.

Пример 2: Тест в основном потоке

class ViewModel {
    func updateState(state: ViewModel.State) {
        self.state = state
        // Обновление пользовательского интерфейса
    }
}

@Test
@MainActor
func testUpdateUI() async throws {
    let viewModel = ViewModel()
    await viewModel.updateState(state: .loading)
    #expect(viewModel.state == .loading)
    // Дополнительные проверки, связанные с обновлением интерфейса
}

Этот пример показывает тестирование функции updateState в классе ViewModel, которая изменяет состояние модели и обновляет пользовательский интерфейс. Атрибут @MainActor гарантирует, что тест testUpdateUI будет выполняться в основном потоке, что важно для правильного функционирования пользовательского интерфейса.

Пример 3: Тест функции, выбрасывающей ошибку

struct FileHandler {
    enum FileError: Error {
        case fileNotFound
    }

    func readFile() throws -> String {
        throw FileError.fileNotFound
    }
}

@Test
func testReadFile() throws {
    let fileHandler = FileHandler()
    do {
        let _ = try fileHandler.readFile()
        Issue.record("Ожидалась ошибка, но не была выброшена")
    } catch FileHandler.FileError.fileNotFound {
        // Ожидаемая ошибка
    } catch {
        Issue.record("Неожиданная ошибка: \(error)")
    }
}

Структура Issue в Swift предоставляет тип для описания сбоев или предупреждений, возникающих во время выполнения тестов. Эквивалентом Issue.record("Неожиданная ошибка: (error)") является XCTFail("Неожиданная ошибка: (error)")

Этот пример тестирует функцию readFile структуры FileHandler, которая выбрасывает ошибку FileError.fileNotFound. Тест testReadFile проверяет, что функция действительно выбрасывает ожидаемую ошибку и регистрирует проблему, если выброшена другая ошибка.

Организация тестов с помощью типов Suite

При управлении значительным количеством тестов важно организовать их в тестовые наборы (test suites), что значительно упрощает их выполнение и поддержку. В Swift это можно сделать двумя способами:

  1. Добавление тестов в тип Swift

    Просто поместите тестовые функции в тип Swift. Это автоматически создаст тестовый набор без необходимости использовать атрибут @Suite. Пример:

    struct FoodTruckTests {
        @Test func foodTruckExists() { ... }
    }
    
  2. Использование атрибута @Suite

    Атрибут @Suite не обязателен, но позволяет настраивать отображение тестового набора в IDE и командной строке, а также использовать дополнительные функции, такие как tags() для пометки тестов. Пример:

    @Suite("Food truck tests") struct FoodTruckTests {
        @Test func foodTruckExists() { ... }
    }
    

Пример использования тестовых наборов

@Suite("Food Truck Management") struct FoodTruckManagementTests {
    @Test func foodTruckExists() { ... }
    @Test func addFoodTruck() { ... }
}

@Suite("Customer Orders") struct CustomerOrdersTests {
    @Test func orderFood() { ... }
    @Test func cancelOrder() { ... }
}

В этом примере FoodTruckManagementTests и CustomerOrdersTests представляют собой отдельные тестовые наборы, сгруппированные по функциональным областям. Такой подход помогает легко организовать и настраивать выполнение тестов в различных сценариях.

Ограничения на типы тестовых наборов

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

  • Инициализатор типа: Если тип содержит тестовые функции, объявленные как методы экземпляра, он должен быть инициализирован с помощью инициализатора без аргументов. Этот инициализатор может быть неявным или вызываемым, синхронным или асинхронным, бросающим или не бросающим ошибки, а также иметь любой уровень доступа.

    @Suite struct FoodTruckTests {
        var batteryLevel = 100
        @Test func foodTruckExists() { ... } // ✅ ОК: Тип имеет неявный инициализатор.
    }
    
    @Suite struct CashRegisterTests {
        private init(cashOnHand: Decimal = 0.0) async throws { ... }
        @Test func calculateSalesTax() { ... } // ✅ ОК: Тип имеет вызываемый инициализатор.
    }
    
  • Доступность типов: Тестовые наборы и их содержимое должны быть всегда доступны, поэтому не следует использовать атрибут @available для ограничения их доступности.

    @Suite struct FoodTruckTests { ... } // ✅ OK: Тип доступен всегда.
    
    @available(macOS 11.0, *) // ❌ Ошибка: Тип может быть недоступен.
    @Suite struct CashRegisterTests { ... }
    
  • Наследование классов: Библиотека тестирования не поддерживает наследование между типами тестовых наборов. Поэтому классы, используемые как тестовые наборы, должны быть объявлены как final.

@Suite final class FoodTruckTests { ... } // ✅ ОК: Класс объявлен как final.

actor CashRegisterTests: NSObject { ... } // ✅ ОК: Акторы по умолчанию являются final.

class MenuItemTests { ... } // ❌ Ошибка: Этот класс не объявлен как final.

Использование .tags для ограничения выполнения теста

В Swift Testing для организации и ограничения выполнения тестов можно использовать теги (tags). Это мощный инструмент для классификации тестов и управления их выполнением в различных сценариях. Теги могут применяться как к отдельным тестовым функциям, так и к целым тестовым наборам.

Основные принципы использования тегов

  1. Определение тегов

    Теги определяются с использованием статического расширения структуры Tag. Это позволяет задать статические свойства, представляющие различные категории тестов:

    extension Tag {
        @Tag static var regression: Self
        @Tag static var ui: Self
        @Tag static var performance: Self
        // Дополнительные теги можно добавлять по мере необходимости
    }
    

    В этом примере определены теги для регрессионного тестирования (regression), тестирования пользовательского интерфейса (ui) и производительности (performance).

  2. Аннотация тестовых функций

    Теги могут быть применены к тестовым функциям с использованием атрибута @Test. Это делает возможным указать, какие категории тестирования относятся к конкретной тестовой функции:

    @Test(.tags(.regression, .ui))
    func testUserInterface() {
        // Логика теста для проверки пользовательского интерфейса
    }
    

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

  3. Аннотация тестовых наборов

    Теги также могут применяться ко всему тестовому набору, используя атрибут @Suite. Это позволяет легко классифицировать и ограничивать выполнение всех тестов внутри набора:

    @Suite(.tags(.performance))
    struct PerformanceTests {
        @Test func testAppLaunchSpeed() {
            // Логика теста для проверки скорости запуска приложения
        }
    
        @Test func testMemoryUsage() {
            // Логика теста для проверки использования памяти
        }
    }
    

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

Примеры использования и пояснения

  • Комбинация тегов

    Теги могут комбинироваться для точной классификации тестов:

    @Test(.tags(.regression, .critical))
    func testCriticalFunctionality() {
        // Логика теста для критически важной функциональности
    }
    

    В этом примере тест testCriticalFunctionality помечен как критически важный и подлежащий проверке в регрессионных тестах.

  • Динамическое управление тегами

    Теги могут также использоваться для динамического управления тестированием в зависимости от окружения или стадии разработки:

    #if DEBUG
    @Test(.tags(.fast))
    #else
    @Test(.tags(.slow, .full))
    #endif
    func testComplexCalculation() {
        // Логика теста для сложных вычислений
    }
    

    В этом примере тест testComplexCalculation отмечен тегом fast в режиме отладки и тегами slow и full в релизном режиме, что позволяет выбирать, какие тесты запускать в зависимости от текущего окружения.

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

Миграция тестов из XCTest в библиотеку Testing

Переход с использования XCTest на новую библиотеку Testing в Swift требует выполнения нескольких ключевых шагов, включая импорт библиотеки, изменение структуры тестовых классов и использование новых методов для проверок условий. Давайте рассмотрим каждый из этих шагов подробнее.

Импорт библиотеки Testing

Первым шагом является замена импорта библиотеки XCTest на библиотеку Testing в ваших тестовых файлах Swift:

import Testing

Этот импорт должен быть добавлен в файлы, где вы определяете ваши тесты. Важно помнить, что библиотеку Testing нужно использовать именно в контексте тестов. Импорт этой библиотеки в основное приложение или библиотеку не поддерживается и не рекомендуется.

Примеры миграции тестов

Конвертация классов тестов

В XCTest тесты обычно группируются внутри классов, которые наследуются от XCTestCase. В библиотеке Testing тесты могут быть объявлены как свободные функции, статические методы структур или классов, или как члены структур с атрибутом @Test.

Пример миграции класса тестов из XCTest в структуру с атрибутом @Test:

До (XCTest):

class FoodTruckTests: XCTestCase {
    func testEngineWorks() {
        // Тестовая логика здесь.
    }
}

После (Testing):

struct FoodTruckTests {
    @Test func engineWorks() {
        // Тестовая логика здесь.
    }
}

Преобразование setup и teardown функций

В XCTest используются методы setUp() и tearDown() для выполнения кода перед и после выполнения каждого теста. В библиотеке Testing аналогичное поведение можно реализовать с помощью инициализаторов и деинициализаторов структур или классов.

Пример преобразования setup и teardown из XCTest в инициализатор и деинициализатор:

До (XCTest):

class FoodTruckTests: XCTestCase {
    var batteryLevel: NSNumber!

    override func setUp() {
        batteryLevel = 100
    }

    override func tearDown() {
        batteryLevel = 0
    }
}

После (Testing):

struct FoodTruckTests {
    var batteryLevel: NSNumber

    init() {
        batteryLevel = 100
    }

    deinit {
        batteryLevel = 0
    }
}

Преобразование тестовых методов

В XCTest тестовый метод должен быть методом класса XCTestCase и начинаться с префикса test. В библиотеке Testing тестовые функции идентифицируются с помощью атрибута @Test.

Пример преобразования тестового метода из XCTest в функцию с атрибутом @Test:

До (XCTest):

class FoodTruckTests: XCTestCase {
    func testEngineWorks() {
        // Тестовая логика здесь.
    }
}

После (Testing):

struct FoodTruckTests {
    @Test func engineWorks() {
        // Тестовая логика здесь.
    }
}

Сравнение функций XCTAssert и expect/require

XCTest предоставляет набор функций XCTAssert() для проверки условий в тестах. В библиотеке Testing альтернативными являются макросы expect(::sourceLocation:) и require(::sourceLocation:). Эти макросы обеспечивают схожее поведение с XCTAssert(), однако require(::sourceLocation:) выбрасывает ошибку, если условие не выполняется.

Примеры использования Assert в XCTest и эквиваленты в Testing:

  • XCTest:

    class FoodTruckTests: XCTestCase {
        func testEngineWorks() throws {
            let engine = FoodTruck.shared.engine
            XCTAssertNotNil(engine.parts.first)
            XCTAssertGreaterThan(engine.batteryLevel, 0)
            try engine.start()
            XCTAssertTrue(engine.isRunning)
        }
    }
    
  • Testing:

    struct FoodTruckTests {
        @Test func engineWorks() throws {
            let engine = FoodTruck.shared.engine
            try #require(engine.parts.first != nil)
            #expect(engine.batteryLevel > 0)
            try engine.start()
            #expect(engine.isRunning)
        }
    }
    

Таблица соответствий функций XCTAssert и expect/require

XCTest

swift-testing

XCTAssert(x), XCTAssertTrue(x)

#expect(x)

XCTAssertFalse(x)

#expect(!x)

XCTAssertNil(x)

#expect(x == nil)

XCTAssertNotNil(x)

#expect(x != nil)

XCTAssertEqual(x, y)

#expect(x == y)

XCTAssertNotEqual(x, y)

#expect(x != y)

XCTAssertIdentical(x, y)

#expect(x === y)

XCTAssertNotIdentical(x, y)

#expect(x !== y)

XCTAssertGreaterThan(x, y)

#expect(x > y)

XCTAssertGreaterThanOrEqual(x, y)

#expect(x >= y)

XCTAssertLessThanOrEqual(x, y)

#expect(x <= y)

XCTAssertLessThan(x, y)

#expect(x < y)

XCTAssertThrowsError(try f())

#expect(throws: (any Error).self) { try f() }

XCTAssertThrowsError(try f()) { error in … }

#expect { try f() } throws: { error in return … }

XCTAssertNoThrow(try f())

#expect(throws: Never.self) { try f() }

try XCTUnwrap(x)

try #require(x)

XCTFail("…")

Issue.record("…")

Эта таблица поможет вам перейти от использования функций XCTest к эквивалентным методам в библиотеке Testing, обеспечивая при этом совместимость и сохранение функциональности ваших тестов.

Проверка ожидаемых значений и результатов в Swift тестировании

В процессе написания тестов на Swift, особенно при использовании библиотеки Testing, важно уметь эффективно проверять ожидаемые значения и результаты выполнения операций. Для этого используются функции expect и require, каждая из которых имеет свои особенности и предназначена для различных сценариев тестирования.

Использование expect

Функция expect предназначена для проверки условий в тестах. Она сообщает о неудаче, если условие не выполняется, но позволяет тесту продолжать своё выполнение.

Пример использования expect:

@Test func calculatingOrderTotal() {
    let calculator = OrderCalculator()
    #expect(calculator.total(of: [3, 3]) == 6)
    // Выведет "Expectation failed: (calculator.total(of: [3, 3]) → 6) == 7"
}

В данном примере, если сумма расчёта не равна 6, тест продолжит выполнение, но фиксируется ошибка, которая будет отображена в выводе тестов.

Использование require

Функция require работает аналогично expect, но с одним важным отличием: если условие не выполняется, тест немедленно завершается с ошибкой ExpectationFailedError.

Пример использования require:

@Test func returningCustomerRemembersUsualOrder() throws {
    let customer = try #require(Customer(id: 123))
    // Тест не продолжится, если customer равен nil.
    #expect(customer.usualOrder.countOfItems == 2)
}

Здесь, если объект customer равен nil, тест будет остановлен, и ошибку можно будет увидеть в выводе тестов.

Проверка опциональных значений

Для проверки опциональных значений в XCTest используется функция XCTUnwrap, которая бросает ошибку, если значение nil. В библиотеке Testing аналогичную функциональность предоставляет require.

Пример с использованием require:

struct FoodTruckTests {
    @Test func engineWorks() throws {
        let engine = FoodTruck.shared.engine
        let part = try #require(engine.parts.first)
        // Далее идёт код, который зависит от наличия `part`
    }
}

Если engine.parts.first равно nil, то будет выброшено исключение, и тест будет прерван.

Запись ошибок

В XCTest для записи ошибок, которые должны приводить к неудачному завершению теста в любом случае, используется функция XCTFail. В библиотеке Testing аналогичной функцией является Issue.record.

Пример с использованием Issue.record:

struct FoodTruckTests {
    @Test func engineWorks() {
        let engine = FoodTruck.shared.engine
        guard case .electric = engine else {
            Issue.record("Engine is not electric")
            return
        }
        // Далее идёт код, зависящий от того, что `engine` является электрическим
    }
}

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

Тестирование асинхронного поведения

Для тестирования асинхронного поведения в Swift, особенно при использовании Confirmation, важно следовать нескольким ключевым шагам и понимать принципы работы этого подхода. Давайте подробнее рассмотрим основные моменты и примеры использования.

Использование Confirmation для проверки асинхронных событий

Создание Confirmation

Для начала нужно создать Confirmation внутри тестовой функции с помощью функции confirmation() из библиотеки Testing. Confirmation создаётся с ожидаемым числом событий и замыканием, которое будет вызвано, когда условие будет выполнено.

Примеры использования

  1. Пример: Проверка выполнения асинхронной задачи

    @Test func asyncTaskCompletion() async {
        await confirmation("Task should complete") { taskCompleted in
            Task {
                await performAsyncTask()
                taskCompleted()
            }
        }
    }
    

    В этом примере confirmation ожидает завершение асинхронной задачи performAsyncTask. Когда задача завершается, вызывается taskCompleted(), подтверждающее выполнение задачи.

  2. Пример: Проверка асинхронного события

    @Test func eventHandlingTest() async {
        await confirmation("Event should be handled") { eventHandled in
            EventManager.shared.eventHandler = { event in
                if event.type == .desiredEvent {
                    eventHandled()
                }
            }
            EventManager.shared.triggerEvent(.desiredEvent)
        }
    }
    

    В этом случае мы ожидаем, что событие .desiredEvent будет обработано в EventManager. Когда событие обрабатывается, вызывается eventHandled(), что подтверждает обработку события.

  3. Пример: Проверка отсутствия асинхронного события

    @Test func orderCalculatorEncountersNoErrors() async {
      let calculator = OrderCalculator()
      await confirmation(expectedCount: 0) { confirmation in
        calculator.errorHandler = { _ in confirmation() }
        calculator.subtotal(for: PizzaToppings(bases: []))
      }
    }
    

    Чтобы убедиться, что определенное событие не происходит во время теста, создайте объект Confirmationс ожидаемым количеством 0.

Контроль выполнения тестов

ConditionTrait в тестировании Swift предоставляет гибкий механизм для контроля выполнения тестов на основе определённых условий. Это особенно полезно для ситуаций, когда нужно пропускать или выполнять тесты в зависимости от различных внешних или внутренних условий приложения или окружения.

Основные принципы использования ConditionTrait

  1. Пропуск тестов на основе условий

    Пример, показанный ниже, демонстрирует использование @Suite и @Test с атрибутами ConditionTrait:

    @Suite(.disabled(if: CashRegister.isEmpty))
    struct CashRegisterTests {
        @Test(.enabled(if: CashRegister.hasMoney))
        func testCashRegisterOperation() {
            // Логика теста
        }
    }
    
    • @Suite(.disabled(if: CashRegister.isEmpty)): Этот атрибут говорит о том, что все тесты в наборе CashRegisterTests будут пропущены, если CashRegister.isEmpty вернёт true. Это может быть полезно, если тесты касаются операций с кассой, которая должна содержать деньги для корректного тестирования.

    • @Test(.enabled(if: CashRegister.hasMoney)): Этот атрибут применяется к конкретному тесту testCashRegisterOperation и указывает, что тест будет выполняться только если CashRegister.hasMoney вернёт true. Это позволяет исключить выполнение теста, если требуемые условия не выполнены.

  2. Проверка различных условий

    ConditionTrait может использоваться для проверки различных условий, таких как:

    • Наличие определённых данных в приложении.

    • Определённое состояние системы или базы данных.

    • Версия операционной системы или другие системные характеристики.

Дополнительные примеры

Пропуск тестов на основе наличия элементов в меню

@Suite struct MenuTests {
    @Test(.enabled(if: Menu.hasItem(.pizza)))
    func testPizzaOrder() {
        // Логика теста для заказа пиццы
    }

    @Test(.enabled(if: Menu.hasItem(.sushi)))
    func testSushiOrder() {
        // Логика теста для заказа суши
    }
}
  • @Test(.enabled(if: Menu.hasItem(.pizza))): Этот тест testPizzaOrder будет выполняться только если в меню присутствует элемент pizza.

  • @Test(.enabled(if: Menu.hasItem(.sushi))): Этот тест testSushiOrder будет выполняться только если в меню присутствует элемент sushi.

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

Аннотация известных проблем

Аннотирование известных проблем в тестах с помощью функции withKnownIssue предоставляет мощный инструмент для управления и отслеживания известных проблем во время выполнения тестов. Давайте подробнее рассмотрим примеры использования и особенности этой функции.

Пример использования withKnownIssue

Простое аннотирование проблемы

struct FoodTruckTests {
    @Test func grillWorks() async {
        withKnownIssue("Grill is out of fuel") {
            try FoodTruck.shared.grill.start()
        }
        // Дополнительные проверки и логика теста
    }
}

В этом примере тест grillWorks помечен как имеющий известную проблему — "Grill is out of fuel". Если при выполнении теста возникнет ошибка, связанная с тем, что гриль не может быть запущен из-за отсутствия топлива, тест не будет считаться проваленным.

Указание интермиттирующих ошибок

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

struct FoodTruckTests {
    @Test func grillWorks() async {
        withKnownIssue("Grill may need fuel", isIntermittent: true) {
            try FoodTruck.shared.grill.start()
        }
        // Дополнительные проверки и логика теста
    }
}

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

Условия и сопоставление проблем

Функция withKnownIssue также позволяет задавать условия, при которых известная проблема считается актуальной, и сопоставлять ошибки с определенными критериями.

struct FoodTruckTests {
    @Test func grillWorks() async {
        withKnownIssue("Grill is out of fuel") {
            try FoodTruck.shared.grill.start()
        } when: {
            FoodTruck.shared.hasGrill
        } matching: { issue in
            issue.error != nil
        }
        // Дополнительные проверки и логика теста
    }
}

Здесь указано, что проблема с отсутствием топлива в гриле актуальна только при условии, что гриль установлен (FoodTruck.shared.hasGrill). Также проверяется, что в объекте issue есть ошибка (issue.error != nil), чтобы считать проблему действительной.

Заключение

Тестирование в Swift играет ключевую роль в обеспечении качества программного обеспечения. От юнит-тестов до системных проверок, тесты помогают разработчикам уверенно вносить изменения и поддерживать стабильность кода. XCTest, как стандартный фреймворк, обеспечивает базовые возможности для написания и запуска тестов. Однако новая библиотека Testing предлагает удобные синтаксические конструкции и дополнительные функции, которые делают процесс тестирования еще более гибким и мощным.

Мы рассмотрели еще не все фичи Swift Testing, так что давайте будем экспериментировать и дальше! А я пошел дальше смотреть видео с WWDC.

Теги:
Хабы:
+6
Комментарии4

Публикации

Истории

Работа

Ближайшие события

12 – 13 июля
Геймтон DatsDefense
Онлайн
14 июля
Фестиваль Selectel Day Off
Санкт-ПетербургОнлайн
19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн