Cucumber und BDD: warum ich die Feature Datei vor dem Code schreibe

Kurz gesagt

Niveau: Fortgeschritten

Die meiste Nacharbeit beginnt mit einer Anforderung, die zwei Menschen auf zwei Arten lesen. BDD schreibt das Verhalten als konkrete Szenarien auf, die zugleich Tests sind.

  • Gherkin: Feature, Rule, Scenario, Given, When, Then, Background, Scenario Outline und Examples. Einfache Sprache mit Struktur.
  • Step Definitions machen aus den Worten Aktionen in Capybara. Hilfsmethoden wiederverwenden, keine Schritte, die Schritte aufrufen.
  • Outlines, Tags, Hooks, Parametertypen und Page Objects halten eine große Suite lesbar.
  • Lebende Dokumentation: Eine Feature Datei, die in jedem Build läuft, bleibt aktuell.
  • Einrichtung in Rails: die Integration Cucumber Rails, Capybara, factory_bot und Selenium ab 4.11. Kein Gem webdrivers, kein zweiter Hook für DatabaseCleaner.

Ich nutze Cucumber seit mehr als 10 Jahren bei jedem Projekt, das ich baue, weil es die Vereinbarung darüber, was Software tun soll, ausdrücklich und geprüft macht. Das ist die lange Fassung, warum, mit Gherkin, Step Definitions, den fortgeschrittenen Funktionen, einer Einrichtung in Rails und den häufigen Fehlern, durchgespielt an einem erfundenen Kaffeeshop in Köln und einem SaaS aus Hamburg.

Mein wichtigstes Werkzeug ist kein Editor und keine KI

Das wichtigste Werkzeug in der Art, wie ich Software baue, ist kein schlauer Editor, und es ist auch kein KI Codegenerator. Es ist Cucumber. Ich nutze es seit mehr als 10 Jahren, und zwar bei jedem Projekt, das ich baue: Systeme für die öffentliche Hand, bei denen ein Fehler gefährlich ist und nicht bloß lästig, Logistik für große Unternehmen, SaaS Produkte, die jede Woche ausliefern, Marktplätze und Onlineshops.

Das ist meine Praxis, kein Naturgesetz. Viel gute Software entsteht ohne Cucumber. Aber ich habe keinen besseren Weg gefunden, um sicherzustellen, dass gebaut wird, was das Unternehmen tatsächlich bestellt hat, und dieser Artikel ist die lange Fassung, warum. Er erklärt, was Behaviour Driven Development ist, die Sprache Gherkin, Step Definitions, die fortgeschrittenen Funktionen, die Einrichtung in Rails und die Fehler, die ich am häufigsten sehe.

Was BDD ist

So entsteht noch immer viel Software, ungefähr:

  1. Jemand aus dem Fachbereich schreibt ein Anforderungsdokument.
  2. Eine Entwicklerin liest es und versteht es auf eine Art.
  3. Sie schreibt Code nach diesem Verständnis.
  4. Ein Tester prüft nach seinem eigenen Verständnis desselben Dokuments.
  5. Der Fachbereich sieht das Ergebnis und sagt "so war das nicht gemeint".
  6. Alle streiten, wessen Lesart die richtige war, und die Nacharbeit beginnt.

Niemand in dieser Kette ist nachlässig. Das Dokument ist schlicht mehrdeutig, und drei Menschen lesen es auf drei Arten.

Behaviour Driven Development (BDD) löst das, indem alle das Verhalten gemeinsam aufschreiben, in einer festen Form, die zugleich ein ausführbarer Test ist. Dan North hat es 2006 beschrieben, als Weg, Test Driven Development über Verhalten statt über Tests zu erklären.1 Wie es sich neben Unit Tests einfügt, beschreibe ich in TDD und BDD in Ruby on Rails.

Der Kerngedanke: Wenn die Spezifikation der Test ist, wird die Lücke zwischen dem Gemeinten und dem Gebauten sehr klein. Die Software verhält sich wie spezifiziert, oder der Test wird rot.

Beispiel: ein Satz, zwei Lesarten

Bohnenpost ist ein erfundener Shop für Kaffeeabos in Köln, unter bohnenpost.example. Die Inhaberin schreibt: "Beschädigte Ware bekommt 50 % erstattet." Die Box Roaster's Selection kostet 49,99 €.

