As projects get faster and teams get leaner, the need to write high quality automated acceptance criteria quickly and efficiently has never been greater. Engineers in Test simply cannot afford to spend time maintaining brittle tests. And yet, without solid test automation strategies, this is what many teams find themselves doing. In this workshop, you will learn a better way. You will learn how to write clean, clear and maintainable tests using the Screenplay Pattern, an innovative new approach to writing BDD-style automated acceptance tests that are easier to understand, easier to extend and easier to maintain. The workshop will be a practical demonstration of the principles of good automated test design. There will be live coding of real-world BDD automated acceptance tests in abundance, using Java, Serenity BDD and Cucumber. We will go from requirements and BDD-style Acceptance Criteria in Cucumber right through to automated acceptance tests and living documentation.
29. serenity-bdd.info#SerenityBDD @Wakaleo
Open the Journey Planner
Choose origin: Waterloo
Choose destination: Canary Wharf
Choose time of departure: 9:00 AM
Click on the “change time” link
Click on the “Leaving” button
30. serenity-bdd.info#SerenityBDD @Wakaleo
Open the Journey Planner
Choose origin: Waterloo
Choose destination: Canary Wharf
Choose time of departure: 9:00 AM
Click on the “change time” link
Click on the “Leaving” button
Select “09:00”
33. serenity-bdd.info#SerenityBDD @Wakaleo
Open the Journey Planner
Choose origin: Waterloo
Choose destination: Canary Wharf
Choose time of departure: 9:00 AM
Confirm selection
Click on the “Plan my journey”
34. serenity-bdd.info#SerenityBDD @Wakaleo
Open the Journey Planner
Choose origin: Waterloo
Choose destination: Canary Wharf
Choose time of departure: 9:00 AM
Confirm selection
Click on the “Plan my journey”
Wait for the results to load
36. serenity-bdd.info#SerenityBDD @Wakaleo
Open the Journey Planner
Choose origin: Waterloo
Choose destination: Canary Wharf
Choose time of departure: 9:00 AM
Confirm selection
See a Jubilee Line train at 08:59
39. serenity-bdd.info#SerenityBDD @Wakaleo
private final By origin = By.id("InputFrom");
private final By originSuggestions = By.cssSelector(".tt-suggestions");
private final By destination = By.id("InputTo");
private final By destinationSuggestions = By.cssSelector(".tt-suggestions");
private final By changeTimeLink = By.partialLinkText("change time");
private final By leavingButton = By.cssSelector("label[for=departing]");
private final By timeSelector = By.id("Time");
private final By planMyJourneyButton = By.cssSelector("#plan-a-journey .plan-journey-button");
private final By journeyResults = By.cssSelector(".summary-results");
private final By fastestJourney = By.cssSelector(".publictransport-box span.time");
49. serenity-bdd.info#SerenityBDD @Wakaleo
private final By origin = By.id("InputFrom");
private final By originSuggestions = By.cssSelector(".tt-suggestions");
private final By destination = By.id("InputTo");
private final By destinationSuggestions = By.cssSelector(".tt-suggestions");
private final By changeTimeLink = By.partialLinkText("change time");
private final By leavingButton = By.cssSelector("label[for=departing]");
private final By timeSelector = By.id("Time");
private final By planMyJourneyButton = By.cssSelector("#plan-a-journey .plan-journey-button");
private final By journeyResults = By.cssSelector(".summary-results");
private final By fastestJourney = By.cssSelector(".publictransport-box span.time");
private WebDriver browser;
@Before
public void openBrowser() {
browser = new FirefoxDriver();
}
@After
public void closeBrowser() {
browser.quit();
}
@Test
public void planning_a_journey() {
browser.get("https://tfl.gov.uk/");
browser.findElement(origin).sendKeys("Waterloo");
WebDriverWait wait = new WebDriverWait(browser, 10);
wait.until(visibilityOfElementLocated(originSuggestions));
browser.findElement(originSuggestions).sendKeys(ARROW_DOWN);
browser.findElement(originSuggestions).sendKeys(ENTER);
browser.findElement(destination).sendKeys("Canary Wharf");
wait.until(visibilityOfElementLocated(destinationSuggestions));
browser.findElement(destinationSuggestions).sendKeys(ARROW_DOWN);
browser.findElement(destinationSuggestions).sendKeys(ENTER);
browser.findElement(changeTimeLink).click();
browser.findElement(leavingButton).click();
new Select(browser.findElement(timeSelector)).selectByVisibleText("09:00");
browser.findElement(planMyJourneyButton).click();
wait.until(visibilityOfElementLocated(journeyResults));
String fastestDepartureTime = startTimeOf(browser.findElement(fastestJourney).getText());
6 tasks ?
50. serenity-bdd.info#SerenityBDD @Wakaleo
6 tasks buried in
36 lines of noise
private final By origin = By.id("InputFrom");
private final By originSuggestions = By.cssSelector(".tt-suggestions");
private final By destination = By.id("InputTo");
private final By destinationSuggestions = By.cssSelector(".tt-suggestions");
private final By changeTimeLink = By.partialLinkText("change time");
private final By leavingButton = By.cssSelector("label[for=departing]");
private final By timeSelector = By.id("Time");
private final By planMyJourneyButton = By.cssSelector("#plan-a-journey .plan-journey-button");
private final By journeyResults = By.cssSelector(".summary-results");
private final By fastestJourney = By.cssSelector(".publictransport-box span.time");
private WebDriver browser;
@Before
public void openBrowser() {
browser = new FirefoxDriver();
}
@After
public void closeBrowser() {
browser.quit();
}
@Test
public void planning_a_journey() {
browser.get("https://tfl.gov.uk/");
browser.findElement(origin).sendKeys("Waterloo");
WebDriverWait wait = new WebDriverWait(browser, 10);
wait.until(visibilityOfElementLocated(originSuggestions));
browser.findElement(originSuggestions).sendKeys(ARROW_DOWN);
browser.findElement(originSuggestions).sendKeys(ENTER);
browser.findElement(destination).sendKeys("Canary Wharf");
wait.until(visibilityOfElementLocated(destinationSuggestions));
browser.findElement(destinationSuggestions).sendKeys(ARROW_DOWN);
browser.findElement(destinationSuggestions).sendKeys(ENTER);
browser.findElement(changeTimeLink).click();
browser.findElement(leavingButton).click();
new Select(browser.findElement(timeSelector)).selectByVisibleText("09:00");
browser.findElement(planMyJourneyButton).click();
wait.until(visibilityOfElementLocated(journeyResults));
String fastestDepartureTime = startTimeOf(browser.findElement(fastestJourney).getText());
82. serenity-bdd.info#SerenityBDD @Wakaleo
Plan my journey
Actor Goals Tasks Interactions
Press “Down”
Type “Waterloo”
Choose Destination
Choose Origin
Choose time of departure
Press “Enter”
Confirm selection
Connor the
commuter
84. serenity-bdd.info#SerenityBDD @Wakaleo
Open the Journey Planner
Choose origin of Waterloo
Choose destination of Canary Wharf
Choose time of departure: 09:00
Confirm selection
See if the fastest train departs at 08:59
91. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN)
);
}
}
92. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
);
}
}
93. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
.forNoMoreThan(10).seconds(),
);
}
}
94. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
.forNoMoreThan(10).seconds(),
PickFirstSuggestion.from(JourneyPlanner.ORIGIN)
);
}
private final String station;
}
95. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
.forNoMoreThan(10).seconds(),
PickFirstSuggestion.from(JourneyPlanner.ORIGIN)
);
}
private final String station;
public ChooseOrigin(String origin) { this.station = origin;}
}
96. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
.forNoMoreThan(10).seconds(),
PickFirstSuggestion.from(JourneyPlanner.ORIGIN)
);
}
private final String station;
public ChooseOrigin(String origin) { this.station = origin;}
public static Performable of(String station) {
return instrumented(ChooseOrigin.class, station);
}
}
97. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Step("{0} selects origin station of #station")
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
.forNoMoreThan(10).seconds(),
PickFirstSuggestion.from(JourneyPlanner.ORIGIN)
);
}
private final String station;
public ChooseOrigin(String origin) { this.station = origin;}
public static Performable of(String station) {
return instrumented(ChooseOrigin.class, station);
}
}
98. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Step("{0} selects origin station of #station")
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
.forNoMoreThan(10).seconds(),
PickFirstSuggestion.from(JourneyPlanner.ORIGIN)
);
}
private final String station;
public ChooseOrigin(String origin) { this.station = origin;}
public static Performable of(String station) {
return instrumented(ChooseOrigin.class, station);
}
}
How the step appears in the reports
99. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Step("{0} selects origin station of #station")
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
.forNoMoreThan(10).seconds(),
PickFirstSuggestion.from(JourneyPlanner.ORIGIN)
);
}
private final String station;
public ChooseOrigin(String origin) { this.station = origin;}
public static Performable of(String station) {
return instrumented(ChooseOrigin.class, station);
}
}
The name of the actor
100. serenity-bdd.info#SerenityBDD @Wakaleo
public class ChooseOrigin implements Task {
@Step("{0} selects origin station of #station")
@Override
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(station).into(JourneyPlanner.ORIGIN),
WaitUntil.the(JourneyPlanner.ORIGIN_SUGGESTIONS, isVisible())
.forNoMoreThan(10).seconds(),
PickFirstSuggestion.from(JourneyPlanner.ORIGIN)
);
}
private final String station;
public ChooseOrigin(String origin) { this.station = origin;}
public static Performable of(String station) {
return instrumented(ChooseOrigin.class, station);
}
}
A member variable
117. serenity-bdd.info#SerenityBDD @Wakaleo
public class JourneyPlanner {
public static final Target ORIGIN = Target.the("Departure station")
.locatedBy("#InputFrom");
public static final Target ORIGIN_SUGGESTIONS = Target.the("Origin Suggestions")
.locatedBy("//*[input[@id='InputFrom']]/*[contains(@class, 'tt-dropdown-menu')]");
public static final Target DESTINATION = Target.the("Destination station")
.locatedBy("#InputTo");
public static final Target DESTINATION_SUGGESTIONS = Target.the("Destination Suggestions")
.locatedBy("//*[input[@id='InputTo']]/*[contains(@class, 'tt-dropdown-menu')]");
public static final Target CHANGE_TIME = Target.the("Change Time")
.located(By.partialLinkText("change time"));
public static final Target LEAVING_BUTTON = Target.the("Leaving")
.located(By.cssSelector("label[for=departing]"));
public static final Target SELECTED_DEPARTURE_TIME = Target.the("Departure Time")
.located(By.id("Time"));
public static final Target SELECTED_DEPARTURE_DROPDOWN = Target.the("Departure Time")
.located(By.cssSelector(".hours"));
public static final Target PLAN_MY_JOURNEY = Target.the("Plan my journey")
.locatedBy("#plan-a-journey .plan-journey-button");;
}
124. serenity-bdd.info#SerenityBDD @Wakaleo
public class FilterItems implements Task {
private final String filter;
public <T extends Actor> void performAs(T theActor) {
}
public static FilterItems toShow(String filter) {
return instrumented(FilterItems.class, status);
}
public FilterItems(TodoStatusFilter filter) { this.filter = filter; }
}
125. serenity-bdd.info#SerenityBDD @Wakaleo
public class FilterItems implements Task {
private final String filter;
public <T extends Actor> void performAs(T theActor) {
theActor.attemptsTo(
Click.on(TodoList.FILTER
);
}
public static FilterItems toShow(String filter) {
return instrumented(FilterItems.class, status);
}
public FilterItems(TodoStatusFilter filter) { this.filter = filter; }
}
Click on the filter button
126. serenity-bdd.info#SerenityBDD @Wakaleo
public class FilterItems implements Task {
private final String filter;
public <T extends Actor> void performAs(T theActor) {
theActor.attemptsTo(
Click.on(TodoList.FILTER.of(filter)
);
}
public static FilterItems toShow(String filter) {
return instrumented(FilterItems.class, status);
}
public FilterItems(TodoStatusFilter filter) { this.filter = filter; }
}
Pass in the name of the filter
127. serenity-bdd.info#SerenityBDD @Wakaleo
public class FilterItems implements Task {
private final String filter;
public <T extends Actor> void performAs(T theActor) {
theActor.attemptsTo(
Click.on(TodoList.FILTER.of(filter)
.called(“filter by "+ filter))
);
}
public static FilterItems toShow(String filter) {
return instrumented(FilterItems.class, status);
}
public FilterItems(TodoStatusFilter filter) { this.filter = filter; }
How will this interaction appear in the reports?
128. serenity-bdd.info#SerenityBDD @Wakaleo
public class FilterItems implements Task {
private final String filter;
@Step("{0} filters items by #filter")
public <T extends Actor> void performAs(T theActor) {
theActor.attemptsTo(
Click.on(TodoList.FILTER.of(filter)
.called(“filter by "+ filter))
);
}
public static FilterItems toShow(String filter) {
return instrumented(FilterItems.class, status);
}
How will this task appear in the reports
143. serenity-bdd.info#SerenityBDD @Wakaleo
public class TheFastest {
public static Question<String> tubeLine() {
return new FastestTubeLineQuestion();
}
}
public class FastestTubeLineQuestion implements Question<String> {
}
144. serenity-bdd.info#SerenityBDD @Wakaleo
public class TheFastest {
public static Question<String> tubeLine() {
return new FastestTubeLineQuestion();
}
}
public class FastestTubeLineQuestion implements Question<String> {
@Override
public String answeredBy(Actor actor) {
}
}
145. serenity-bdd.info#SerenityBDD @Wakaleo
public class TheFastest {
public static Question<String> tubeLine() {
return new FastestTubeLineQuestion();
}
}
public class FastestTubeLineQuestion implements Question<String> {
@Override
public String answeredBy(Actor actor) {
return Text.of(JourneyResults.ITINERARY)
}
}
146. serenity-bdd.info#SerenityBDD @Wakaleo
public class TheFastest {
public static Question<String> tubeLine() {
return new FastestTubeLineQuestion();
}
}
public class FastestTubeLineQuestion implements Question<String> {
@Override
public String answeredBy(Actor actor) {
return Text.of(JourneyResults.ITINERARY)
.viewedBy(actor)
}
}
147. serenity-bdd.info#SerenityBDD @Wakaleo
public class TheFastest {
public static Question<String> tubeLine() {
return new FastestTubeLineQuestion();
}
}
public class FastestTubeLineQuestion implements Question<String> {
@Override
public String answeredBy(Actor actor) {
return Text.of(JourneyResults.ITINERARY)
.viewedBy(actor)
.asString();
}
}
148. serenity-bdd.info#SerenityBDD @Wakaleo
public class TheFastest {
public static Question<String> tubeLine() {
return new FastestTubeLineQuestion();
}
}
@Subject("the fastest tube line")
public class FastestTubeLineQuestion implements Question<String> {
@Override
public String answeredBy(Actor actor) {
return Text.of(JourneyResults.ITINERARY)
.viewedBy(actor)
.asString();
}
}
166. serenity-bdd.info#SerenityBDD @Wakaleo
Feature: Plan a new journey
Scenario: Plan a new journey
Given that Connie is a London commuter
When she plans a journey from Waterloo to Canary Wharf departing at 09:00
Then she should see that the fastest train departs at 08:59
168. serenity-bdd.info#SerenityBDD @Wakaleo
public class PlanAJourneyStepDefinitions {
@Before
public void set_the_stage() {
OnStage.setTheStage(new OnlineCast());
}
@Given("^that (.*) is a London commuter$")
public void a_london_commuter_named(String commuterName) throws Throwable {
theActorCalled(commuterName);
}
169. serenity-bdd.info#SerenityBDD @Wakaleo
public class PlanAJourneyStepDefinitions {
@Before
public void set_the_stage() {
OnStage.setTheStage(new OnlineCast());
}
@Given("^that (.*) is a London commuter$")
public void a_london_commuter_named(String commuterName) throws Throwable {
theActorCalled(commuterName);
}
170. serenity-bdd.info#SerenityBDD @Wakaleo
Feature: Plan a new journey
Scenario: Plan a new journey
Given that Connie is a London commuter
When she plans a journey from Waterloo to Canary Wharf departing at 09:00
Then she should see that the fastest train departs at 08:59
171. serenity-bdd.info#SerenityBDD @Wakaleo
public class PlanAJourneyStepDefinitions {
@Before
public void set_the_stage() {
OnStage.setTheStage(new OnlineCast());
}
@Given("^that (.*) is a London commuter$")
public void a_london_commuter_named(String commuterName) throws Throwable {
theActorCalled(commuterName);
}
@When("^(.*) plans a journey from (.*) to (.*) departing at (.*)$")
public void plan_a_journeys(String name, String departure, String destination,
String departureTime) throws Throwable {
theActorCalled(name).attemptsTo(
PlanAJourney.from(departure).to(destination).departingAt(departureTime)
);
}
172. serenity-bdd.info#SerenityBDD @Wakaleo
Feature: Plan a new journey
Scenario: Plan a new journey
Given that Connie is a London commuter
When she plans a journey from Waterloo to Canary Wharf departing at 09:00
Then she should see that the fastest train departs at 08:59
173. serenity-bdd.info#SerenityBDD @Wakaleo
public class PlanAJourneyStepDefinitions {
@Before
public void set_the_stage() {
OnStage.setTheStage(new OnlineCast());
}
@Given("^that (.*) is a London commuter$")
public void a_london_commuter_named(String commuterName) throws Throwable {…}
@When("^(.*) plans a journey from (.*) to (.*) departing at (.*)$")
public void plan_a_journeys(String name, String departure, String destination,
String departureTime) throws Throwable {…}
@Then("^(.*) should see that the fastest train departs at (.*)$")
public void should_see_departure_time(String name,
String expectedDepartureTime) throws Throwable {
theActorCalled(name).should(
seeThat("the departure time", TheFastest.departureTime(),
is(expectedDepartureTime))
);
175. serenity-bdd.info#SerenityBDD @Wakaleo
Feature: Plan a new journey
Scenario Outline: Plan many a journey
Given that Connie is a London commuter
When she plans a journey from <departure> to <destination> departing at <plannedDepartureTime>
Then she should see a trip on the <line> line departing at <departureTime>
Examples:
| departure | destination | line | plannedDepartureTime | departureTime |
| Waterloo | Canary Wharf | Jubilee line to Canary Wharf | 09:00 | 08:59 |
| London Bridge | Moorgate | Northern line to Moorgate | 10:00 | 09:59 |
176. serenity-bdd.info#SerenityBDD @Wakaleo
Feature: Plan a new journey
Scenario Outline: Plan many a journey
Given that Connie is a London commuter
When she plans a journey from <departure> to <destination> departing at <plannedDepartureTime>
Then she should see a trip on the <line> line departing at <departureTime>
Examples:
| departure | destination | line | plannedDepartureTime | departureTime |
| Waterloo | Canary Wharf | Jubilee line to Canary Wharf | 09:00 | 08:59 |
| London Bridge | Moorgate | Northern line to Moorgate | 10:00 | 09:59 |
177. serenity-bdd.info#SerenityBDD @Wakaleo
@Then("^(.*) should see a trip on the (.*) line departing at (.*)")
public void should_see_trip(String name, String line, String departureTime) {
theActorCalled(name).should(
seeThat("the fastest tube line”, TheFastest.tubeLine(),
is(line)),
seeThat("the departure time", TheFastest.departureTime(),
is(departureTime))
);
}
178. serenity-bdd.info#SerenityBDD @Wakaleo
Scenario: Coordinating journeys
Given that Connie is a London commuter
And that Charles is a London commuter
When Connie plans a journey from Waterloo to Canary Wharf departing at 09:00
And Charles plans a journey from London Bridge to Moorgate departing at 10:00
Then Connie should see a trip on the Jubilee line to Canary Wharf line departing at 08:59
And Charles should see a trip on the Northern line to Moorgate line departing at 09:59
188. serenity-bdd.info#SerenityBDD @Wakaleo
Questions to ask:
- How much time do I spend maintaining the tests?
- Am I modelling user’s interactions with the system,
or how the system is built?
- Can I share elements of my testing framework with others?
- How much duplication and dead code is there in my tests?
- How long would it take me to find the issue if a test fails?
- How long to change the tests when the system changes?
- Can I use my tests to drive the design of the system?
192. serenity-bdd.info#SerenityBDD @Wakaleo
Thank you!
Learn more at serenity-bdd.info
Source code:
How we can help:
Get in touch:
Learn Serenity BDD online
bit.ly/serentiy-planner
johnfergusonsmart.com
reachme@johnfergusonsmart.com
http://serenity-dojo.com