Writing Tests Like Shakespeare

Keep your automated tests readable with the Screenplay pattern

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

Why (not) Structure Tests?

Please put your own reasons in the chat…

readablility

I can read the test and understand what it does

understandability

I understand how the test code works and what it does

extendability

I can add new tests easily (e.g. by reusing code)

changability

I can easily change the details of a test

debugability

I can easily debug a test

report verbosity

I can understand what happened in test test output

reusability

I can reuse parts of the code to write new test faster

maintainability

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

The Challenge

Direct Interaction

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

Page Objects

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

Screenplays

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

More Screenplays

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

Conclusion

readablility

I can read the test and understand what it does

understandability

I understand how the test code works and what it does

extendability

I can add new tests easily (e.g. by reusing code)

changability

I can easily change the details of a test

debugability

I can easily debug a test

report verbosity

I can understand what happened in test test output

reusability

I can reuse parts of the code to write new test faster

maintainability

I can reflect changes in the SUT with similar effort to the test code

Further Reading &
Implementations

Page Objects Refactored – SOLID steps to the Screenplay/Journey Pattern

Writing Tests Like Shakespeare – Keep your automated tests readable with the Screenplay pattern

By Michael Kutz

Writing Tests Like Shakespeare – Keep your automated tests readable with the Screenplay pattern

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.

  • 3,407