Der Entwickler versteht das als 50 % vom Preis, abgerundet, also 24,99 €. Die Inhaberin hat, später gefragt, 25,00 € erwartet, denn "die Hälfte von fünfzig ist fünfundzwanzig". Keiner liegt falsch. Der Satz hat es nicht gesagt. Als Szenario geschrieben muss die Zahl auf dem Papier stehen, bevor irgendwer Code schreibt:

Scenario: Half refund for a damaged bag
  Given a delivered order for "Roaster's Selection" at 49,99 €
  When customer support approves a 50% refund for damage
  Then a refund of 25,00 € should be issued

Die Hälfte von 49,99 € ist 24,99524{,}995 €. Das Szenario erzwingt die Entscheidung, kaufmännisch runden, und hält sie dort fest, wo beide sie lesen können.

Was Cucumber macht

Cucumber ist das Werkzeug, das BDD praktikabel macht. Du schreibst Spezifikationen in Gherkin, einer kleinen Sprache, die sich wie einfaches Englisch liest, aber genug Struktur hat, damit ein Computer sie zerlegen und als Tests ausführen kann.2

Hier ist eine Feature Datei:

# features/user_login.feature
Feature: User login
  As a registered customer
  I want to sign in to my account
  So that I can see my subscription

  Background:
    Given a customer exists with email "sarah@example.com" and password "correcthorse"

  Scenario: Successful sign in with valid details
    Given I am on the sign in page
    When I fill in "Email" with "sarah@example.com"
    And I fill in "Password" with "correcthorse"
    And I press "Sign in"
    Then I should see "Welcome back, Sarah"
    And I should be on the "account" page

  Scenario: Failed sign in with the wrong password
    Given I am on the sign in page
    When I fill in "Email" with "sarah@example.com"
    And I fill in "Password" with "wrongpassword"
    And I press "Sign in"
    Then I should see "Invalid email or password"
    And I should still be on the "sign in" page

Lies es laut vor. Die Product Ownerin kann folgen, der Geschäftsführer auch, und der Entwickler, der nächstes Jahr dazukommt, ebenso. Genau darum geht es.

Und es ist nicht nur Dokumentation. Wenn ich bundle exec cucumber features/user_login.feature starte, steuert Cucumber die Anwendung über Capybara: Es ruft die Seite auf, füllt die Felder aus, drückt den Knopf und prüft das Ergebnis.3 Verhält sich die Anwendung nicht wie spezifiziert, scheitert der Test.

Die Sprache Gherkin, das Wichtigste

Gherkin hat einen kleinen Wortschatz. Die zentralen Schlüsselwörter sind Feature, Rule, Scenario (oder Example), Given, When, Then, And, But, Background, Scenario Outline und Examples.2

Feature

Jede Datei mit der Endung .feature beginnt mit einem Feature und einer kurzen Beschreibung der Fähigkeit:

Feature: Basket
  As an online shopper
  I want to manage the items in my basket
  So that I can buy several products in one order

Die Zeilen "As a, I want, So that" sind eine Konvention, keine Syntax. Gherkin behandelt sie als freien Text.2 Ich behalte sie trotzdem, weil sie dich zwingen zu sagen, für wen die Funktion ist und warum es sie gibt. Wenn du diese drei Zeilen nicht füllen kannst, verstehst du die Funktion noch nicht gut genug, um sie zu bauen.

Scenario

Ein Szenario ist ein konkretes Beispiel für ein Verhalten:

Scenario: Adding a product to an empty basket
  Given my basket is empty
  When I add "House Blend 1kg" to my basket
  Then my basket should contain 1 item
  And the basket total should be 18,50 €

Ein gutes Szenario ist unabhängig (es verlässt sich nicht darauf, dass ein anderes Szenario vorher läuft), fokussiert (ein Verhalten) und klar (jeder erkennt, was geprüft wird).

Die Schlüsselwörter für Schritte

Given legt den Kontext fest, den Zustand der Welt vor der Handlung. When ist die Handlung, das, was die Person tut. Then ist das erwartete Ergebnis.2 And und But setzen die jeweils vorherige Art von Schritt fort, und But liest sich einfach besser für etwas, das nicht passieren soll:

Scenario: Discounted order with free delivery
  Given I am signed in as "regular@example.com"
  And I have the discount code "SAVE20"
  And "Espresso Gift Box" is priced at 39,95 €
  When I add "Espresso Gift Box" to my basket
  And I apply the discount code
  And I go to the checkout
  Then I should see the discounted price of 31,96 €
  And I should see free delivery applied
  But I should not see the express delivery option

