Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Cucumber and Spock Primer

2,660 views

Published on

The essentials of Cucumber-JVM and Spock - a handbook written for the BDD/TDD Masterclass (https://johnfergusonsmart.com/programs-courses/bdd-tdd-clean-coding/)

Published in: Software

Cucumber and Spock Primer

  1. 1. Cucumber and Spock Primer BDD Masterclass Building software that makes a difference, and building it well @wakaleo www.johnfergusonsmart.com reachme@johnfergusonsmart.com
  2. 2. Writing Acceptance Criteria
 in Gherkin
  3. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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 """
  16. 16. Cucumber Step Definitions
  17. 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. 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. 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. 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. 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. 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)); } }
  23. 23. BDD Unit Testing with Spock
  24. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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
  35. 35. Overview Overview Test Doubles A complex remote service
  36. 36. Overview Overview Test Doubles def "should apply correct discount for a valid discount code"() { given: PriceService priceService = ??? OrderProcessor processor = new OrderProcessor(priceService); and: def mouse = new Product("Wireless Mouse", 20.00); def tenPercentOffPrice = 18.00; when: def order = ProductOrder.discountedOrder(mouse, “MINUS_10_PERCENT"); def discountedPrice = processor.calculateTotalPriceFor(order) then: discountedPrice == tenPercentOffPrice; } How do we get a PriceService?
  37. 37. Overview Overview Test Doubles class PriceServiceTestDouble extends PriceService { private final double discount; TestPriceService(double discount) { this.discount = discount; } @Override public double percentageDiscountForCode(String discountCode) { return discount; } } A test double
  38. 38. Overview Overview Test Doubles def "should apply correct discount for a valid discount code"() { given: PriceService priceService = new PriceServiceTestDouble(0.20) OrderProcessor processor = new OrderProcessor(priceService); and: def mouse = new Product("Wireless Mouse", 20.00); def tenPercentOffPrice = 18.00; when: def order = ProductOrder.discountedOrder(mouse, “MINUS_10_PERCENT"); def discountedPrice = processor.calculateTotalPriceFor(order) then: discountedPrice == tenPercentOffPrice; } Now use the test double
  39. 39. Overview Overview Test Doubles ✓ There are several types of Test Doubles ✓ Stubs ✓ Fakes ✓ Mocks
  40. 40. Overview Overview Test Doubles ✓ Stubs ✓ Minimal implementation ✓ Returns hard-coded values ✓ State-based testing class PriceServiceTestDouble extends PriceService { @Override public double percentageDiscountForCode(String discountCode) { return 10; } }
  41. 41. Overview Overview Test Doubles ✓ Fakes ✓ Smarter stubs ✓ Some simple behaviour ✓ State-based testing public class MockEnvironmentVariables implements EnvironmentVariables { private Properties properties = new Properties(); public String getProperty(String name) { return properties.getProperty(name); } public String getProperty(String name, String defaultValue) { return properties.getProperty(name, defaultValue); } public void setProperty(String name, String value) { properties.setProperty(name, value); } public void setValue(String name, String value) { values.setProperty(name, value); } }
  42. 42. Overview Overview Test Doubles ✓ Mocks ✓ Sophisticated stubs ✓ Record and verify interactions ✓ Need a mocking framework
  43. 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. 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. 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. 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. 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. 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. 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. 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. 51. Thank You! AUTHOR OF ‘BDD IN ACTION’ @wakaleo www.johnfergusonsmart.com reachme@johnfergusonsmart.com

×