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.
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.
implementation 'org.shakespeareframework:core:0.8.1'
implementation("org.shakespeareframework:core:0.8.1")
<dependency>
<groupId>org.shakespeareframework</groupId>
<artifactId>core</artifactId>
<version>0.8.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.
implementation platform('org.shakespeareframework:bom:0.8.1')
implementation 'org.shakespeareframework:selenium'
implementation 'org.shakespeareframework:retrofit'
implementation(platform("org.shakespeareframework:bom:0.8.1"))
implementation("org.shakespeareframework:selenium")
implementation("org.shakespeareframework:retrofit")
<project>
<!--…-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.shakespeareframework</groupId>
<artifactId>bom</artifactId>
<version>0.8.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.
var robin = new Actor("Robin");
val robin = Actor("Robin")
var user = new Actor();
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.
class Log implements Ability {
private final Logger logger;
Log(String name) {
this.logger = Logger.getLogger(name);
}
public Logger getLogger() {
return logger;
}
}
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.
var anna = new Actor("Anna").can(new Log("Anna"));
val anna = Actor("Anna").can(Log("Anna"))
To get an Ability from an Actor, there’s the uses(Class<? extends Ability>)
method.
var log = anna.uses(Log.class);
log.getLogger().info("Hello World");
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.
class SayHelloWorld implements Task {
@Override
public void performAs(Actor actor) {
var logger = actor.uses(Log.class).getLogger();
logger.info("Hello World");
}
}
class SayHelloWorld() : Task {
override fun performAs(actor: Actor) {
val logger = actor.uses(Log::class.java).logger
logger.info("Hello World")
}
}
tom.does(new SayHelloWorld());
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.
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));
}
}
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.
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));
}
}
alex.does(new SayHello("Shakespeare"));
alex.does(new SayHelloAsRecord("Shakespeare"));
alex.does(SayHello("Shakespeare"))
Lambdas as Tasks
As Tasks only have one abstract method, they can be declared using Java’s Lambda syntax.
mila.does(she -> she.uses(Log.class).getLogger().info("Hi"));
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:
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);
}
}
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:
-
Add any product to the shopping cart,
-
navigate to the checkout page,
-
enter a delivery address,
-
enter valid payment details,
-
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
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.
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";
}
}
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:
-
Optionals that are present,
-
non-empty Collections, Maps or Arrays,
-
any Boolean answer, and
-
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.
kate.learns(new PhoneNumber("+49 0180 4 100 100"));
PhoneNumber katesPhoneNumber = kate.remembers(PhoneNumber.class);
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.
record PhoneNumber(String number) implements Fact {}
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.
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"));
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:
record PhoneNumbers(String home, String work) implements Fact {}
data class PhoneNumbers(val home: String, val work: String) : Fact
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();
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:
record CreditCard(String type, String number, YearMonth expiration) implements Fact {
public static CreditCard DEFAULT_VISA =
new CreditCard("visa", "4111111111111111", YearMonth.of(2026, 10));
}
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:
record EmailAddress(String address) implements Fact {
public static EmailAddress random() {
return new EmailAddress("%s@shakespeareframework.org".formatted(UUID.randomUUID()));
}
}
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.
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.
var tim =
new Actor("Tim").can(new BrowseTheWeb(new LocalWebDriverSupplier(BrowserType.CHROME)));
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.
var webDriverManager = WebDriverManager.edgedriver().browserInDocker();
var alex =
new Actor("Alex")
.can(
new BrowseTheWeb(
new WebDriverManagerWebDriverSupplier(webDriverManager, BrowserType.CHROME)));
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.
var browseTheWeb = new BrowseTheWeb(webDriverSupplier, "https://shakespeareframework.org/");
assertThat(browseTheWeb.getWebDriver().getCurrentUrl())
.isEqualTo("https://shakespeareframework.org/");
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.
var cameron =
new Actor("Cameron")
.can(
new BrowseTheWeb(
new LocalWebDriverSupplier(
BrowserType.CHROME, new ChromeOptions().addArguments("--headless"))));
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
.
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();
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.
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();
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.
interface ActorsApi {
@GET("/actors")
Call<List<ActorInfo>> getActors();
@GET("/actors/{id}/name")
Call<String> getActorName(@Path("id") String id);
}
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)
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);
}
}
}
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
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)
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.