Background

Wenn alle Szenarien einer Datei dieselbe Vorbereitung teilen, läuft ein Background vor jedem von ihnen:2

Feature: Product management
  Background:
    Given I am signed in as an admin
    And I am on the product management page

  Scenario: Creating a product
    When I click "Add product"
    And I fill in the product details
    And I click "Save"
    Then I should see "Product created"

  Scenario: Deleting a product
    Given a product "Old Blend" exists
    When I click "Delete" next to "Old Blend"
    And I confirm the deletion
    Then "Old Blend" should not appear in the product list

Step Definitions machen aus Worten Handlungen

Feature Dateien sagen, was passieren soll. Step Definitions sagen wie, in Ruby. Die Wörter in Anführungszeichen werden über Cucumber Expressions wie {string} und {int} zu Argumenten.

# features/step_definitions/authentication_steps.rb
Given('I am signed in as {string}') do |email|
  @current_user = User.find_by(email: email) || create(:user, email: email)
  sign_in_through_the_form(@current_user)
end

Given('I am signed in as an admin') do
  @current_user = create(:user, :admin)
  sign_in_through_the_form(@current_user)
end

When('I sign out') do
  click_button 'Sign out'
end

Then('I should be signed in') do
  expect(page).to have_button('Sign out')
end

create kommt aus factory_bot, und Cucumber sieht es erst, wenn World(FactoryBot::Syntax::Methods) in einer Datei unter features/support steht.4 visit, fill_in, click_button und expect(page) kommen aus Capybara.3

Achte darauf, was der Schritt für den Admin nicht tut. Eine ältere Fassung dieses Artikels hat mit step %(...) einen Schritt aus einem anderen aufgerufen. Die eigene Anleitung von Cucumber sagt, dass das in Cucumber Ruby möglich, aber nicht empfohlen ist: Zieh eine Hilfsmethode heraus und ruf sie aus beiden Schritten auf.5 Genau das ist sign_in_through_the_form.

Eine kleine Menge allgemeiner Schritte deckt die meisten Seiten ab:

# features/step_definitions/navigation_steps.rb
PAGES = {
  'home' => -> { root_path },
  'sign in' => -> { new_user_session_path },
  'account' => -> { account_path },
  'basket' => -> { cart_path },
  'checkout' => -> { checkout_path }
}.freeze

def path_for(name)
  route = PAGES.fetch(name.downcase) { raise "No path for '#{name}', add it to PAGES in navigation_steps.rb" }
  instance_exec(&route)
end

Given('I am on the {string} page') { |name| visit path_for(name) }
Given('I am on the sign in page') { visit path_for('sign in') }
Then('I should be on the {string} page') { |name| expect(page).to have_current_path(path_for(name)) }
Then('I should still be on the {string} page') { |name| expect(page).to have_current_path(path_for(name)) }

# features/step_definitions/form_steps.rb
When('I fill in {string} with {string}') { |field, value| fill_in field, with: value }
When('I select {string} from {string}') { |value, field| select value, from: field }
When('I check {string}') { |box| check box }
When('I press {string}') { |button| click_button button }
When('I click {string}') { |label| click_on label } # ein Link oder ein Button

# features/step_definitions/assertion_steps.rb
Then('I should see {string}') { |text| expect(page).to have_content(text) }
Then('I should not see {string}') { |text| expect(page).not_to have_content(text) }
Then('I should see {string} within {string}') do |text, selector|
  within(selector) { expect(page).to have_content(text) }
end

Eine Liste der Seitennamen, genutzt von den Schritten "on" und "be on", damit die beiden nie auseinanderlaufen. have_current_path wartet und versucht es erneut, ein direkter Vergleich mit current_path tut das nicht, und das zählt, sobald JavaScript im Spiel ist. Und click_on aus Capybara klickt schon einen Link oder einen Button, du musst also nicht eine Ausnahme abfangen und das andere probieren.3

Die fortgeschrittenen Funktionen, die ich nutze

Scenario Outlines für Geschäftsregeln mit vielen Fällen

Muss dasselbe Verhalten mit verschiedenen Eingaben geprüft werden, läuft ein Scenario Outline einmal für jede Zeile seiner Tabelle Examples und füllt die Platzhalter.2

