Site icon AppTractor

Введение в Swift Testing

На 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 покажет вам бриллиантовую кнопку.

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. Тестовая функция

2. Ожидания

Ожидания в обоих фреймворках сильно отличаются.

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

Однако в Swift Test есть два основных макроса — #expect и #require.

3. Тестовые наборы

Советы по миграции с XCTest

Продолжайте использовать XCTest

Отказаться от XCTest сложно, если:

Заключение

Swift Testing предлагает выразительный API с макросами, позволяющий объявлять поведение тестов с минимальным количеством кода. API #expect использует выражения и операторы Swift для проверки предоставленных выражений. Параметризованные тесты помогают сократить количество избыточного кода, позволяя использовать код с похожей структурой. Кроме того, Swift Testing поддерживает параллельное тестирование, что повышает общую эффективность и производительность тестирования. Однако если вы хотите включить сериализованное тестирование, вам необходимо использовать трейт, чтобы сделать его сериализованным.

Источник

Exit mobile version