Introduction

Shakespeare

Shakespeare is a Java implementation of Screenplay. It was mainly created to explain the basic concepts of Screenplay and hence is kept rather simple in its core.

Screenplay

A framework helping to write tests like screenplays. It is based on the ideas from Page Objects Refactored by Antony Marcano, Andy Palmer & John Ferguson Smart, with Jan Molak.

Screenplay does not replace any testing framework like JUnit, TestNG, Spock, RSpec, Jasmine etc. In fact, it works with any of these. Screenplay rather structures complex test code in a different way that makes it

  • more readably,

  • more reusable, and

  • more maintainable.

It does that mainly by taking a user-centric and object-oriented view on things.

In screenplay a user is represented by as an Actor. Actors can have Abilities. These enable them to do Tasks or answer Questions.

Diagram
Figure 1. Screenplay Core Concepts

In screenplay each of these concepts—​Actor, Ability, Task and Question—​are objects which abstract from the actual actions.

Actions can be calling an API, clicking a link, fetching an email, waiting for something to appear. All these tiny noisy details are hidden within the Task or Question objects.

Getting Started

Requirements

In oder to use Shakespeare you need a JDK 11 or higher.

Dependencies

To use Shakespeare in your own project, you need to add it as a dependency to your project.

Gradle
implementation 'org.shakespeareframework:core:0.7.1'
Maven
<dependency>
  <groupId>org.shakespeareframework</groupId>
  <artifactId>core</artifactId>
  <version>0.7.1</version>
</dependency>
The core module is a transient dependency of any other module. Hence, you don’t need to keep it once you add another module (e.g. selenium or retrofit)

Bill of Materials

If you want to use more than one module in the same project, you can use Shakespeare’s bill of materials (BOM) and omit the explicit version for the other modules.

Gradle
implementation platform('org.shakespeareframework:bom:0.7.1')
implementation 'org.shakespeareframework:selenium'
implementation 'org.shakespeareframework:retrofit'
Maven
<project>
  <!--…-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.shakespeareframework</groupId>
        <artifactId>bom</artifactId>
        <version>0.7.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <!-- … -->
  <dependencies>
    <dependency>
      <groupId>org.shakespeareframework</groupId>
      <artifactId>selenium</artifactId>
    </dependency>
    <dependency>
      <groupId>org.shakespeareframework</groupId>
      <artifactId>retrofit</artifactId>
    </dependency>
  </dependencies>
  <!-- … -->
</project>

Actors with Abilities

Actors

The central object in a Screenplay and Shakespeare is the Actor. Actors represent something or someone that interacts with the system under test.

There can be any number of Actors in a Screenplay (Test). To make different Actors easily identifiable, they have a name. The name can be explicitly given or will be randomly assigned.

Actor with explicitly given name
var robin = new Actor("Robin");
Actor with random name
var user = new Actor();

Abilities

Actors may use Abilities to interact with the system under test.

Ability is a simple marker interface. Implementations should provide methods providing an object which allows interaction with the system under test. If that object is being generated on demand or simply wrapped by the Ability is up to the implementation.

A simple example for an Ability implementation
class Log implements Ability {

  private final Logger logger;

  Log(String name) {
    this.logger = Logger.getLogger(name);
  }

  public Logger getLogger() {
    return logger;
  }
}

To allow Actors to use an Ability it needs to be given to them via the can(Ability) method.

Give an Ability to an Actor
var anna = new Actor("Anna").can(new Log("Anna"));

To get an Ability from an Actor, there’s the uses(Class<? extends Ability>) method.

Get an Ability from an Actor
var log = anna.uses(Log.class);
log.getLogger().info("Hello World");

Doing Tasks

Tasks

Tasks can be done by an Actor. What the actor should do is defined in the performAs(Actor) method of the Task.

As the Actor is given as an argument, the Task implementation has access to the Actor’s Abilities via the uses(Class<? extends Ability>) method.

SayHelloWorld Task
class SayHelloWorld implements Task {

  @Override
  public void performAs(Actor actor) {
    var logger = actor.uses(Log.class).getLogger();
    logger.info("Hello World");
  }
}
Doing SayHelloWorld Task
tom.does(new SayHelloWorld());

Tasks with Parameters

Some tasks require data to be executed. E.g. a SearchFor Task probably needs a query string, or an AddToCart Task might need a product identifier.

Such data can be added as properties of the Task.