Feature: Subscription pricing
  Scenario Outline: Discount codes apply correctly
    Given I am on the checkout page for the "<plan>" plan
    When I enter the discount code "<code>"
    Then the final price should be <final_price>

    Examples:
      | plan     | code       | final_price |
      | Pro      | SAVE10     | 26,99 €     |
      | Pro      | SAVE20     | 23,99 €     |
      | Pro      | FLAT5      | 24,99 €     |
      | Business | SAVE10     | 89,99 €     |
      | Business | ENTERPRISE | 69,99 €     |

Beispiel: die Preistabelle von Hand prüfen

Schichtwerk ist ein erfundenes SaaS für Schichtplanung in Hamburg, unter schichtwerk.example. Pro kostet 29,99 € im Monat und Business 99,99 €. Bevor die Tabelle in die Feature Datei kommt, prüfen der Inhaber und ich jede Zeile:

  • Pro mit SAVE10: 29,99×0,9=26,99129{,}99 \times 0{,}9 = 26{,}991, also 26,99 €.
  • Pro mit SAVE20: 29,99×0,8=23,99229{,}99 \times 0{,}8 = 23{,}992, also 23,99 €.
  • Pro mit FLAT5: 29,99−5=24,9929{,}99 - 5 = 24{,}99, also 24,99 €.
  • Business mit SAVE10: 99,99×0,9=89,99199{,}99 \times 0{,}9 = 89{,}991, also 89,99 €.
  • Business mit ENTERPRISE (30 % Rabatt): 99,99×0,7=69,99399{,}99 \times 0{,}7 = 69{,}993, also 69,99 €.

Jede Zeile mit Prozentrabatt wird hier abgerundet, aber nur, weil die dritte Nachkommastelle zufällig klein ist. Im Beispiel mit der Erstattung oben wurde aus 24,99524{,}995 € kaufmännisch gerundet 25,00 €. Die Tabelle und das Szenario zur Erstattung legen zusammen eine Rundungsregel für das ganze Produkt fest: auf den Cent genau, ab der Hälfte aufrunden.

Tags zum Ordnen und Filtern

Tags fassen Szenarien zu Gruppen zusammen, damit du eine Teilmenge laufen lassen kannst. Cucumber filtert sie mit Tag Ausdrücken, die Tags mit and, or und not verknüpfen.6

@authentication @critical
Feature: Customer authentication

  @smoke
  Scenario: Successful sign in
    Given I am on the sign in page
    When I sign in with valid details
    Then I should be signed in

  @wip
  Scenario: Two factor authentication
    Given I have two factor authentication turned on
    When I enter my password
    Then I should be asked for my code

  @javascript @external
  Scenario: Sign in with Google
    Given I am on the sign in page
    When I sign in with Google
    Then I should be signed in
bundle exec cucumber --tags @smoke
bundle exec cucumber --tags "not @wip"
bundle exec cucumber --tags "@critical and @authentication"
bundle exec cucumber --tags "@javascript and not @external"

Die Tags, die ich typischerweise nutze: @smoke für schnelle Prüfungen bei jedem Commit, @regression für die ganze Suite vor einem Release, @critical für alles, was vor jedem Deployment bestehen muss, @slow für lange Szenarien, @wip für Arbeit in Arbeit, die aus der Continuous Integration herausgehalten wird, und @external für alles, was einen externen Dienst braucht. @javascript ist der einzige Tag mit eingebauter Bedeutung: Capybara schaltet dieses Szenario auf seinen Treiber mit JavaScript um, der ohne Weiteres headless laufen kann.3

Hooks für Vorbereitung und Aufräumen

Hooks führen Code vor oder nach Szenarien aus, auf Wunsch nur für Szenarien, die zu einem Tag Ausdruck passen.6

# features/support/hooks.rb
Before do
  ActionMailer::Base.deliveries.clear
  Timecop.freeze(Time.zone.parse('2026-04-08 10:00'))
end

After do |scenario|
  Timecop.return
  if scenario.failed? && Capybara.current_driver != :rack_test
    page.save_screenshot("tmp/screenshots/#{scenario.name.parameterize}.png")
  end
end

Before('@external') do
  next if ENV['USE_REAL_APIS'] == 'true'

  stub_request(:post, %r{api\.stripe\.com}).to_return(status: 200, body: { id: 'pi_test' }.to_json)
end

