Writing Tests Like Shakespeare

Komplexe Tests organsisieren
mit dem Screenplay Pattern

Michael Kutz

linkedin.com/in/micha-kutz

mkutz

stackoverflow.com/users/437621

@MichaKutz

👨‍💻 Arbeitet be REWE digital in Köln als Entwickler/Ambassador

🔌 Mag (Test-) Automation

🚛 mag es CI/CD-Pipelines zu bauen und zu warten

🎤 Redet auf Konferenzen über das alles

🧠 ist fasziniert von kognitiven Verzerrungen

🏃 ist ein passionierter Läufer

Warum Test Code strukturieren?

Lesbarkeit

Ich kann den Code lesen und verstehe was er tut (und warum)

Verständlichkeit

Ich verstehe wie die Code funktioniert

Erweiterbarkeit

Ich kann einfach neue Tests hinzufügen

Änderbarkeit

Ich kann Details schnell und einfach ändern

Analysierbarkeit

Ich kann den Code einfach Debuggen

Nachvollziehbarkeit

Ich kann nachvollziehen, was während der Ausführung passiert ist (Reports/Logs)

Wiederverwendbarkeit

Ich kann Teile des Codes verwenden, um weitere Tests schneller zu schreiben

Wartbarkeit

Änderungen am Testprodukt können durch ähnlichen Aufwand im Code umgesetzt werden

There is nothing either good or bad but thinking makes it so.

Hamlet, Act 2, Scene 2

Die Herausforderung

Direkte Interaktion

interagiert mit

Test

Test 2

interagiert mit

den selben

Elemente

Lokatoren

Interaktionen

Werkzeuge

Zustand

Lokatoren

Interaktionen

Werkzeuge

Zustand

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();
  }
}
Zeilen Test Code Zeilen Code insg. Anzahl Dateien
48 48 1

Weitere Tests werden eine ähnliche Größe und Struktur haben!

You speak an infinite deal of nothing.

The Merchant of Venice, Act 1, Scene 1

Page Objects

PageObject

nutzt

nutzt

zur Interaktion mit

Test

Test 2

Elemente

Lokatoren

Interaktionen

Werkzeuge

Zustand

Werkzeuge

Zustand

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 sind in den 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);
  }
}

Lokator

Interaktion

Lokator

Interaktion

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();
  }
}
Zeilen Test Code Zeilen Code insg. Anzahl Dateien
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 können wiederverw. werden

… müssen aber ggf. auch für andere Tests angepasst werden

O Romeo, Romeo, wherefore art thou Romeo?

Romeo and Juliet, Act 2, Scene 2

Kein Benutzer-Konzept

Screenplays

Actor

Abilities

Elemente

Aktionen

ermöglichen

hat

dirigiert

zur Interaktion mit

Werkzeuge

Zustand

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"));
}

Werkzeug

Zustand

Screenplay

Actor

Abilities

Elemente

Aktionen

ermöglichen

hat

dirigiert

zur Interaktion mit

Tasks

erledigt

bestehend aus

Test

Werkzeuge

Zustand

Lokatoren

Interaktionen

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 können sich über mehrere Seiten erstrecken

Wir können mehr als eine Ability benutzen!

Tasks verbinden Aktionen mit Intentionen

Lokatoren

Interaktionen

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)));
  }
}

Wie können wir den Erfolg verifizieren?

Screenplay

Actor

Abilities

Elemente

Aktionen

ermöglichen

hat

dirigiert

zur Interaktion mit

Tasks

Questions

fragt

erledigt

über den Zustand von

bestehend aus

Test

Werkzeuge

Zustand

Locatoren

Interaktionen

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 benutzen ebenfalls Abilities

Aber haben einen Rückgabe-wert

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();
  }
}
Zeilen Test Code Zeilen Code insg. Anzahl Dateien
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 und Questions sind wiederverw.

Sie dienen jedoch nur einem Zweck

"Tricks" werden immer im Kontext angewendet

Mehr Screenplays

Screenplay

Actor

Abilities

Elemente

Aktionen

ermöglichen

hat

dirigiert

zur Interaktion mit

Tasks

Questions

fragt

erledigt

über den Zustand von

bestehend aus

Test

Werkzeuge

Zustand

Lokatoren

Interaktionen

Facts

ermöglichen

lernt

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();
  }
}

Viele potentielle Duplikate in Testdaten

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 können wiederverw. werden, enthalten Konstanten und Generatoren

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();
  }
}

Wo sind die Daten jetzt?

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();
  }
}

Wie können wir wissen was genau fehlgeschlagen ist?

class RegisterScreenplay {

  Actor romeo = new Actor("Romeo")
      .can(new BrowseTheWeb(
          new LocalWebDriverSupplier(BrowserType.CHROME),
          "https://automationpractice.com"))
      .informs(new Slf4jReporter())
      .learns(defaultEmailAddress)
      .learns(defaultPersonalInformation)
      .learns(defaultAddress);

  @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 und Questions geben bei Fehlern Kontext

Fazit

Lesbarkeit

Ich kann den Code lesen und verstehe was er tut (und warum)

Verständlichkeit

Ich verstehe wie die Code funktioniert

Erweiterbarkeit

Ich kann einfach neue Tests hinzufügen

Änderbarkeit

Ich kann Details schnell und einfach ändern

Analysierbarkeit

Ich kann den Code einfach Debuggen

Nachvollziehbarkeit

Ich kann nachvollziehen, was während der Ausführung passiert ist (Reports/Logs)

Wiederverwendbarkeit

Ich kann Teile des Codes verwenden, um weitere Tests schneller zu schreiben

Wartbarkeit

Änderungen am Testprodukt können durch ähnlichen Aufwand im Code umgesetzt werden

Leseempfehlungen &
Implementierungen

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

Email    michael.kutz@rewe-digital.com

Twitter  @MichaKutz

Fragen?

🇩🇪

🇬🇧

Tests schreiben wie Shakespeare

By Michael Kutz

Tests schreiben wie Shakespeare

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.

  • 200