SayHello Parameterized Task
class SayHello implements Task {

  private final String to;

  public SayHello(String to) {
    this.to = to;
  }

  @Override
  public void performAs(Actor actor) {
    var logger = actor.uses(Log.class).getLogger();
    logger.info(String.format("Hello %s", to));
  }
}

As Tasks should not change over time and should be reusable, it is also possible to declare them as Java Records—​given that you can use Java 17+ features.

Doing the Tasks is the same for Classes and Records.

SayHelloAsRecord Parameterized Task
record SayHelloAsRecord(String to) implements Task {

  @Override
  public void performAs(Actor actor) {
    var logger = actor.uses(Log.class).getLogger();
    logger.info(String.format("Hello %s", to));
  }
}
Doing SayHello Parameterized Task
alex.does(new SayHello("Shakespeare"));
alex.does(new SayHelloAsRecord("Shakespeare"));

Lambdas as Tasks

As Tasks only have one abstract method, they can be declared using Java’s Lambda syntax.

Simple parameterized Task
mila.does(she -> she.uses(Log.class).getLogger().info("Hi"));
A Task should group complex interactions into a semantic unit of work. So declaring them as Lambdas might be useful for short-lived experiments or debugging, but is not generally advisable.

Abstraction Levels

Tasks are a powerful tool. A task can be as simple as clicking on a link and as complex as a double-opt-in registration process. Finding the right abstraction can be hard. This chapter contains some examples how to find the right level of abstraction for Tasks.

Grouping Interactions

The simplest kind of Tasks are those who group simple interactions with the system under test.

Let’s take a Task to log in to a website. This might consist of the following interactions with the website:

LoginTask
class Login implements Task {

  private final String username;
  private final String password;

  Login(String username, String password) {
    this.username = username;
    this.password = password;
  }

  @Override
  public void performAs(Actor actor) {
    var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();

    webDriver.get(mockWebServer.url("/").toString()); (1)

    webDriver
        .findElement(By.name("username")) (2)
        .sendKeys(username); (3)
    webDriver
        .findElement(By.name("password")) (4)
        .sendKeys(password); (5)
    webDriver
        .findElement(By.cssSelector(".login button")) (6)
        .click(); (7)
  }

  @Override
  public String toString() {
    return String.format("login as %s", username);
  }
}
1 Navigate to the website,
2 find the "username" input field,
3 write "john" into it,
4 find the "password" input field,
5 write "demo" into it,
6 find the login button,
7 click on it.

As you can see all the interactions are wrapped in the Task object. It has fields for the username and the password, so we can easily reuse the Task with other credentials.

Grouping Tasks

As the Actor is fully injected into the performAs method, it is also possible to do other tasks in a Task.

Let’s take a shop website as an example. In order to check out, there are several complex steps needed:

  1. Add any product to the shopping cart,

  2. navigate to the checkout page,

  3. enter a delivery address,

  4. enter valid payment details,

  5. confirm the checkout.

All these steps could be Tasks in their own right, but if we want to test that the user is able to cancel the order after check out, we might want to bundle the steps into something like a Checkout Task.

It is generally a bad idea to mix simple interactions with sub-tasks, as it makes finding problems much harder!

Checking Questions

Questions

Questions are pretty similar to Tasks, but may return an answer.

They can be checked (answered) by an Actor. How the Actor finds the answer to a Question is defined in the answerAs(Actor) method of the Question.

Similar to the performAs(Actor) method of the Task, the Actor is given as an argument, so a Question can use all the Actor’s Abilities to find the answer.

LoggedInState Question
class LoggedInState implements Question<Boolean> {

  @Override
  public Boolean answerAs(Actor actor) {
    var webDriver = actor.uses(BrowseTheWeb.class).getWebDriver();

    return !webDriver.findElements(By.linkText("Log Out")).isEmpty();
  }

  @Override
  public String toString() {
    return "login state";
  }
}

While it is possible to put complex interactions in Questions it is generally advisable to keep them simple and independent.

Good Questions

Undoubtedly good questions do not change the state of the system under test in any way

  • Checking a web element is present,

  • performing a GET call on an HTTP API and return its body,

  • fetching email and retuning its content.

Ugly Questions

Doubtfully ugly questions don’t change the persisted state of the system under test, but change the local state. For example the current displayed view. Using such questions requires to check the state in following Tasks or Questions.

  • Navigate to the user’s profile page to check for data that is only visible there,

  • checking available products by switching to the search results page.

