BDD на Kotlin: Пишем тесты, которые читают и менеджеры, и разработчики
Вы когда-нибудь показывали unit-тест своему продакт-менеджеру? Скорее всего, нет. Потому что это всё равно что показывать чертежи реактивного двигателя пилоту — полезно, но непонятно. Behaviour-Driven Development (BDD) на Kotlin решает именно эту проблему: он превращает сухие assert'ы в живые сценарии, которые описывают поведение системы человеческим языком.
В этой статье мы разберём, как внедрить BDD на Kotlin с помощью современных фреймворков — Kotest и Spek. Вы увидите, как писать тесты в стиле Given-When-Then, которые одновременно являются и документацией, и автоматической проверкой. И да, их сможет прочитать даже ваш тимлид без подготовки.
Что такое BDD и чем он отличается от TDD?
Многие путают TDD (Test-Driven Development) и BDD (Behaviour-Driven Development). Разница проста:
- TDD — это про «как я проверю этот метод?»
- BDD — это про «как пользователь получит ценность от этой функции?»
Behaviour-driven development фокусируется на поведении системы, а не на деталях реализации. Вы описываете сценарий: при определённых условиях (Given), когда происходит действие (When), система выдаёт ожидаемый результат (Then).
Например, вместо того чтобы писать: assertEquals(42, calculator.add(20, 22)),
вы напишете:
Given("у меня есть калькулятор") {
val calc = Calculator()
When("я складываю 20 и 22") {
val result = calc.add(20, 22)
Then("результат должен быть 42") {
result shouldBe 42
}
}
}
Согласитесь, второй вариант читается как история, а не как техническая инструкция.
Почему Kotlin идеален для BDD
Kotlin — язык, который сам по себе располагает к написанию читаемого кода. Функции-расширения, инфиксная нотация, лямбды с ресивером — всё это делает BDD тесты на Kotlin максимально естественными. Вы можете писать shouldBe, shouldThrow и другие матчеры, которые выглядят как английский язык.
Кроме того, Kotlin поддерживает несколько парадигм, что позволяет использовать разные фреймворки для BDD. Рассмотрим два самых популярных.
Фреймворк Kotest: BDD из коробки
Kotest (ранее KotlinTest) — это мощный мультиплатформенный тестовый фреймворк. Он поддерживает несколько стилей, включая BehaviorSpec, который идеально подходит для BDD.
Вот как выглядит полноценный BDD сценарий на Kotlin с использованием Kotest:
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
class CartSpec : BehaviorSpec({
given("пустая корзина") {
val cart = ShoppingCart()
`when`("добавляем товар с ценой 100") {
cart.addItem(Item("Книга", 100.0))
then("в корзине должен быть 1 товар") {
cart.itemCount shouldBe 1
}
then("общая сумма должна быть 100") {
cart.total shouldBe 100.0
}
}
`when`("добавляем товар и применяем скидку 10%") {
cart.addItem(Item("Книга", 100.0))
cart.applyDiscount(10.0)
then("итоговая сумма должна быть 90") {
cart.total shouldBe 90.0
}
}
}
})
Обратите внимание на обратные кавычки вокруг when. Это особенность Kotlin: when — зарезервированное слово, но в кавычках оно работает как идентификатор. Некоторые разработчики предпочитают использовать When с заглавной буквы.
Spek: структурированный BDD для Kotlin
Spek — ещё один популярный фреймворк для behaviour-driven development Kotlin. Он вдохновлён RSpec из Ruby и использует вложенные группы describe, context, it.
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import kotlin.test.assertEquals
class UserRegistrationSpec : Spek({
describe("регистрация пользователя") {
val validator = EmailValidator()
context("когда email валидный") {
it("должен вернуть true") {
assertEquals(true, validator.isValid("test@example.com"))
}
}
context("когда email без @") {
it("должен вернуть false") {
assertEquals(false, validator.isValid("testexample.com"))
}
}
context("когда email пустой") {
it("должен выбросить исключение") {
assertThrows<IllegalArgumentException> {
validator.isValid("")
}
}
}
}
})
Spek отлично подходит для проектов, где важна строгая структура и чёткое разделение контекстов. Он также поддерживает параллельное выполнение тестов.
Given-When-Then: шаблон, который спасает проекты
Шаблон Given-When-Then — это сердце BDD. Давайте разберём его на реальном примере из интернет-магазина.
- Given — начальное состояние системы (предусловия)
- When — действие, которое совершает пользователь
- Then — ожидаемый результат
Вот пример для сервиса заказов:
class OrderServiceSpec : BehaviorSpec({
given("пользователь с балансом 500 рублей") {
val user = User(balance = 500.0)
val service = OrderService()
`when`("он оформляет заказ на 300 рублей") {
val order = service.placeOrder(user, listOf(Item("Товар", 300.0)))
then("заказ должен быть подтверждён") {
order.status shouldBe OrderStatus.CONFIRMED
}
then("баланс пользователя должен стать 200") {
user.balance shouldBe 200.0
}
}
}
})
Такой тест легко читать не только разработчикам, но и тестировщикам, аналитикам и даже менеджерам. Это снижает количество недопониманий и багов на стыке бизнеса и разработки.
Интеграция BDD с Cucumber на Kotlin
Если вы работаете в команде, где сценарии пишутся на Gherkin (файлы .feature), вы можете использовать Cucumber для Kotlin. Это позволяет хранить сценарии в едином формате, понятном всем участникам проекта.
Пример файла checkout.feature:
Feature: Оформление заказа
Scenario: Успешная покупка
Given пользователь авторизован
When он добавляет товар в корзину
And нажимает "Оформить заказ"
Then заказ создаётся с стату