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.8.2'
Gradle.kts
implementation("org.shakespeareframework:core:0.8.2")
Maven
<dependency>
  <groupId>org.shakespeareframework</groupId>
  <artifactId>core</artifactId>
  <version>0.8.2</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.8.2')
implementation 'org.shakespeareframework:selenium'
implementation 'org.shakespeareframework:retrofit'
Gradle.kts
implementation(platform("org.shakespeareframework:bom:0.8.2"))
implementation("org.shakespeareframework:selenium")
implementation("org.shakespeareframework:retrofit")
Maven
<project>
  <!--…-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.shakespeareframework</groupId>
        <artifactId>bom</artifactId>
        <version>0.8.2</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.

Java
var robin = new Actor("Robin");
Kotlin
val robin = Actor("Robin")
Java
var user = new Actor();
Kotlin
val user = 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.

Java
class Log implements Ability {

  private final Logger logger;

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

  public Logger getLogger() {
    return logger;
  }
}
Kotlin
data class Log(val name: String, val logger: Logger = Logger.getLogger(name)) : Ability

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

Java
var anna = new Actor("Anna").can(new Log("Anna"));
Kotlin
val anna = Actor("Anna").can(Log("Anna"))

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

Java
var log = anna.uses(Log.class);
log.getLogger().info("Hello World");
Kotlin
val log = anna.uses(Log::class.java)
log.logger.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.

Java
class SayHelloWorld implements Task {

  @Override
  public void performAs(Actor actor) {
    var logger = actor.uses(Log.class).getLogger();
    logger.info("Hello World");
  }
}
Kotlin
class SayHelloWorld() : Task {
  override fun performAs(actor: Actor) {
    val logger = actor.uses(Log::class.java).logger
    logger.info("Hello World")
  }
}
Java
tom.does(new SayHelloWorld());
Kotlin
tom.does(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.

Java
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));
  }
}
Kotlin
data class SayHello(val to: String) : Task {
  override fun performAs(actor: Actor) {
    val logger = actor.uses(Log::class.java).logger
    logger.info("Hello $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.

Java
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));
  }
}
Java
alex.does(new SayHello("Shakespeare"));
alex.does(new SayHelloAsRecord("Shakespeare"));
Kotlin
alex.does(SayHello("Shakespeare"))

Lambdas as Tasks

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

Java
mila.does(she -> she.uses(Log.class).getLogger().info("Hi"));
Kotlin
mila.does { she -> she.uses(Log::class.java).logger.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:

Java
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);
  }
}
Kotlin
data class Login(private val username: String, private val password: String) : Task {

  override fun performAs(actor: Actor) {
    val webDriver = actor.uses(BrowseTheWeb::class.java).webDriver

    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 fun toString(): String {
    return "login as $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.

Java
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";
  }
}
Kotlin
class LoggedInState : Question<Boolean> {

  override fun answerAs(actor: Actor): Boolean {
    val webDriver = actor.uses(BrowseTheWeb::class.java).webDriver
    return webDriver.findElements(By.linkText("Log Out")).isNotEmpty()
  }

