Почему обычные тесты не спасают, и что такое 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:
<