Especially in web applications this type of Questions are often unavoidable, but should be kept to a minimum.

Bad Questions

Changing the persistent state of the system under test should genrally be avoided and be left to Tasks.

  • Sending data to a backend API,

  • login to a web application,

  • deleting email.

Retryables

Some things take time. E.g. after creating an account, it might take some time for a confirmation email to arrive.

Shakespeare provides a generic way to wait for things to succeed: Retryables.

They can be used just like regular Tasks and Questions, but they additionally have a defined timeout after which an Actor will give up and an interval in which an Actor will retry. The default timeout is 2 seconds, the default interval is the 10th of the timeout.

Shakespeare’s Retryables are meant for things that a human user would need to retry as well. To wait for UI elements to show up, there are better usually more efficient ways available (e.g. in Selenium, Cypress etc.).

Retryable Tasks

A RetryableTask also has a set of acknowledged exceptions. By default, an Actor will ignore all Exceptions thrown in retries, but will immediately fail if it is an instance of an acknowledged Exception.

E.g. you might expect a NotFoundException but not a AuthorizationFailedException. So you can just add the latter to the acknowledgedExceptions.

Retryable Questions

A RetryableQuestion has a set of ignored Exceptions and an acceptable method. While any Exception thrown that is not in the set of ignored exceptions will break the retrying loop, the acceptable method will be called for each yielded answer. Only if the acceptable method returns true, the retrying will stop and return the (accepted) answer.

There is a default implementation of acceptable, which will a number of possible answers:

  1. Optionals that are present,

  2. non-empty Collections, Maps or Arrays,

  3. any Boolean answer, and

  4. anything that not null.

Checked in that order.

This default implementation might suit your needs already, but you can always override it with your own logic.

Learning & Remembering Facts

Facts are a way to add data to the Actor. After a Fact was learned, it can always be remembered by its class and be used to do Tasks or answer Questions.

Learn and remember Facts
kate.learns(new PhoneNumber("+49 0180 4 100 100"));

PhoneNumber katesPhoneNumber = kate.remembers(PhoneNumber.class);

Fact classes only need to implement the (empty) Fact marker interface. Apart from that they can be basically anything but should be immutable value objects or records.

PhoneNumber Fact
record PhoneNumber(String number) implements Fact {}

Relearning Facts

Note that an Actor cannot remember multiple instances of a Fact. When you call remember with two different instances of the same Fact class, the older one is being forgotten and replaced by the new one.

Learn and remember Facts
kate.learns(new PhoneNumber("+49 0180 4 100 100"));

kate.learns(new PhoneNumber("+1 (303) 499-7111"));

PhoneNumber katesNewPhoneNumber = kate.remembers(PhoneNumber.class);
assertThat(katesNewPhoneNumber).isEqualTo(new PhoneNumber("+1 (303) 499-7111"));

Poly Facts

One way to achieve something similar are Poly Facts: Simply Facts with multiple fields.

E.g. a PhoneNumbers Fact might contain a field for a work phone number and one for a home phone number:

PhoneNumbers Poly Fact
record PhoneNumbers(String home, String work) implements Fact {}
Learn and remember Poly Facts
robin.learns(new PhoneNumbers("+49 0180 4 100 100", "+1 (303) 499-7111"));

var robinsPhoneNumbers = robin.remembers(PhoneNumbers.class);
var robinsHomePhoneNumber = robinsPhoneNumbers.home();
var robinsWorkPhoneNumber = robinsPhoneNumbers.work();

Facts with Defaults and Generators

One great advantage of Fact classes is the possibility of adding static fields and methods.

E.g. you can put that one valid test credit card number in a public static CreditCard DEFAULT in your CreditCard Fact class:

CreditCard Fact with default
record CreditCard(String type, String number, YearMonth expiration) implements Fact {
  public static CreditCard DEFAULT_VISA =
      new CreditCard("visa", "4111111111111111", YearMonth.of(2026, 10));
}

Or you can add a method to generate valid unique random email addresses to your EmailAddress Fact class:

EmailAddress with random generator
record EmailAddress(String address) implements Fact {
  public static EmailAddress random() {
    return new EmailAddress("%s@shakespeareframework.org".formatted(UUID.randomUUID()));
  }
}

Personalized Tasks/Questions

Facts can be used to provide an Actor with necessary information for certain Tasks and Questions instead of using Tasks with Parameters.

