The essentials of Cucumber-JVM and Spock - a handbook written for the BDD/TDD Masterclass (https://johnfergusonsmart.com/programs-courses/bdd-tdd-clean-coding/)
1. Cucumber and Spock Primer
BDD Masterclass
Building software that makes a difference, and building it well
@wakaleo
www.johnfergusonsmart.com
reachme@johnfergusonsmart.com
3. Overview
Overview
Basic Gherkin
Feature: Earning Frequent Flyer points through flights
Scenario: Economy flights earn points by distance
Given the distance from London to Paris is 344 km
When I fly from London to Paris in Economy
Then I should earn 344 points
A feature represents a
deliverable piece of functionality
Each feature should
have a title
A feature contains one
or more scenarios
Features and Scenarios
4. Overview
Overview
Basic Gherkin
Feature: Earning Frequent Flyer points through flights
In order to encourage repeat business
As an Airline Sales Manager
I want customers to be able to cumulate points when they
fly with us
Scenario: Economy flights earn points by distance
Economy class earns 1 point per kilometre
Given the distance from London to Paris is 344 km
When I fly from London to Paris in Economy
Then I should earn 344 points
Free-text description
(great for living documentation)
You can also put a description
for a Scenario
Free text descriptions
5. Overview
Overview
Basic Gherkin
Scenario: Economy flights earn points by distance
Economy class earns 1 point per kilometre
Given the distance from London to Paris is 344 km
When I fly from London to Paris in Economy
Then I should earn 344 points
GIVEN: Preconditions
WHEN: Action to illustrate
THEN: Expected outcome
Scenarios
6. Overview
Overview
Basic Gherkin
Scenario: Economy flights over several legs
Given the distance from London to Paris is 344 km
And the distance from Paris to Krakow is 1500 km
When I fly from London to Krakow via Paris in Economy
Then I should earn 1844 points
And I should earn 18 status points
Multiple preconditions
Scenarios
Multiple outcomes
7. Overview
Overview
Basic Gherkin
Given steps
✓ Defines preconditions and context
✓ Typically something that happened in the past
✓ Can interact with the system
✓ Should not perform any actions being tested by the
scenario
Given a user registers on the site
And the user logs in
When the home page appears
Then the user’s name should be visible
Given a user has registered on the site
When the user logs in
Then the user’s name should be visible on
the home page
What are we testing here?
8. Overview
Overview
Basic Gherkin
When steps
✓ The action under test
✓ Avoid chaining too many clauses
Given a user has registered on the site
When the user enters 'Scott' in the user name
And the user enters 'Tiger' in the password
And the user selects 'UK' as the country
And the user clicks on the Login button
And the user validates the Cookies alert
Then the user’s name should be visible on the home page
Too much detail
To implementation-specific
9. Overview
Overview
Basic Gherkin
Then steps
✓ The expected outcome
✓ Can query the system
✓ Should not perform actions on the system
Given a batch file needs to be processed
When the user uploads the batch file
Then the result should be visible
And a notification message should be sent to the user
10. Overview
Overview
Basic Gherkin
Background steps
✓ Common steps for all the scenarios in a feature file
✓ Avoid duplication
✓ More focused scenarios
Feature: Earning Frequent Flyer points through flights
In order to encourage repeat business
As an Airline Sales Manager
I want customers to be able to cumulate points when
they fly with us
Background:
Given the distance from London to Paris is 344 km
And the distance from Paris to Krakow is 1500km
Scenario: Economy flights earn points by distance
When I fly from London to Paris in Economy
Then I should earn 344 points
And I should earn 3 status points
Scenario: Economy flights over several legs
When I fly from London to Krakow via Paris in Economy
Then I should earn 1844 points
And I should earn 18 status points
11. Overview
Overview
Basic Gherkin
Scenario Outlines
✓ Use a table to simplify duplicated but similar scenarios
Background:
Given the distance from London to Paris is 344 km
And the distance from Paris to Krakow is 1500km
And the distance from London to Budapest is 1600km
Scenario: Economy flights earn points by distance
When I fly from London to Paris in Economy
Then I should earn 344 points
And I should earn 3 status points
Scenario: Economy flights to Krakow
When I fly from London to Krakow in Economy
Then I should earn 1500 points
And I should earn 15 status points
Scenario: Economy flights to Budapest
When I fly from London to Budapest in Economy
Then I should earn 1600 points
And I should earn 16 status points
12. Overview
Overview
Basic Gherkin
Scenario Outlines
✓ Use a table to simplify duplicated but similar scenarios
Background:
Given the distance from London to Paris is 344 km
And the distance from Paris to Krakow is 1500km
And the distance from London to Budapest is 1600km
Scenario Outline:
When I fly from <departure> to <destination> in Economy
Then I should earn <points> points
And I should earn <status-points> status points
Examples:
| departure | destination | points | status-points |
| London | Paris | 344 | 3 |
| Paris | Krakow | 1500 | 15 |
| London | Budapest | 1600 | 16 |
13. Overview
Overview
Basic Gherkin
Embedded tables
✓ Use tables in steps to pass tabular data to the step
Scenario: Deposit into a current account
Given Joe has the following accounts:
| Number | Type | Balance |
| 123456 | Current | 1000 |
When he deposits €100 into his Current account
Then he should have the following balances:
| Number | Type | Balance |
| 123456 | Current | 1100 |
14. Overview
Overview
Basic Gherkin
Embedded tables
✓ Use tables in steps to pass tabular data to the step
Feature: Earning Frequent Flyer points through flights
In order to encourage repeat business
As an Airline Sales Manager
I want customers to be able to cumulate points when they fly with us
Background:
Given the following flight routes:
| departure | destination | distance |
| London | Paris | 344 |
| Paris | Krakow | 1500 |
| London | Budapest | 1600 |
Scenario Outline:
When I fly from <departure> to <destination> in Economy
Then I should earn <points> points
And I should earn <status-points> status points
Examples:
| departure | destination | points | status-points |
| London | Paris | 344 | 3 |
| Paris | Krakow | 1500 | 15 |
| London | Budapest | 1600 | 16 |
15. Overview
Overview
Basic Gherkin
Multi-line string parameters
✓ Pass in multi-line strings as parameters
Given a travel provides feedback about the flight
"""
It was OK
The food was rubbish but the staff were nice
The flight was delayed by 15 minutes
"""
17. Step Definitions
in Java with
Cucumber and
Serenity BDD
The Cucumber Test Runner
import cucumber.api.CucumberOptions;
import net.serenitybdd.cucumber.CucumberWithSerenity;
import org.junit.runner.RunWith;
@RunWith(CucumberWithSerenity.class)
@CucumberOptions(features = "src/test/resources/features/deposit_funds",
glue = "com.bddinaction.serenitybank")
public class DepositFunds {}
A Cucumber test runner
Which feature files to run
Where to find the step definition classes
18. Step Definitions
in Java with
Cucumber and
Serenity BDD
Step Definitions
Feature: Earning Frequent Flyer points through flights
Scenario: Economy flights earn points by distance
Given the distance from London to Paris is 344 km
When I fly from London to Paris in Economy
Then I should earn 344 points
@Given("^the distance from (.*) to (.*) is (d+) km$")
public void pointsEarnedBetweenCities(String departure,
String destination,
int distance)
throws Throwable {
}
@Given, @When, or @Then annotation
Matching regular expression
Matched expressions are passed in as parameters
19. Step Definitions
in Java with
Cucumber and
Serenity BDD
Matching Lists
Given the following possible destinations: London, Paris, Amsterdam
@Given("^the following possible destinations: (.*)$")
public void theFollowingPossibleDestinations(List<String> destinations) {
}
Declare a list of Strings
Given the following possible destinations:
| London |
| Paris |
| Amsterdam |
alternative notation
20. Step Definitions
in Java with
Cucumber and
Serenity BDD
Matching enums
When I fly from London to Paris in Economy
public enum City {
London, Paris, Amsterdam
}
public enum CabinClass {
Economy, Business, First
}
@When("^I fly from (.*) to (.*) in (.*)$")
public void iFly(City departure, City destination, CabinClass cabinClass){
}
Enums are matched automatically
21. Step Definitions
in Java with
Cucumber and
Serenity BDD
Matching tables
Given the following flight routes:
| departure | destination | distance |
| London | Paris | 344 |
| Paris | Krakow | 1500 |
| London | Budapest | 1600 |
public void flightRoutes(Map<String, String> flightRoutes) {}
Tables can be passed as a map of Strings
22. Step Definitions
in Java with
Cucumber and
Serenity BDD
Transformers
When I fly from San Francisco to Paris in Economy
public enum City {
London("London"),
Paris("Paris"),
SanFrancisco("San Francisco"),
SaintPetersbuug("St Petersburg");
public final String name;
City(String name) { this.name = name; }
}
@When("^I fly from (.*) to (.*) in (.*)$")
public void iFly(@Transform(CityConverter.class) City departure,
@Transform(CityConverter.class) City destination,
CabinClass cabinClass) {}
public class CityConverter extends Transformer<City> {
@Override
public City transform(String cityName) {
return Arrays.stream(City.values())
.filter(city -> city.name.equalsIgnoreCase(cityName))
.findFirst()
.orElseThrow(() -> new UnknownCityException(cityName));
}
}
24. Overview
Overview
Introduction
to Spock
http://spockframework.org/
import spock.lang.Specification
class WhenManagingAnAccount extends Specification {
def "depositing a sum into the account should update the balance"() {
given:
def currentAccount = new BankAccount("123456", AccountType.Current)
when:
currentAccount.deposit(1000)
then:
currentAccount.balance == 1000
}
}
All Spock tests extend Specification
Plain English test names
Arrange
Act
Assert
Acts as an assertion
25. Overview
Overview
Using Fields
class WhenWithdrawingFunds extends Specification {
def accountService = new AccountService();
def "withdrawing funds from a current account"() {
given:
def accountNumber = accountService.createNewAccount(Current, 100.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
accountService.getBalance(accountNumber) == 50.00
}
def "withdrawing funds from a savings account"() {
given:
def accountNumber = accountService.createNewAccount(BigSaver, 1000.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
thrown(MinimumBalanceRequiredException)
and:
accountService.getBalance(accountNumber) == 1000.00
}
}
Instantiated once before each test
26. Overview
Overview
Using Fields
class WhenWithdrawingFunds extends Specification {
@Shared def accountService = new AccountService()
def "withdrawing funds from a current account"() {
given:
def accountNumber = accountService.createNewAccount(Current, 100.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
accountService.getBalance(accountNumber) == 50.00
}
def "withdrawing funds from a savings account"() {
given:
def accountNumber = accountService.createNewAccount(BigSaver, 1000.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
thrown(MinimumBalanceRequiredException)
and:
accountService.getBalance(accountNumber) == 1000.00
}
}
Instantiated once and shared between the tests
27. Overview
Overview
Using Fields
class WhenWithdrawingFunds extends Specification {
def accountService
def setup() {
accountService = new AccountService()
}
def "withdrawing funds from a current account"() {
given:
def accountNumber = accountService.createNewAccount(Current, 100.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
accountService.getBalance(accountNumber) == 50.00
}
def "withdrawing funds from a savings account"() {
given:
def accountNumber = accountService.createNewAccount(BigSaver, 1000.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
thrown(MinimumBalanceRequiredException)
and:
accountService.getBalance(accountNumber) == 1000.00
}
}
Runs before each test
28. Overview
Overview
Using Fields
class WhenWithdrawingFunds extends Specification {
@Shared def accountService = new AccountService()
def setupSpec() {
accountService = new AccountService()
}
def "withdrawing funds from a current account"() {
given:
def accountNumber = accountService.createNewAccount(Current, 100.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
accountService.getBalance(accountNumber) == 50.00
}
def "withdrawing funds from a savings account"() {
given:
def accountNumber = accountService.createNewAccount(BigSaver, 1000.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
thrown(MinimumBalanceRequiredException)
and:
accountService.getBalance(accountNumber) == 1000.00
}
}
Runs once at the start of the Specification
See also cleanup() and cleanupSpec()
29. Overview
Overview
Exceptions
class WhenWithdrawingFunds extends Specification {
@Shared def accountService = new AccountService()
def "withdrawing funds from a savings account"() {
given:
def accountNumber = accountService.createNewAccount(BigSaver, 1000.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00,
LocalDate.now())
then:
thrown(MinimumBalanceRequiredException)
and:
accountService.getBalance(accountNumber) == 1000.00
}
}
Expect this exception to be thrown
Also, the balance should be unchanged
30. Overview
Overview
Exceptions
def "withdrawing funds from a savings account"() {
given:
def accountNumber = accountService.createNewAccount(BigSaver, 1000.00)
when:
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then:
MinimumBalanceRequiredException exception = thrown()
and:
exception.message == "Minimum balance for this account is €1000.00"
and:
accountService.getBalance(accountNumber) == 1000.00
}
Expect this exception to be thrown
Check exception attributes
31. Overview
Overview
Data-Driven
Specifications
class WhenCalculatingDepositFees extends Specification {
def “correct deposit should be calculated"() {
expect:
DepositFee.forAccountType(accountType).apply(amount) == expectedDepositFee
where:
accountType | amount | expectedDepositFee
Current | 1000.0 | 0.0
BasicSavings | 100.00 | 0.50
BigSaver | 10.00 | 0.50
BigSaver | 100.01 | 0.75
BigSaver | 1000.01 | 1.25
}
}
Use these variables in the test
32. Overview
Overview
Data-Driven
Specifications
class WhenCalculatingDepositFees extends Specification {
@Unroll
def “A deposit of #amount in an #accountType account"() {
expect:
DepositFee.forAccountType(accountType).apply(amount) == expectedDepositFee
where:
accountType | amount | expectedDepositFee
Current | 1000.0 | 0.0
BasicSavings | 100.00 | 0.50
BigSaver | 10.00 | 0.50
BigSaver | 100.01 | 0.75
BigSaver | 1000.01 | 1.25
}
}
Table variables can be placed in the test title
Report a separate test for each row of data
33. Overview
Overview
Test Doubles
✓ Test Doubles
✓ Stand-ins for dependencies
✓ Transparently replace objects used by
the class under test
✓ Make tests faster and easier to write
34. Overview
Overview
Test Doubles
✓ Test Doubles stand in for objects that are
✓ Unavailable or unfinished
✓ Slow
✓ Needs something that is unavailable
✓ Hard to instantiate
43. Overview
Overview
Test Doubles
✓ Mocks
✓ Only mock what you own
✓ Verify specification, not implementation
✓ Specify as little as possible in a test
✓ Only mock your immediate neighbours
✓ Don’t mock boundary objects
✓ Don’t mock domain objects
✓ Keep it simple
44. Overview
Overview
Test Doubles
in Spock
def vatService = Mock(VATService)
def seller = new Seller(vatService);
def “VAT should apply on ordinary articles"() {
given: "the VAT rate for shirts is 20%"
seller = new Seller(vatService);
vatService.getRateFor("shirt") >> 0.2
and: "we are selling a shirt"
def sale = seller.sells(1, "shirt").forANetPriceOf(10.00)
when: "we calculate the price including VAT"
def totalPrice = sale.totalPrice
then: "the price should include GST of 20%"
totalPrice == 12.00
}
The seller thinks he is using a real VATService
Create a mocked version of the VATService class
Stubbed return value for the getRateFor() method
45. Overview
Overview
Test Doubles
in Spock
def vatService = Mock(VATService)
…
VATService vatService = Mock()
You can create a mock object in two ways
(This way is more IDE-friendly)
def vatService = Mock(VATService)
…
VATService vatService = Mock()
Creating Mocks
46. Overview
Overview
Test Doubles
in Spock
def vatService = Stub(VATService)
…
VATService vatService = Stub()
A Stub returns empty or “dummy” responses for all method calls
(This way is more IDE-friendly)
Creating Stubs
47. Overview
Overview
Test Doubles
in Spock
vatService.getRateFor("shirt") >> 0.2
Mock the result of a particular method call
Return different values on successive calls
vatService.getRateFor("shirt") >>> [0.2, 0.1]
vatService.getRateFor("shirt") >>
{ throw new UnknownProductException()}
Do something in a closure
Mocking interactions
VATService vatService = Mock() {
getRateFor("shirt") >> 0.2
getRateFor("banana") >> 0.1
}
Mock the interactions at creation time
48. Overview
Overview
Test Doubles
in Spock
def "The VAT Service should be used to obtain VAT values"() {
given: "the GST rate for shirts is 10%"
seller = new Seller(vatService);
vatService.getRateFor("shirt") >> 0.1
when: "we sell a shirt"
seller.sells(1, "shirt").forANetPriceOf(10.00)
then: "the VAT service should be used to determine the GST rate"
1 * vatService.getRateFor("shirt")
}
This method should be called exactly once
Checking interactions
49. Overview
Overview
Interactions in
Spock
1 * vatService.getRateFor("shirt") // Exactly once with this parameter
(1.._) * vatService.getRateFor("shirt") // At least once
(_..2) * vatService.getRateFor("shirt") // No more than twice
vatService.getRateFor(_) // Called with any parameter
vatService.getRateFor({it != "banana"}) // Custom constraint
Checking interactions
50. Overview
Overview
Documentation
def "withdrawing funds from a current account"() {
given: "a current account with €100"
def accountNumber = accountService.createNewAccount(Current, 100.00)
when: "we withdraw €50"
accountService.makeWithdrawal(accountNumber, 50.00, LocalDate.now())
then: "the balance should be €50"
accountService.getBalance(accountNumber) == 50.00
}
Descriptive texts go after the labels
51. Thank You!
AUTHOR OF ‘BDD IN ACTION’
@wakaleo
www.johnfergusonsmart.com
reachme@johnfergusonsmart.com