BDD на Swift: как писать тесты, которые понимает бизнес

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему обычные тесты не спасают, и что такое BDD

Каждый iOS-разработчик сталкивался с ситуацией: вы написали сотню unit-тестов, покрытие — 90%, но при очередном демо менеджер говорит: «А почему при нажатии на эту кнопку ничего не происходит?». Вы смотрите в код — всё работает. Проблема в том, что unit-тесты проверяют код, а не поведение системы. Здесь на помощь приходит BDD (Behaviour-Driven Development) — методология, которая стирает границу между «техническим» тестированием и «бизнес-логикой».

BDD на Swift — это не просто модный термин. Это способ писать тесты так, чтобы их мог прочитать не только разработчик, но и аналитик, тестировщик или даже заказчик. В этой статье мы разберём, как внедрить behaviour-driven development в ваш iOS-проект, используя фреймворки Quick и Nimble, и покажем живые примеры на Swift.

Что такое BDD простыми словами

Представьте, что вы пишете не тесты, а сценарий для фильма. В сценарии есть действующие лица (пользователь, система), события (нажатие кнопки, отправка запроса) и ожидаемый результат (появление алерта, обновление UI). BDD — это и есть такой «сценарий», только для вашего приложения.

Основная идея: тесты должны описывать поведение системы с точки зрения пользователя. Вместо «метод saveUser() возвращает true» вы пишете «когда пользователь вводит корректный email и нажимает "Сохранить", система показывает сообщение "Успешно"».

Как BDD отличается от TDD

Многие путают BDD с TDD (Test-Driven Development). Разница проста:

  • TDD — пишем тест, потом код, потом рефакторинг. Фокус на корректности реализации.
  • BDD — пишем сценарий поведения, потом код, который это поведение реализует. Фокус на ценности для пользователя.

BDD — это эволюция TDD. Если TDD отвечает на вопрос «правильно ли работает код?», то BDD — «делает ли код то, что нужно бизнесу?».

Инструменты для BDD на Swift

В мире Swift есть два главных «кита» для behaviour-driven development:

  • Quick — фреймворк для написания тестов в стиле BDD. Он заменяет стандартный XCTest и предоставляет синтаксис describe/context/it.
  • Nimble — библиотека для «матчеров» (проверок). Вместо XCTAssertEqual вы пишете expect(value).to(equal(5)) — это читается как английское предложение.

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

Установка Quick и Nimble через Swift Package Manager

Добавьте зависимости в ваш Package.swift или через Xcode: File → Add Packages → введите URL репозитория Quick: https://github.com/Quick/Quick.git и Nimble: https://github.com/Quick/Nimble.git.

Пример для Package.swift:

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyApp",
    dependencies: [
        .package(url: "https://github.com/Quick/Quick.git", from: "7.0.0"),
        .package(url: "https://github.com/Quick/Nimble.git", from: "13.0.0"),
    ],
    targets: [
        .target(name: "MyApp"),
        .testTarget(
            name: "MyAppTests",
            dependencies: ["MyApp", "Quick", "Nimble"]
        )
    ]
)

Пишем первый BDD-тест на Swift

Допустим, у нас есть простой LoginService, который проверяет логин и пароль. Вместо того чтобы писать скучный XCTest, создадим тест в стиле BDD.

Код сервиса (для контекста)

class LoginService {
    func authenticate(username: String, password: String) -> Result<User, LoginError> {
        guard !username.isEmpty else { return .failure(.emptyUsername) }
        guard !password.isEmpty else { return .failure(.emptyPassword) }
        guard password.count >= 6 else { return .failure(.weakPassword) }
        // Имитация успешной аутентификации
        return .success(User(name: username))
    }
}

enum LoginError: Error, Equatable {
    case emptyUsername
    case emptyPassword
    case weakPassword
}

struct User: Equatable {
    let name: String
}

BDD-тест с Quick и Nimble

import Quick
import Nimble
@testable import MyApp

class LoginServiceSpec: QuickSpec {
    override func spec() {
        describe("LoginService") {
            var service: LoginService!
            
            beforeEach {
                service = LoginService()
            }
            
            context("когда пользователь вводит корректные данные") {
                it("возвращает успешный результат с пользователем") {
                    let result = service.authenticate(username: "john", password: "123456")
                    
                    switch result {
                    case .success(let user):
                        expect(user.name).to(equal("john"))
                    case .failure:
                        fail("Ожидался успех, но получена ошибка")
                    }
                }
            }
            
            context("когда имя пользователя пустое") {
                it("возвращает ошибку emptyUsername") {
                    let result = service.authenticate(username: "", password: "123456")
                    
                    expect(result).to(beFailure(LoginError.emptyUsername))
                }
            }
            
            context("когда пароль короче 6 символов") {
                it("возвращает ошибку weakPassword") {
                    let result = service.authenticate(username: "john", password: "123")
                    
                    expect(result).to(beFailure(LoginError.weakPassword))
                }
            }
        }
    }
}

// Вспомогательный матчер для Result
func beFailure<T, E: Equatable>(_ expectedError: E) -> Nimble.Predicate<Result<T, E>> {
    return Predicate { expression in
        guard let result = try expression.evaluate() else {
            return PredicateResult(status: .fail, message: .fail("nil"))
        }
        switch result {
        case .success:
            return PredicateResult(status: .doesNotMatch, message: .fail("ожидалась ошибка \(expectedError), но получен успех"))
        case .failure(let error):
            return PredicateResult(
                status: error == expectedError ? .matches : .doesNotMatch,
                message: .fail("ожидалась ошибка \(expectedError), но получена \(error)")
            )
        }
    }
}

Обратите внимание: тест читается как обычный текст. describe, context, it — это не просто функции, это структура, которая делает тест понятным для любого участника команды.

Gherkin и Swift: идеальный тандем для BDD

Для более сложных сценариев (например, интеграционных тестов или UI-тестов) используют язык Gherkin. Это формат «Given-When-Then»:

  • Given — предварительное условие (контекст)
  • When — действие
  • Then — ожидаемый результат

В Swift вы можете использовать библиотеку XCTest-Gherkin или просто эмулировать Gherkin-стиль в Quick:

<                

Блоги

Рекомендаций по книгам