Der Screenshot läuft nur, wenn ein echter Browser steuert, weil der Standardtreiber rack_test keinen Bildschirm hat, den er festhalten könnte. Zwei Hooks aus der älteren Fassung habe ich entfernt: einen, der für @javascript den Treiber umschaltete, was Capybara schon selbst tut, und einen, der Datumsangaben aus Kommentaren im Szenario las, was cleverer als klar war.

Eigene Parametertypen für deine Fachsprache

Cucumber lässt sich die Wörter deiner Domäne beibringen, damit ein Schritt {money} sagen kann statt eines regulären Ausdrucks:

# features/support/parameter_types.rb
ParameterType(
  name: 'money',
  regexp: /\d[\d.]*(?:,\d{2})? €/, # deutsches Format: 1.299,99 €
  transformer: ->(amount) { BigDecimal(amount.delete('. €').tr(',', '.')) }
)

ParameterType(
  name: 'order_state',
  regexp: /cart|address|delivery|payment|confirm|complete|canceled/,
  transformer: ->(state) { state }
)
Scenario: A large discount
  Given the product costs 1.299,99 €
  When I apply a discount of 200,00 €
  Then the final price should be 1.099,99 €

Geld wird zu einem BigDecimal, nie zu einer Gleitkommazahl, weil Floats die meisten Centbeträge nicht exakt halten können. Vergleich es mit einem BigDecimal, das von der Seite gelesen wurde, nicht mit to_f.

Page Objects, damit eine Änderung eine Datei betrifft

Bei komplexen Seiten verpacke ich die Selektoren in ein Page Object. Ändert sich das Design, ändert sich eine Datei, und die Step Definitions bleiben, wie sie sind.

# features/support/pages/checkout_page.rb
class CheckoutPage
  include Capybara::DSL

  def fill_address(address)
    within('#shipping-address') do
      fill_in 'Address line 1', with: address[:line1]
      fill_in 'Town or city', with: address[:city]
      fill_in 'Postcode', with: address[:postcode]
    end
  end

  def apply_discount_code(code)
    fill_in 'Discount code', with: code
    click_button 'Apply'
  end

  def place_order = click_button('Place order')

  def order_total
    BigDecimal(find('#order-total').text.delete('. €').tr(',', '.')) # deutsches Format: 1.099,99 €
  end
end

# features/step_definitions/checkout_steps.rb
def checkout_page = (@checkout_page ||= CheckoutPage.new)

When('I fill in my delivery address') do
  checkout_page.fill_address(line1: 'Ehrenstraße 12', city: 'Köln', postcode: '50672')
end

When('I apply the discount code {string}') { |code| checkout_page.apply_discount_code(code) }

Then('the order total should be {money}') do |expected|
  expect(checkout_page.order_total).to eq(expected)
end

Hilfsmethoden in der World

Cucumber Ruby führt jedes Szenario in einem frischen Objekt aus, das es World nennt, und jeder Schritt läuft in diesem Kontext, du kannst also eigene Module hineinmischen.7 Dort leben Hilfsmethoden wie sign_in_through_the_form:

# features/support/world_extensions.rb
module AuthenticationHelpers
  def sign_in_through_the_form(user, password: 'password123')
    visit new_user_session_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: password
    click_button 'Sign in'
    expect(page).to have_button('Sign out')
  end
end

module MailHelpers
  def emails_sent_to(address) = ActionMailer::Base.deliveries.select { |m| m.to.include?(address) }
end

World(AuthenticationHelpers, MailHelpers)

Feature Dateien als lebende Dokumentation

Das ist der am meisten unterschätzte Nutzen. Wie oft ist die Dokumentation eines Projekts veraltet? Das Anforderungsdokument vom letzten Frühjahr, die Wikiseite, die beschreibt, wie es früher lief, die README, die schon am Tag ihres Entstehens optimistisch war.

Eine Feature Datei, die in jedem Build läuft, kann sich nicht weit von der Wahrheit entfernen, ohne dass es jemand merkt, denn eine grüne Suite heißt, dass das beschriebene Verhalten da ist. Sie dokumentiert nur, was sie abdeckt, sie ist also nicht die ganze Wahrheit, aber was sie sagt, ist aktuell. Die wichtigen baue ich so auf, dass man sie lesen kann:

# features/orders/order_lifecycle.feature
@orders
Feature: Order lifecycle
  Orders move from the basket to completion, and some come back.
  This file documents and checks the valid transitions.

  Background:
    Given a customer with email "customer@example.com"
    And a product "House Blend 1kg" priced at 18,50 €

  Rule: A paid order is confirmed and reduces stock

    Scenario: Normal order
      Given the customer has "House Blend 1kg" in the basket
      When they complete the checkout and the payment succeeds
      Then the order should be "complete"
      And the stock of "House Blend 1kg" should fall by 1
      And the customer should receive an order confirmation email

    Scenario: Card declined
      Given the customer has "House Blend 1kg" in the basket
      When they complete the checkout and the card is declined
      Then the order should still be at "payment"
      And the stock should not change

  Rule: Orders can be cancelled until they ship

    Scenario: Cancelled before dispatch
      Given the customer has a complete order that has not shipped
      When they cancel it
      Then a full refund of 18,50 € should be issued
      And the stock should be restored

    Scenario: Too late to cancel
      Given the customer has an order that has shipped
      When they try to cancel it
      Then the cancellation should be refused
      And they should be pointed to the returns page

Rule, verfügbar seit Gherkin 6, fasst die Szenarien zusammen, die eine Geschäftsregel veranschaulichen.2 Eine neue Entwicklerin kann diese Datei lesen und weiß, wie Bestellungen funktionieren, ohne sich durch den Code zu graben.

Cucumber in einem Rails Projekt einrichten

# Gemfile
group :test do
  gem 'cucumber-rails', require: false
  gem 'database_cleaner-active_record'
  gem 'capybara'
  gem 'selenium-webdriver'
  gem 'factory_bot_rails'
  gem 'timecop'
  gem 'webmock'
end

Die ältere Fassung dieser Liste enthielt das Gem webdrivers. Seine eigene README sagt inzwischen, dass du es ab Selenium 4.11 nicht mehr laden sollst, weil Selenium Manager den Browsertreiber für dich findet und installiert.89

bundle install
bin/rails generate cucumber:install

Der Generator legt features/support/env.rb an. Ich ergänze nur, was die Voreinstellungen nicht abdecken:310

# features/support/env.rb (nach den generierten Zeilen)
Capybara.default_driver = :rack_test                    # schnell, ohne JavaScript
Capybara.javascript_driver = :selenium_chrome_headless  # von Capybara schon registriert
Capybara.default_max_wait_time = 5

Cucumber::Rails::Database.javascript_strategy = :truncation
World(FactoryBot::Syntax::Methods)

require 'webmock/cucumber'
WebMock.disable_net_connect!(allow_localhost: true)

Es gibt keine eigene Datei, die DatabaseCleaner.start und DatabaseCleaner.clean aufruft. Standardmäßig führt cucumber-rails beides selbst um jedes Szenario herum aus, es noch einmal zu tun, macht die Suite nur langsamer.10

bundle exec cucumber                                    # alles
bundle exec cucumber features/orders/order_lifecycle.feature:14  # ein Szenario, nach Zeile
bundle exec cucumber --format html --out reports/cucumber.html

Die Fehler, die ich am häufigsten sehe

Die Umsetzung testen statt des Verhaltens

# Nicht so
Scenario: Create user record
  When I POST to /api/users with JSON {"name": "Dave"}
  Then the response status should be 201
  And the users table should have 1 row

# Sondern so
Scenario: Registering a new account
  Given I am on the registration page
  When I register with the name "Dave"
  Then I should see "Welcome, Dave"
  And I should receive a welcome email

Das erste bricht, sobald sich die API ändert, auch wenn das Verhalten gleich bleibt. Das zweite prüft, was der Kundin wichtig ist. API Verträge verdienen auch Tests, auf ihrer eigenen Ebene.

Schritte, die am Markup hängen

When I click on the button with id "submit-form" and class "btn-primary" bricht bei jedem Redesign. When I submit the form überlebt es. Selektoren gehören in Step Definitions und Page Objects, nie in die Feature Datei.

Eine Datei mit 50 Szenarien

Teil nach Fähigkeit auf: features/orders/placing_orders.feature, features/orders/cancellation.feature, features/users/registration.feature und so weiter.

Riesige Schritte, oder Schritte, die Schritte aufrufen

Ein Schritt, der 100 Zeilen Code versteckt, oder einer, der fünf andere Schritte aufruft, ist schwer zu debuggen. Setz sie aus Hilfsmethoden zusammen, wie bei sign_in_through_the_form.5