By doing so, we can instruct an Actors to do something with their memory as context.

For example, to create an account we usually require a lot of personal information. To human testers, I can say "go create an account", and they will use their own data to do that. Using Facts we can achieve the same feel of a personal context.

In that case, an Actors learn their facts in the setup phase of a test.

Using Facts in your Tasks and Questions can be a really nice experience. You don’t need to keep your test data at hand in you test code, but can confine it in the test’s setup phase. However, by doing so you might end up scrolling back and forth in order to understand a failing test. So you really want to limit this to data that is really owned by an Actor.

Learning by Doing

Another possible use case for Facts is to learn things while doing things. E.g. after finishing a shop’s checkout process it might be useful to remember the displayed order number.

In that case the Facts are learned as a side effect of a Task or a Question.

Don’t overdo this! Side effects are not easy to keep track of,

Reporting

Actors can report actions and their outcome via Reporters that can be added using the link:../javadoc/core/org/shakespeareframework/Actor.html#informs(org.shakespeareframework.reporting.Reporter…​))[informs(Reporter…​) method.

Actors call each of their Reporters in the declared order for the following events:

  • When starting to do a Task or answer a Question,

  • when retrying a RetryableTask or RetryableQuestion,

  • when successfully finishing a Task or successfully answering a Question,

  • when failing to do a Task or answer a Question.

Built-In Reporters

The Slf4jReporter

Uses the Slf4j Logger to print each action of the Actor. Successful actions will be logged as info, failure as warnings. Note that actions are only logged after finishing.

Each action is printed with

  • the Actor’s name,

  • the Task’s/Question’s toString output,

  • the result (success = , failure = , each retry = ),

  • the action’s duration (e.g. 245ms),

  • in case of a failure the causing exception’s simple class name (e.g. ElementNotFoundException),

  • in case of a successful question the given answer (e.g. → answer)

Sub Tasks and Questions are printed as a hierarchic tree.

Slf4jReporter example output
19:45:18.359 [Test worker] WARN Logan - Logan does login ✓ 1s15ms
├── Logan does enter username ✓ 312ms
├── Logan does enter password ✓ 432ms
└── Logan does submit login form ✓ 43ms
    └── Logan does confirm terms and conditions •✗ 245ms ElementNotFoundException
19:45:19.876 [Test worker] INFO Logan - Logan checks login state ✓ 1s15ms → false

Additional Modules

Browser Automation with Selenium

The Selenium Module provides the BrowseTheWeb Ability, which basically provides a managed Selenium WebDriver.

The Ability not only warps a WebDriver, but also manages it using a WebDriverSupplier which comes in different flavors for different scenarios (e.g. local debugging or continuous integration).

Setup

For Local Development

When you write your UI tests locally, it is quite useful (and also satisfying) to be able to watch the automation work in a local browser, checking the browser’s developer and logs. For this Shakespeare comes with the LocalWebDriverSupplier, which relies on a local installation of the browser.

It does take care of downloading the required binary, starting the browser as soon as the Actor actually uses it and closing it after the test case.

Setup for local development
var tim =
    new Actor("Tim").can(new BrowseTheWeb(new LocalWebDriverSupplier(BrowserType.CHROME)));
Using WebDriverManager

The above variant relies on the WebDriverManager internally.

WebDriverManagerWebDriverSupplier allows to use it directly and thereby use all of its features.

Setup with WebDriverManager
var webDriverManager = WebDriverManager.edgedriver().browserInDocker();
var alex =
    new Actor("Alex")
        .can(
            new BrowseTheWeb(
                new WebDriverManagerWebDriverSupplier(webDriverManager, BrowserType.CHROME)));

Base URL

Usually you will be testing one specific web application. In order to reset the browser to the URL of that application, you can give that base URL as an additional parameter to the WebDriverManager. The URL will be automatically called when you call getWebDriver.

Configure a base URL
var browseTheWeb = new BrowseTheWeb(webDriverSupplier, "https://shakespeareframework.org/");

assertThat(browseTheWeb.getWebDriver().getCurrentUrl())
    .isEqualTo("https://shakespeareframework.org/");

Additional Capabilities

All WebDriverSuppliers provide a constructor, which allows configuring additional Capabilities.

Additional Capabilities in a LocalWebDriverSupplier
var cameron =
    new Actor("Cameron")
        .can(
            new BrowseTheWeb(
                new LocalWebDriverSupplier(
                    BrowserType.CHROME, new ChromeOptions().addArguments("--headless"))));

