Michael Kutz
Quality Engineer at REWE digital, Conference Speaker about QA & Agile, Founder of Agile QA Cologne meetup, Freelance QA Consultant
Michael Kutz
linkedin.com/in/micha-kutz
mkutz
stackoverflow.com/users/437621
@MichaKutz
π¨βπ» Working at REWE digital in Cologne as a
developer β quality engineer
π Likes automating (not only) tests
π Rare defect: likes to create and maintain continuous integration pipelines
π€ Talks at conference all about that
π§ Is fascinated by cognitive biases
π Passionate runner
Please put your own reasons in the chatβ¦
I can read the test and understand what it does
I understand how the test code works and what it does
I can add new tests easily (e.g. by reusing code)
I can easily change the details of a test
I can easily debug a test
I can understand what happened in test test output
I can reuse parts of the code to write new test faster
I can reflect changes in the SUT with similar effort to the test code
There is nothing either good or bad but thinking makes it so.
Hamlet, Act 2, Scene 2
interacts with
Test
Test 2
interacts with
the same
Elements
Locators
Interactions
Tools
State
Locators
Interactions
Tools
State
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
webDriver.findElement(By.linkText("Sign in")).click();
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
webDriver.findElement(By.linkText("Sign in")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(By.id("email_create")))
.sendKeys(emailAddress);
webDriver.findElement(By.name("SubmitCreate")).click();
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
webDriver.findElement(By.linkText("Sign in")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(By.id("email_create")))
.sendKeys(emailAddress);
webDriver.findElement(By.name("SubmitCreate")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(6))
.until(elementToBeClickable(By.name("id_gender1")))
.click();
webDriver.findElement(By.name("customer_firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("customer_lastname")).sendKeys("Montague");
webDriver.findElement(By.name("passwd")).sendKeys("jul13t");
new Select(webDriver.findElement(By.name("days")))
.selectByValue("14");
new Select(webDriver.findElement(By.name("months")))
.selectByValue("2");
new Select(webDriver.findElement(By.name("years")))
.selectByValue("1999");
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
webDriver.findElement(By.linkText("Sign in")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(By.id("email_create")))
.sendKeys(emailAddress);
webDriver.findElement(By.name("SubmitCreate")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(6))
.until(elementToBeClickable(By.name("id_gender1")))
.click();
webDriver.findElement(By.name("customer_firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("customer_lastname")).sendKeys("Montague");
webDriver.findElement(By.name("passwd")).sendKeys("jul13t");
new Select(webDriver.findElement(By.name("days")))
.selectByValue("14");
new Select(webDriver.findElement(By.name("months")))
.selectByValue("2");
new Select(webDriver.findElement(By.name("years")))
.selectByValue("1999");
webDriver.findElement(By.name("firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("lastname")).sendKeys("Montague");
webDriver.findElement(By.name("company")).sendKeys("Montague Ltd.");
webDriver.findElement(By.name("address1")).sendKeys("515 W Verona Ave");
webDriver.findElement(By.name("address2")).sendKeys("");
webDriver.findElement(By.name("city")).sendKeys("Verona");
new Select(webDriver.findElement(By.name("id_state"))).selectByVisibleText("Wisconsin");
webDriver.findElement(By.name("postcode")).sendKeys("53593");
webDriver.findElement(By.name("phone")).sendKeys("(202) 762-1401");
webDriver.findElement(By.name("phone_mobile")).sendKeys("");
webDriver.findElement(By.name("alias")).sendKeys("Home");
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
webDriver.findElement(By.linkText("Sign in")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(By.id("email_create")))
.sendKeys(emailAddress);
webDriver.findElement(By.name("SubmitCreate")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(6))
.until(elementToBeClickable(By.name("id_gender1")))
.click();
webDriver.findElement(By.name("customer_firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("customer_lastname")).sendKeys("Montague");
webDriver.findElement(By.name("passwd")).sendKeys("jul13t");
new Select(webDriver.findElement(By.name("days")))
.selectByValue("14");
new Select(webDriver.findElement(By.name("months")))
.selectByValue("2");
new Select(webDriver.findElement(By.name("years")))
.selectByValue("1999");
webDriver.findElement(By.name("firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("lastname")).sendKeys("Montague");
webDriver.findElement(By.name("company")).sendKeys("Montague Ltd.");
webDriver.findElement(By.name("address1")).sendKeys("515 W Verona Ave");
webDriver.findElement(By.name("address2")).sendKeys("");
webDriver.findElement(By.name("city")).sendKeys("Verona");
new Select(webDriver.findElement(By.name("id_state"))).selectByVisibleText("Wisconsin");
webDriver.findElement(By.name("postcode")).sendKeys("53593");
webDriver.findElement(By.name("phone")).sendKeys("(202) 762-1401");
webDriver.findElement(By.name("phone_mobile")).sendKeys("");
webDriver.findElement(By.name("alias")).sendKeys("Home");
webDriver.findElement(By.name("submitAccount")).click();
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
webDriver.findElement(By.linkText("Sign in")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(By.id("email_create")))
.sendKeys(emailAddress);
webDriver.findElement(By.name("SubmitCreate")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(6))
.until(elementToBeClickable(By.name("id_gender1")))
.click();
webDriver.findElement(By.name("customer_firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("customer_lastname")).sendKeys("Montague");
webDriver.findElement(By.name("passwd")).sendKeys("jul13t");
new Select(webDriver.findElement(By.name("days")))
.selectByValue("14");
new Select(webDriver.findElement(By.name("months")))
.selectByValue("2");
new Select(webDriver.findElement(By.name("years")))
.selectByValue("1999");
webDriver.findElement(By.name("firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("lastname")).sendKeys("Montague");
webDriver.findElement(By.name("company")).sendKeys("Montague Ltd.");
webDriver.findElement(By.name("address1")).sendKeys("515 W Verona Ave");
webDriver.findElement(By.name("address2")).sendKeys("");
webDriver.findElement(By.name("city")).sendKeys("Verona");
new Select(webDriver.findElement(By.name("id_state"))).selectByVisibleText("Wisconsin");
webDriver.findElement(By.name("postcode")).sendKeys("53593");
webDriver.findElement(By.name("phone")).sendKeys("(202) 762-1401");
webDriver.findElement(By.name("phone_mobile")).sendKeys("");
webDriver.findElement(By.name("alias")).sendKeys("Home");
webDriver.findElement(By.name("submitAccount")).click();
assertThat(webDriver.findElements(By.className("alert"))).isEmpty();
assertThat(webDriver.findElements(By.linkText("Sign in"))).isEmpty();
}
}
Tool
State
Interaction
Locator
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
webDriver.findElement(By.linkText("Sign in")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(By.id("email_create")))
.sendKeys(emailAddress);
webDriver.findElement(By.name("SubmitCreate")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(6))
.until(elementToBeClickable(By.name("id_gender1")))
.click();
webDriver.findElement(By.name("customer_firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("customer_lastname")).sendKeys("Montague");
webDriver.findElement(By.name("passwd")).sendKeys("jul13t");
new Select(webDriver.findElement(By.name("days")))
.selectByValue("14");
new Select(webDriver.findElement(By.name("months")))
.selectByValue("2");
new Select(webDriver.findElement(By.name("years")))
.selectByValue("1999");
webDriver.findElement(By.name("firstname")).sendKeys("Romeo");
webDriver.findElement(By.name("lastname")).sendKeys("Montague");
webDriver.findElement(By.name("company")).sendKeys("Montague Ltd.");
webDriver.findElement(By.name("address1")).sendKeys("515 W Verona Ave");
webDriver.findElement(By.name("address2")).sendKeys("");
webDriver.findElement(By.name("city")).sendKeys("Verona");
new Select(webDriver.findElement(By.name("id_state"))).selectByVisibleText("Wisconsin");
webDriver.findElement(By.name("postcode")).sendKeys("53593");
webDriver.findElement(By.name("phone")).sendKeys("(202) 762-1401");
webDriver.findElement(By.name("phone_mobile")).sendKeys("");
webDriver.findElement(By.name("alias")).sendKeys("Home");
webDriver.findElement(By.name("submitAccount")).click();
assertThat(webDriver.findElements(By.className("alert"))).isEmpty();
assertThat(webDriver.findElements(By.linkText("Sign in"))).isEmpty();
}
}
Lines of Test Code | Total Lines of Code | Number of Files |
---|---|---|
48 | 48 | 1 |
Other tests will have a similar size and structure
You speak an infinite deal of nothing.
The Merchant of Venice, Act 1, Scene 1
PageObject
uses
uses
to interact with
Test
Test 2
Elements
Locators
Interactions
Tools
State
Tools
State
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
var homePage = new HomePage(webDriver);
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
var homePage = new HomePage(webDriver);
var signInPage = homePage.goToSignInPage();
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
var homePage = new HomePage(webDriver);
var signInPage = homePage.goToSignInPage();
var registerPage = signInPage.startRegistration(emailAddress);
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
var homePage = new HomePage(webDriver);
var signInPage = homePage.goToSignInPage();
var registerPage = signInPage.startRegistration(emailAddress);
registerPage.enterPersonalInformation(
"MR", "Romeo", "Montague", "jul13t",
LocalDate.of(1999, 2, 14));
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
var homePage = new HomePage(webDriver);
var signInPage = homePage.goToSignInPage();
var registerPage = signInPage.startRegistration(emailAddress);
registerPage.enterPersonalInformation(
"MR", "Romeo", "Montague", "jul13t",
LocalDate.of(1999, 2, 14));
registerPage.enterAddress(
"Romeo", "Montague",
"Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home");
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
var homePage = new HomePage(webDriver);
var signInPage = homePage.goToSignInPage();
var registerPage = signInPage.startRegistration(emailAddress);
registerPage.enterPersonalInformation(
"MR", "Romeo", "Montague", "jul13t",
LocalDate.of(1999, 2, 14));
registerPage.enterAddress(
"Romeo", "Montague",
"Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home");
registerPage.submit();
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
webDriver.get("https://automationpractice.com");
var homePage = new HomePage(webDriver);
var signInPage = homePage.goToSignInPage();
var registerPage = signInPage.startRegistration(emailAddress);
registerPage.enterPersonalInformation(
"MR", "Romeo", "Montague", "jul13t",
LocalDate.of(1999, 2, 14));
registerPage.enterAddress(
"Romeo", "Montague",
"Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home");
registerPage.submit();
assertThat(registerPage.getErrors()).isEmpty();
assertThat(homePage.isLoggedIn()).isTrue();
}
}
Details are in Page Objects
public record SignInPage(WebDriver webDriver) {
private static final By registerEmailInput = By.id("email_create");
private static final By registerButton = By.name("SubmitCreate");
public SignInPage {
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(registerEmailInput));
}
RegisterPage startRegistration(String emailAddress) {
webDriver.findElement(registerEmailInput).sendKeys(emailAddress);
webDriver.findElement(registerButton).click();
return new RegisterPage(webDriver);
}
}
Locator
Interaction
Locator
Interaction
public record HomePage(WebDriver webDriver) {
private static final By signInLink = By.linkText("Sign in");
SignInPage goToSignInPage() {
webDriver.findElement(signInLink).click();
return new SignInPage(webDriver);
}
boolean isLoggedIn() {
return webDriver.findElements(signInLink).isEmpty();
}
}
class RegisterTest {
WebDriver webDriver = WebDriverManager.chromedriver().create();
@Test
void register() {
webDriver.get("https://automationpractice.com");
var homePage = new HomePage(webDriver);
var signInPage = homePage.goToSignInPage();
var emailAddress =
"romeo-%s@shakespeareframework.org".formatted(randomUUID().getMostSignificantBits());
var registerPage = signInPage.startRegistration(emailAddress);
registerPage.enterPersonalInformation(
"MR", "Romeo", "Montague", "jul13t", LocalDate.of(1999, 2, 14));
registerPage.enterAddress(
"Romeo", "Montague",
"Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home");
registerPage.submit();
assertThat(registerPage.getErrors()).isEmpty();
assertThat(homePage.isLoggedIn()).isTrue();
}
}
Lines of Test Code | Total Lines of Code | Number of Files |
---|---|---|
28 (-20) | 130 (+82) | 4 (+3) |
public record SignInPage(WebDriver webDriver) {
private static final By registerEmailInput = By.id("email_create");
private static final By registerButton = By.name("SubmitCreate");
public SignInPage {
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(registerEmailInput));
}
RegisterPage startRegistration(String emailAddress) {
webDriver.findElement(registerEmailInput).sendKeys(emailAddress);
webDriver.findElement(registerButton).click();
return new RegisterPage(webDriver);
}
}
public record RegisterPage(WebDriver webDriver) {
private static final Map<String, By> titleRadioButtons = Map.of(
"MR", By.id("id_gender1"),
"MRS", By.id("id_gender2"));
private static final By customerFirstNameInput = By.name("customer_firstname");
private static final By customerLastNameInput = By.name("customer_lastname");
private static final By passwordInput = By.name("passwd");
private static final By dateOfBirthDaySelect = By.name("days");
private static final By dateOfBirthMonthSelect = By.name("months");
private static final By dateOfBirthYearSelect = By.name("years");
private static final By errors = By.className("alert");
private static final By addressFirstNameInput = By.name("firstname");
private static final By addressLastNameInput = By.name("lastname");
private static final By companyInput = By.name("company");
private static final By addressLine1Input = By.name("address1");
private static final By addressLine2Input = By.name("address2");
private static final By cityInput = By.name("city");
private static final By stateSelect = By.name("id_state");
private static final By zipCodeInput = By.name("postcode");
private static final By homePhoneInput = By.name("phone");
private static final By mobilePhoneInput = By.name("phone_mobile");
private static final By addressAliasInput = By.name("alias");
private static final By submitButton = By.name("submitAccount");
public RegisterPage {
new WebDriverWait(webDriver, Duration.ofSeconds(6))
.until(ExpectedConditions.elementToBeClickable(customerFirstNameInput));
}
void enterPersonalInformation(
String title,
String firstName, String lastName,
String password,
LocalDate dateOfBirth) {
webDriver.findElement(titleRadioButtons.get(title)).click();
webDriver.findElement(customerFirstNameInput).sendKeys(firstName);
webDriver.findElement(customerLastNameInput).sendKeys(lastName);
webDriver.findElement(passwordInput).sendKeys(password);
new Select(webDriver.findElement(dateOfBirthDaySelect))
.selectByValue(Integer.toString(dateOfBirth.getDayOfMonth()));
new Select(webDriver.findElement(dateOfBirthMonthSelect))
.selectByValue(Integer.toString(dateOfBirth.getMonthValue()));
new Select(webDriver.findElement(dateOfBirthYearSelect))
.selectByValue(Integer.toString(dateOfBirth.getYear()));
}
void enterAddress(String firstName, String lastName, String company,
String addressLine1, String addressLine2,
String city, String state, String zipCode,
String homePhone, String mobilePhone,
String addressAlias) {
webDriver.findElement(addressFirstNameInput).sendKeys(firstName);
webDriver.findElement(addressLastNameInput).sendKeys(lastName);
webDriver.findElement(companyInput).sendKeys(company);
webDriver.findElement(addressLine1Input).sendKeys(addressLine1);
webDriver.findElement(addressLine2Input).sendKeys(addressLine2);
webDriver.findElement(cityInput).sendKeys(city);
new Select(webDriver.findElement(stateSelect)).selectByVisibleText(state);
webDriver.findElement(zipCodeInput).sendKeys(zipCode);
webDriver.findElement(homePhoneInput).sendKeys(homePhone);
webDriver.findElement(mobilePhoneInput).sendKeys(mobilePhone);
webDriver.findElement(addressAliasInput).sendKeys(addressAlias);
}
void submit() {
webDriver.findElement(submitButton).click();
}
public List<String> getErrors() {
return webDriver.findElements(errors).stream().map(WebElement::getText).toList();
}
}
Page Objects may be reused by other tests
β¦ but also need to be changed for these
O Romeo, Romeo, wherefore art thou Romeo?
Romeo and Juliet, Act 2, Scene 2
No notion of a user identity or role
Actor
Abilities
Elements
Actions
enable
has
directs
interact with
Tools
State
Screenplay
Test
class RegisterScreenplay {
Actor romeo = new Actor("Romeo");
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
}
Tools
State
Screenplay
Actor
Abilities
Elements
Actions
enable
has
directs
interact with
Tasks
performs
made up of
Test
Tools
State
Locators
Interactions
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress));
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
}
}
record StartRegistration(String emailAddress) implements Task {
@Override
public void performAs(Actor actor) {
var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();
}
}
record StartRegistration(String emailAddress) implements Task {
@Override
public void performAs(Actor actor) {
var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();
webDriver.findElement(By.linkText("Sign in")).click();
}
}
record StartRegistration(String emailAddress) implements Task {
@Override
public void performAs(Actor actor) {
var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();
webDriver.findElement(By.linkText("Sign in")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(By.id("email_create")))
.sendKeys(emailAddress);
webDriver.findElement(By.name("SubmitCreate")).click();
}
}
Tasks can span multiple pages
We could use multiple Abilities
Tasks tie interactions to an intention
Locators
Interactions
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"));
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)));
}
}
How do we confirm success?
Screenplay
Actor
Abilities
Elements
Actions
enable
has
directs
interact with
Tasks
Questions
asks
performs
about the state of
made up of
Test
Tools
State
Locators
Interactions
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
}
}
public record LoginStatus() implements Question<Boolean> {
@Override
public Boolean answerAs(Actor actor) {
var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();
return webDriver.findElements(By.linkText("Sign in")).isEmpty();
}
}
Questions use Abilities as well
But return some info about the app
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
Lines of Test Code | Total Lines of Code | Number of Files |
---|---|---|
28 (Β±0) | 108 (-22) | 5 (+1) |
public record LoginStatus() implements Question<Boolean> {
@Override
public Boolean answerAs(Actor actor) {
var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();
return webDriver.findElements(By.linkText("Sign in")).isEmpty();
}
}
record StartRegistration(String emailAddress) implements Task {
@Override
public void performAs(Actor actor) {
var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();
webDriver.findElement(By.linkText("Sign in")).click();
new WebDriverWait(webDriver, Duration.ofSeconds(5))
.until(elementToBeClickable(By.id("email_create")))
.sendKeys(emailAddress);
webDriver.findElement(By.name("SubmitCreate")).click();
}
}
record EnterPersonalInformation(
String title, String firstName, String lastName, String password, LocalDate dateOfBirth)
implements Task {
@Override
public void performAs(Actor actor) {
var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();
new WebDriverWait(webDriver, Duration.ofSeconds(6))
.until(
elementToBeClickable(
switch (title) {
case "MR" -> By.id("id_gender1");
case "MRS" -> By.id("id_gender2");
default -> throw new IllegalArgumentException();
}))
.click();
webDriver.findElement(By.name("customer_firstname")).sendKeys(firstName);
webDriver.findElement(By.name("customer_lastname")).sendKeys(lastName);
webDriver.findElement(By.name("passwd")).sendKeys(password);
new Select(webDriver.findElement(By.name("days")))
.selectByValue(Integer.toString(dateOfBirth.getDayOfMonth()));
new Select(webDriver.findElement(By.name("months")))
.selectByValue(Integer.toString(dateOfBirth.getMonthValue()));
new Select(webDriver.findElement(By.name("years")))
.selectByValue(Integer.toString(dateOfBirth.getYear()));
}
}
record EnterAddress(
String firstName,
String lastName,
String company,
String addressLine1,
String addressLine2,
String city,
String state,
String zipCode,
String homePhone,
String mobilePhone,
String addressAlias)
implements Task {
@Override
public void performAs(Actor actor) {
var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();
webDriver.findElement(By.name("firstname")).sendKeys(firstName);
webDriver.findElement(By.name("lastname")).sendKeys(lastName);
webDriver.findElement(By.name("company")).sendKeys(company);
webDriver.findElement(By.name("address1")).sendKeys(addressLine1);
webDriver.findElement(By.name("address2")).sendKeys(addressLine2);
webDriver.findElement(By.name("city")).sendKeys(city);
new Select(webDriver.findElement(By.name("id_state"))).selectByVisibleText(state);
webDriver.findElement(By.name("postcode")).sendKeys(zipCode);
webDriver.findElement(By.name("phone")).sendKeys(homePhone);
webDriver.findElement(By.name("phone_mobile")).sendKeys(mobilePhone);
webDriver.findElement(By.name("alias")).sendKeys(addressAlias);
}
}
Men of few words are the best men.
Henry V, Act 3, Scene 2
Tasks and Questions are reusable
They serve only one purpose, though
"Tricks" are always applied in context
Screenplay
Actor
Abilities
Elements
Actions
enable
has
directs
interact with
Tasks
Questions
asks
performs
about the state of
made up of
Test
Tools
State
Locators
Interactions
Facts
enable
learns
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
A lot of potential duplication in data
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
var emailAddress = "romeo@shakespeareframework.org";
romeo
.does(new StartRegistration(emailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
public record EmailAddress(
String address
) implements Fact {
public static EmailAddress defaultEmailAddress =
new EmailAddress("romeo@shakespeareframework.org");
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
romeo
.does(new StartRegistration(defaultEmailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
romeo
.does(new StartRegistration(defaultEmailAddress))
.does(new EnterPersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14)))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
public record PersonalInformation(
String title,
String firstName,
String lastName,
String password,
LocalDate dateOfBirth
) implements Fact {
public static final PersonalInformation defaultPersonalInformation =
new PersonalInformation(
"MR", "Romeo", "Montague",
"jul13t", LocalDate.of(1999, 2, 14));
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
romeo
.does(new StartRegistration(defaultEmailAddress))
.does(new EnterPersonalInformation(
defaultPersonalInformation))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
romeo
.does(new StartRegistration(defaultEmailAddress))
.does(new EnterPersonalInformation(
defaultPersonalInformation))
.does(new EnterAddress(
"Romeo", "Montague", "Montague Ltd.",
"515 W Verona Ave", "",
"Verona", "Wisconsin", "53593",
"(202) 762-1401", "",
"Home"))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
Facts can be reused, contain constants and generators
public record Address(
String firstName,
String lastName,
String company,
String addressLine1,
String addressLine2,
String city,
String state,
String zipCode,
String homePhone,
String mobilePhone,
String addressAlias
) implements Fact {
public static final Address defaultAddress =
new Address(
"Romeo",
"Montague",
"Montague Ltd.",
"515 W Verona Ave",
"",
"Verona",
"Wisconsin",
"53593",
"(202) 762-1401",
"",
"Home");
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
romeo
.does(new StartRegistration(defaultEmailAddress))
.does(new EnterPersonalInformation(
defaultPersonalInformation))
.does(new EnterAddress(
defaultAddress))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"));
@Test
void register() {
romeo
.does(new StartRegistration(defaultEmailAddress))
.does(new EnterPersonalInformation(
defaultPersonalInformation))
.does(new EnterAddress(
defaultAddress))
.does(new SubmitRegisterForm());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
These tasks depend on each other
Let's group them together
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"))
.learns(defaultEmailAddress)
.learns(defaultPersonalInformation)
.learns(defaultAddress);
@Test
void register() {
romeo.does(new Register());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"))
.learns(defaultEmailAddress)
.learns(defaultPersonalInformation)
.learns(defaultAddress);
@Test
void register() {
romeo.does(new Register());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
Where is the data now?
public record Register() implements Task {
@Override
public void performAs(Actor actor) {
actor
.does(new StartRegistration(
actor.remembers(EmailAddress.class)))
.does(new EnterPersonalInformation(
actor.remembers(PersonalInformation.class)))
.does(new EnterAddress(
actor.remembers(Address.class)))
.does(new SubmitRegisterForm());
}
}
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"))
.learns(defaultEmailAddress)
.learns(defaultPersonalInformation)
.learns(defaultAddress);
@Test
void register() {
romeo.does(new Register());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
How do we know what failed?
class RegisterScreenplay {
Actor romeo = new Actor("Romeo")
.can(new BrowseTheWeb(
new LocalWebDriverSupplier(BrowserType.CHROME),
"https://automationpractice.com"))
.learns(defaultEmailAddress)
.learns(defaultPersonalInformation)
.learns(defaultAddress)
.informs(new Slf4jReporter());
@Test
void register() {
romeo.does(new Register());
assertThat(romeo.checks(new LoginStatus()))
.isTrue();
}
}
INFO Romeo does Register[emailAddress=romeo@shakespeareframework.org] β 27s
βββ Romeo does StartRegistration[emailAddress=romeo@shakespeareframework.org] β 320ms
βββ Romeo does EnterPersonalInformation[personalInformation=MR Romeo Montague, born 1999-02-14] β 3s428ms
βββ Romeo does EnterAddress[address=Home
β Romeo Montague Montague Ltd.
β 515 W Verona Ave
β Verona Wisconsin 53593
β (202) 762-1401] β 939ms
βββ Romeo does SubmitRegisterForm[] β 6s
INFO Romeo checks LoginStatus[] β 37ms β true
WARN Romeo does Register[] β 3s787ms NoSuchElementException
βββ Romeo does StartRegistration[emailAddress=romeo@shakespeareframework.org] β 61ms NoSuchElementException
no such element: Unable to locate element: {"method":"link text","selector":"Sign in"}
(Session info: chrome=100.0.4896.127)
Tasks and Questions provide context to failures
I can read the test and understand what it does
I understand how the test code works and what it does
I can add new tests easily (e.g. by reusing code)
I can easily change the details of a test
I can easily debug a test
I can understand what happened in test test output
I can reuse parts of the code to write new test faster
I can reflect changes in the SUT with similar effort to the test code
By Michael Kutz
The screenplay pattern can help you to write complex test code (especially but not only for UIs) in a readable, maintainable and strictly user-centric way using any given language and framework.
Quality Engineer at REWE digital, Conference Speaker about QA & Agile, Founder of Agile QA Cologne meetup, Freelance QA Consultant