Beispiel: ein schlechtes Szenario umschreiben

Der erste Entwurf von Bohnenpost für Geschenkabos lautete:

Scenario: Gift
  Given I click "#gift-toggle"
  And I fill in "gift_email" with "friend@example.com"
  When I press "Pay"
  Then the gift_subscriptions table should have 1 row

Drei Probleme: ein Given, das eine Handlung ist, Selektoren und Feld Ids im Text, und eine Prüfung der Datenbank statt eines Ergebnisses, das ein Mensch sieht. Umgeschrieben:

Scenario: Sending a three month gift subscription
  Given I am buying the "3 month gift" subscription
  When I send it to "friend@example.com" and pay
  Then I should see "Your gift is on its way"
  And "friend@example.com" should receive a gift email

Die Inhaberin kann es jetzt lesen, freigeben und das bemerken, wonach niemand gefragt hatte: was in der Geschenkmail stehen soll.

Warum es für mich zur Arbeit gehört

Ich baue kein Projekt ohne Cucumber oder eine gleichwertige BDD Schicht. Das ist meine Regel, und das bringt sie:

  • Klare Anforderungen. Das Verhalten steht genau fest, bevor der Code entsteht, also verschwindet "ich dachte, du meinst" weitgehend.
  • Weniger Nacharbeit. Wenn das Szenario die Spezifikation ist, schrumpft die Lücke zwischen Bestelltem und Gebautem.
  • Refactoring ohne Angst. Sind die wichtigen Wege abgedeckt, kann ich Code umbauen und weiß innerhalb von Minuten, ob etwas kaputt ist.
  • Dokumentation, die aktuell bleibt. Neue Leute lesen die Feature Dateien.
  • Eine Sprache für alle. Inhaber, Designerinnen und Entwickler lesen und schreiben dieselben Szenarien.
  • Fehler, die behoben bleiben. Jede Fehlerbehebung kommt mit einem Szenario, das den Fehler gefunden hätte.

Nichts davon macht Software perfekt. Es macht die Vereinbarung darüber, was die Software tun soll, ausdrücklich, geprüft und sichtbar, und genau dort beginnt der meiste Ärger, den ich sehe.

Deine erste Feature Datei

  1. Such dir einen Weg aus, der zählt, etwa die Anmeldung oder den Checkout.
  2. Schreib die Feature Datei zuerst in einfacher Sprache. Denk noch nicht an Step Definitions.
  3. Zeig sie jemandem, der keinen Code schreibt, und frag, ob sie sagt, was gemeint ist.
  4. Schreib die Step Definitions, eine nach der anderen.
  5. Lass sie laufen und sieh zu, wie sie besteht, dann mach den Code absichtlich kaputt und sieh zu, wie sie scheitert.
  6. Nimm den nächsten Weg dazu.

Innerhalb weniger Wochen merkst du, dass sich die Gespräche ändern, bevor sich der Code ändert.

Footnotes

  1. Dan North, Introducing BDD, zuerst erschienen im Magazin Better Software, März 2006, https://dannorth.net/blog/introducing-bdd/

  2. Cucumber, Gherkin reference, https://cucumber.io/docs/gherkin/reference/

  3. Capybara, README, https://github.com/teamcapybara/capybara

  4. thoughtbot, factory_bot Getting Started (Konfiguration für RSpec und Cucumber), https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md

  5. Cucumber, Cucumber anti patterns, conjunction steps, https://cucumber.io/docs/guides/anti-patterns/

  6. Cucumber, API reference: tag expressions and hooks, https://cucumber.io/docs/cucumber/api/

  7. Cucumber, Sharing state between steps, https://cucumber.io/docs/cucumber/state/

  8. Titus Fortner, webdrivers README, update on the future of this project, https://github.com/titusfortner/webdrivers

  9. Selenium, Selenium Manager, Selenium Dokumentation, https://www.selenium.dev/documentation/selenium_manager/

  10. Cucumber, cucumber-rails README, https://github.com/cucumber/cucumber-rails

Wenn aus deinen Anforderungen immer wieder Nacharbeit wird, bring eine Funktion, die du gleich bauen willst, in den kostenlosen Workshop mit. Wir schreiben ihre Szenarien gemeinsam, bevor es Code gibt, und die ersten zwei Wochen Arbeit gehen auf meine Rechnung.