Selenium Reporting

Screenshot Reporting

An actor can be instructed to inform ScreenshotReporter (see Reporting). By default, this will create a screenshot of the current page whenever doing a Task or answering a Question needs to be retried or fails, but it can also be configured to report successes.

The screenshots can then be found at the configured reports path and are named <counter>-<actor>-<retry|failure|success>-<task|question string>.png, where the <task|question string> is a file system friendly version of the Task’s or Question’s toString(). E.g. 001-imogen-success-latest_shakespeare_release_version.png.

Using a ScreenshotReporter
var reportsPath = Path.of("build", "reports", "shakespeare");
var imogen =
    new Actor("Imogen")
        .can(
            new BrowseTheWeb(
                new WebDriverManagerWebDriverSupplier(
                    WebDriverManager.firefoxdriver().browserInDocker(), BrowserType.FIREFOX)))
        .informs(new ScreenshotReporter(reportsPath, true));

imogen.checks(new LatestShakespeareReleaseVersion());

assertThat(reportsPath.resolve("001-imogen-success-latest_shakespeare_release_version.png"))
    .isNotEmptyFile();
HTML Snapshot Reports

Similar to the Screenshot Reporting there’s also a HtmlSnapshotReporter, which captures the current page’s HTML snapshot.

Using a HtmlSnapshotReporter
var reportsPath = Path.of("build", "reports", "shakespeare");
var tim =
    new Actor("Tim")
        .can(
            new BrowseTheWeb(
                new WebDriverManagerWebDriverSupplier(
                    WebDriverManager.firefoxdriver().browserInDocker(), BrowserType.FIREFOX)))
        .informs(new HtmlSnapshotReporter(reportsPath, true));

tim.checks(new LatestShakespeareReleaseVersion());

assertThat(reportsPath.resolve("001-tim-success-latest_shakespeare_release_version.html"))
    .isNotEmptyFile();

HTTP API Testing with Retrofit

The Retrofit Module provides the CallHttpApis Ability, which allows building Retrofit-based HTTP API clients.

Declaring the API

In order to create a client, Retrofit requires an interface, which describes the API. For this, please follow the instructions in the Retrofit "API Declaration" documentation chapter.

API declaration example
interface ActorsApi {

  @GET("/actors")
  Call<List<ActorInfo>> getActors();

  @GET("/actors/{id}/name")
  Call<String> getActorName(@Path("id") String id);
}

Building the Client

Once the API interface is declared, the client can be build.

For Scalar Bodies (Strings, Integers, Floats)
Example for scalar bodies
record ActorName(String id) implements Question<String> {

  @Override
  public String answerAs(Actor actor) {
    var actorsApiClient =
        actor
            .uses(CallHttpApis.class)
            .buildClient() (1)
            .baseUrl(ACTORS_SERVICE_URL) (2)
            .addScalarsConverterFactory() (3)
            .build(ActorsApi.class); (4)
    try {
      return actorsApiClient.getActorName(id).execute().body();
    } catch (IOException e) {
      throw new RuntimeException("%s failed".formatted(this), e);
    }
  }
}
1 The getClient method returns a CallHttpApis.Builder,
2 configure the base URL of the service—​it will be put in front of the relative URLs in the API annotations,
3 add needed ConverterFactory to convert response/request bodies,
4 build the client for the given API declaration.
For JSON Bodies
Example for JSON bodies
record AllActors() implements Question<List<ActorInfo>> {

  @Override
  public List<ActorInfo> answerAs(Actor actor) {
    var actorsApiClient =
        actor
            .uses(CallHttpApis.class)
            .buildClient()
            .baseUrl(ACTORS_SERVICE_URL)
            .addJacksonConverterFactory() (1)
            .build(ActorsApi.class);
    try {
      return actorsApiClient.getActors().execute().body();
    } catch (IOException e) {
      throw new RuntimeException("%s failed".formatted(this), e);
    }
  }
}

record ActorInfo(String id, String name) {} (2)
1 By adding the JacksonConverterFactory, the client will be able to parse the JSON body
2 into an instance of ActorInfo.
Other Converters

Retrofit provides a number of other converters.

Shakespeare currently ships only with Jackson and Scalars, but you can add other ones. To do so, add the required dependency to your project, and use the generic addConverterFactory method to add it to your client.

Further Reading