[Перевод] Ранний взгляд на будущее тестирования с swift-testing

2af9710419dd6574dbdadd1026132cc5.png

Пару месяцев назад Стюарт Монтгомери, программист из команды XCTest в Apple, поделился новой библиотекой тестирования Swift с открытым исходным кодом на основе макросов.

Библиотека называется Swift-testing, и, как указано в ее документации, она предназначена для проверки концепции нового API тестирования для Swift, основанного на макросах и интегрированного в Swift так же, как и XCTest.

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

Начнем

Первое, что я сделал, — это создал небольшой swift package, который позволил бы мне написать несколько тестов с использованием новой библиотеки:

import PackageDescription

let package = Package(
    name: "SwiftTestingDemo",
    products: [
        .library(
            name: "SwiftTestingDemo",
            targets: ["SwiftTestingDemo"]),
    ],
    dependencies: [
    ],
    targets: [
        .target(
            name: "SwiftTestingDemo",
            swiftSettings: [
                .enableUpcomingFeature("BareSlashRegexLiterals")
            ]
        )
    ]
)

Затем я добавил код из своего приложения QReate, который декодирует строки URL-адреса WIFI в структуры Swift, что, по моему мнению, было бы идеальным кандидатом для проверки swift-testing:

import Foundation

struct WifiNetwork {
    let ssid: String
    let password: String
    let security: String?
    let hidden: String?
}

protocol ErrorMonitoring {
    func monitor(_ error: Error)
}

struct WifiParser {
    enum Error: Swift.Error {
        case noMatches
    }

    private let monitoring: ErrorMonitoring

    init(monitoring: ErrorMonitoring) {
        self.monitoring = monitoring
    }

    func parse(wifi: String) throws -> WifiNetwork {
        let regex = /WIFI:S:(?[^;]+);(?:T:(?[^;]*);)?P:(?[^;]+);(?:H:(?[^;]*);)?;/

        guard let result = try? regex.wholeMatch(in: wifi) else {
            let error = Error.noMatches
            monitoring.monitor(error)
            throw error
        }

        return WifiNetwork(
            ssid: String(result.ssid),
            password: String(result.password),
            security: result.security.map(String.init),
            hidden: result.hidden.map(String.init)
        )
    }
}

Настройка swift-testing

Теперь, когда у меня был код для тестирования, я пошел дальше и настроил цель тестирования, чтобы иметь возможность начать использовать swift-testing.

Подключаем swift-testing

Первое, что мне нужно было сделать, — это добавить swift-testing в качестве зависимости как к пакету, так и к цели тестирования:

import PackageDescription

let package = Package(
    name: "SwiftTestingDemo",
    platforms: [
        .macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16), .visionOS(.v1)
    ],
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "SwiftTestingDemo",
            targets: ["SwiftTestingDemo"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-testing.git", branch: "main"),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "SwiftTestingDemo",
            swiftSettings: [
                .enableUpcomingFeature("BareSlashRegexLiterals")
            ]
        ),
        .testTarget(
            name: "SwiftTestingDemoTests",
            dependencies: ["SwiftTestingDemo", .product(name: "Testing", package: "swift-testing")]),
    ]
)

Запускаем тесты

Из-за особенностей организации тестов в swift-testing и того факта, что оно еще не интегрировано в Xcode или SPM, вам необходимо создать небольшой подкласс XCTestCase, создающий единый тестовый метод, который находит и запускает все тесты с помощью XCTestScaffold:

import XCTest

final class AllTests: XCTestCase {
    func testAll() async {
        await XCTestScaffold.runAllTests(hostedBy: self)
    }
}

Добавим тесты

Теперь, когда я правильно настроил цель теста, я могу приступить к написанию тестов. На основе своего кода я решил написать три отдельных теста:

  1. Тест, который проверяет, может ли парсер распарсить строку WIFI со всеми присутствующими полями.

  2. Тест, проверяющий, что парсер выдает ошибку, когда строка WIFI недействительна.

  3. Тест, который проверяет, может ли парсер распарсить строку WIFI, в которой присутствуют только обязательные поля.

Перед реализацией

Подход, к тому, как вы пишите тесты изменился в сравнении с XCTest. Вам больше не нужно добавлять к функциям префикс test, и теперь вместо этого вы можете прикрепить макрос @Test к любой функции, которую вы хотите использовать в качестве теста:

@Test
func whenParseIsCalledWithAllFields_ThenNetworkIsInitialisedCorrectly() {
    // ...
}