  override fun toString(): String {
    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.

Java
kate.learns(new PhoneNumber("+49 0180 4 100 100"));

PhoneNumber katesPhoneNumber = kate.remembers(PhoneNumber.class);
Kotlin
kate.learns(PhoneNumber("+49 0180 4 100 100"))

val katesPhoneNumber = kate.remembers(PhoneNumber::class.java)

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.

Java
record PhoneNumber(String number) implements Fact {}
Kotlin
data class PhoneNumber(val number: String) : 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.

Java
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"));
Kotlin
kate.learns(PhoneNumber("+49 0180 4 100 100"))

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

val katesNewPhoneNumber = kate.remembers(PhoneNumber::class.java)
assertThat(katesNewPhoneNumber).isEqualTo(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:

Java
record PhoneNumbers(String home, String work) implements Fact {}
Kotlin
data class PhoneNumbers(val home: String, val work: String) : Fact
Java
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();
Kotlin
robin.learns(PhoneNumbers(home = "+49 0180 4 100 100", work = "+1 (303) 499-7111"))

val robinsPhoneNumbers = robin.remembers(PhoneNumbers::class.java)
val robinsHomePhoneNumber = robinsPhoneNumbers.home
val 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:

Java
record CreditCard(String type, String number, YearMonth expiration) implements Fact {
  public static CreditCard DEFAULT_VISA =
      new CreditCard("visa", "4111111111111111", YearMonth.of(2026, 10));
}
Kotlin
data class CreditCard(val type: String, val number: String, val expiration: YearMonth) : Fact {
  companion object {
    val DEFAULT_VISA =
      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:

Java
record EmailAddress(String address) implements Fact {
  public static EmailAddress random() {
    return new EmailAddress("%s@shakespeareframework.org".formatted(UUID.randomUUID()));
  }
}
Kotlin
data class EmailAddress(val address: String) : Fact {
  companion object {
    fun random() = EmailAddress("${UUID.randomUUID()}@shakespeareframework.org")
  }
}

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.

Java
var tim =
    new Actor("Tim").can(new BrowseTheWeb(new LocalWebDriverSupplier(BrowserType.CHROME)));
Kotlin
val tim = Actor("Tim").can(BrowseTheWeb(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.

Java
var webDriverManager = WebDriverManager.edgedriver().browserInDocker();
var alex =
    new Actor("Alex")
        .can(
            new BrowseTheWeb(
                new WebDriverManagerWebDriverSupplier(webDriverManager, BrowserType.CHROME)));
Kotlin
val webDriverManager = WebDriverManager.edgedriver().browserInDocker()
val alex = Actor("Alex")
  .can(
    BrowseTheWeb(
      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.

Java
var browseTheWeb = new BrowseTheWeb(webDriverSupplier, "https://shakespeareframework.org/");

assertThat(browseTheWeb.getWebDriver().getCurrentUrl())
    .isEqualTo("https://shakespeareframework.org/");
Kotlin
val browseTheWeb = BrowseTheWeb(webDriverSupplier, "https://shakespeareframework.org/")

assertThat(browseTheWeb.webDriver.currentUrl)
  .isEqualTo("https://shakespeareframework.org/")

Additional Capabilities

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

Java
var cameron =
    new Actor("Cameron")
        .can(
            new BrowseTheWeb(
                new LocalWebDriverSupplier(
                    BrowserType.CHROME, new ChromeOptions().addArguments("--headless"))));
Kotlin
val cameron = Actor("Cameron")
  .can(
    BrowseTheWeb(
      LocalWebDriverSupplier(
        BrowserType.CHROME, 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.

Java
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();
Kotlin
val reportsPath = Path.of("build", "reports", "shakespeare")
val imogen = Actor("Imogen")
  .can(
    BrowseTheWeb(
      WebDriverManagerWebDriverSupplier(
        WebDriverManager.firefoxdriver().browserInDocker(), BrowserType.FIREFOX
      )
    )
  )
  .informs(ScreenshotReporter(reportsPath, true))

imogen.checks(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.

Java
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();
Kotlin
val reportsPath = Path.of("build", "reports", "shakespeare")
val tim = Actor("Tim")
  .can(
    BrowseTheWeb(
      WebDriverManagerWebDriverSupplier(
        WebDriverManager.firefoxdriver().browserInDocker(), BrowserType.FIREFOX
      )
    )
  )
  .informs(HtmlSnapshotReporter(reportsPath, true))

tim.checks(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.

Java
interface ActorsApi {

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

  @GET("/actors/{id}/name")
  Call<String> getActorName(@Path("id") String id);
}
Kotlin
interface ActorsApi {
  @GET("/actors")
  fun getActors(): Call<List<ActorInfo>>

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

Building the Client

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

For Scalar Bodies (Strings, Integers, Floats)
Java
record ActorName(String id) implements Question<String> {

  @Override
  public String answerAs(Actor actor) {
    var actorsApiClient =
        actor
            .uses(CallHttpApis.class)
            .buildClient() (1)
            .baseUrl(serviceUrl) (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);
    }
  }
}
Kotlin
data class ActorName(val id: String) : Question<String> {
  override fun answerAs(actor: Actor): String {
    val actorsApiClient = actor
        .uses(CallHttpApis::class.java)
              .buildClient() (1)
        .baseUrl(serviceUrl) (2)
        .addScalarsConverterFactory() (3)
        .build(ActorsApi::class.java) (4)
    return try {
      actorsApiClient.getActorName(id).execute().body() ?: throw RuntimeException("No body")
    } catch (e: IOException) {
      throw RuntimeException("${this} failed", 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
Java
record AllActors() implements Question<List<ActorInfo>> {

  @Override
  public List<ActorInfo> answerAs(Actor actor) {
    var actorsApiClient =
        actor
            .uses(CallHttpApis.class)
            .buildClient()
            .baseUrl(serviceUrl)
            .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)
Kotlin
class AllActors : Question<List<ActorInfo>> {
  override fun answerAs(actor: Actor): List<ActorInfo> {
    val actorsApiClient = actor
        .uses(CallHttpApis::class.java)
              .buildClient()
        .baseUrl(serviceUrl)
        .addJacksonConverterFactory(ObjectMapper().registerModule(kotlinModule())) (1)
        .build(ActorsApi::class.java)
    return try {
      actorsApiClient.getActors().execute().body() ?: throw RuntimeException("No body")
    } catch (e: IOException) {
      throw RuntimeException("${this} failed", e)
    }
  }
}

data class ActorInfo(val id: String, val name: String) (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