Организация тестов

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

@Suite
struct WifiParserTests {
    func whenParseIsCalledWithAllFields_ThenNetworkIsInitialisedCorrectly() {
        // ...
    }
}

Из того, что я понял, прочитав документацию, кажется хорошей практикой прикреплять макрос @Suite к структурам, которые группируют тесты.

Тест на неверную строку WIFI

Первый тест, который я хотел добавить и который продемонстрировал бы различия между swift-testing и XCTest, заключался в проверке того, что парсер выдает ошибку и отправляет событие мониторинга, когда строка WIFI неверная:

@Suite
struct WifiParserTests {
    let sut: WifiParser
    let errorMonitoring: SpyErrorMonitoring

    // 1
    init() async throws {
        errorMonitoring = SpyErrorMonitoring()
        sut = WifiParser(monitoring: errorMonitoring)
    }

    func whenParseIsCalledWithWrongString_ThenNoMatchesErrorIsThrownAndMonitoringEventIsSent() {
        // 2
        #expect(throws: WifiParser.Error.noMatches) { try sut.parse(wifi: "") }
        // 3
        #expect(errorMonitoring.capturedErrors.compactMap { $0 as? WifiParser.Error } == [.noMatches])
    }
}

Давайте разберем приведенный выше набор тестов и поймем, что происходит:

  1. Первое, что я сделал, — это настроил тестируемую систему (WifiParser) и тестовый дублер для отслеживания событий мониторинга (SpyErrorMonitoring). В XCTest это обычно делается в методе setUp, чтобы экземпляры создавались перед каждым тестом. Однако в swift-testing, поскольку пакет создается перед запуском каждого теста, достаточно сделать это в методе init.

  2. Использовал перегрузку макроса #expect, которая позволяет убедиться, что замыкание вызывает определенную ошибку. Можно передать тип вместо экземпляра ошибки, чтобы не углубляться в конкретизацию.

  3. Использовал макрос #expect с логическим условием, чтобы подтвердить, что дублер зафиксировал правильную ошибку.

Тест со всеми полями

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

@Suite
struct WifiParserTests {
    func whenParseIsCalledWithAllFields_ThenNetworkIsInitialisedCorrectly() throws {
        let wifi = "WIFI:S:superwificonnection;T:WPA;P:strongpassword;H:YES;;"

        let network = try sut.parse(wifi: wifi)

        #expect(try #require(network.security) == "WPA")
        #expect(try #require(network.hidden) == "YES")
        #expect(network.ssid == "superwificonnection")
        #expect(network.password == "strongpassword")
    }
}

Как вы можете видеть в приведенном выше примере, аналогично тому, как вы пишете тесты в XCTest, вы все равно можете объявить методы как генерирующие исключение (и даже async), но вместо выделенных методов XCTAssert у вас теперь есть один макрос #expect. который вы можете использовать, чтобы утверждать, что условие истинно.

Вдобавок ко всему, как вы можете видеть выше, где я разворачиваю необязательный параметр security и hidden, вместо использования XCTUnwrap можно использовать макрос #require.

Тест только с обязательными полями

Последний тест, который я добавил, заключался в проверке того, что моя логика будет работать, даже если строка Wi-Fi содержит только обязательные поля, используя только макрос #expect:

@Suite
struct WifiParserTests {
    func whenParseIsCalledWithStringContainingOnlyRequiredFields_ThenCorrectValuesAreReturned() throws {
        let wifi = "WIFI:S:superwificonnection;P:strongpassword;;"

        let network = try sut.parse(wifi: wifi)

        #expect(network.hidden == nil)
        #expect(network.security == nil)
        #expect(network.ssid == "superwificonnection")
        #expect(network.password == "strongpassword")
    }
}

Что почитать далее

Приведенные выше примеры — лишь небольшая часть того, что вы можете сделать с помощью swift-testing. Если вы пробуете библиотеку и пытаетесь выяснить, как выполнить конкретную проверку, которую вы обычно делаете в XCTest, я рекомендую вам просмотреть эту диаграмму миграции из документации, которая сравнивает обе библиотеки:

Соответствие методов

Соответствие методов

Примеры и более подробное описание самой библиотеки можно найти в ее документации, которая размещена на сайте.

Перевод статьи подготовлен в преддверии старта специализации IOS Developer. На странице специализации вы можете зарегистрироваться на бесплатные вебинары и интенсив курса.

© Habrahabr.ru