Saltar al contenido principal

Screenplay Pattern con Playwright

El Screenplay Pattern es un enfoque moderno y centrado en el Actor para escribir pruebas automatizadas. Cuando se combina con Playwright, proporciona una forma poderosa y expresiva de escribir pruebas de automatizacion de navegadores que se leen como documentacion viva.

El modulo serenity-screenplay-playwright une:

  • Expresividad de Screenplay - Diseno de pruebas centrado en el Actor y basado en Task
  • Confiabilidad de Playwright - Automatizacion de navegadores moderna con espera automatica
  • Reportes enriquecidos de Serenity - Documentacion paso a paso con capturas de pantalla

Por que Screenplay con Playwright?​

El Screenplay Pattern ofrece varias ventajas sobre los enfoques tradicionales con Page Object:

Page ObjectScreenplay
Las pruebas llaman metodos de pagina directamenteLos Actor realizan Task y hacen Question
Estilo de codigo proceduralEstilo declarativo enfocado en comportamiento
Pruebas atadas a estructura de UIPruebas expresan intencion de negocio
Mas dificil componer accionesLos Task se componen facilmente en flujos de trabajo

Con Playwright, tambien obtienes:

  • Espera automatica integrada en todas las Interaction
  • Aserciones integradas con reintento automatico
  • Intercepcion de red para simular API
  • Soporte para multiples navegadores (Chromium, Firefox, WebKit)

Configuracion del proyecto​

Dependencias de Maven​

Agrega estas dependencias a tu pom.xml:

<properties>
<serenity.version>5.3.3</serenity.version>
<playwright.version>1.58.0</playwright.version>
</properties>

<dependencies>
<!-- Serenity Screenplay con Playwright -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay-playwright</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>

<!-- Serenity Screenplay Core -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-screenplay</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>

<!-- Serenity Ensure (para aserciones fluidas) -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-ensure</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>

<!-- Serenity JUnit 5 -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>${serenity.version}</version>
<scope>test</scope>
</dependency>

<!-- Playwright -->
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>${playwright.version}</version>
<scope>test</scope>
</dependency>

<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>

Conceptos principales​

Actor y Ability​

En Screenplay, las pruebas se escriben desde la perspectiva de Actor que tienen Ability. Para pruebas de Playwright, los Actor necesitan la Ability BrowseTheWebWithPlaywright:

import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;

Actor toby = Actor.named("Toby");
toby.can(BrowseTheWebWithPlaywright.usingTheDefaultConfiguration());

La Ability gestiona el ciclo de vida del navegador Playwright automaticamente:

  • Crea el navegador, contexto y pagina bajo demanda
  • Limpia automaticamente cuando la prueba termina (no se necesita limpieza explicita)
  • Se suscribe a los eventos del ciclo de vida de pruebas de Serenity para gestion de recursos fluida

Task​

Los Task representan acciones de alto nivel que un Actor realiza. Expresan que hace el Actor en terminos de negocio:

import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.playwright.interactions.Open;
import net.serenitybdd.annotations.Step;

public class OpenTodoMvcApp implements Task {

@Override
@Step("{0} opens the TodoMVC application")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Open.url("https://todomvc.com/examples/react/dist/")
);
}

public static OpenTodoMvcApp onTheHomePage() {
return new OpenTodoMvcApp();
}
}

Los Task pueden componerse de otros Task e Interaction:

public class AddATodoItem implements Task {
private final String todoItem;

public AddATodoItem(String todoItem) {
this.todoItem = todoItem;
}

@Override
@Step("{0} adds a todo item called '#todoItem'")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(todoItem).into(TodoList.NEW_TODO_INPUT),
Press.keys("Enter")
);
}

public static AddATodoItem called(String todoItem) {
return new AddATodoItem(todoItem);
}
}

Question​

Las Question permiten a los Actor consultar el estado de la aplicacion:

import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;

public class TheVisibleTodos {

public static Question<Collection<String>> displayed() {
return Question.about("the visible todos").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(".todo-list li label")
.allTextContents()
);
}

public static Question<Integer> count() {
return Question.about("visible todo count").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(".todo-list li")
.count()
);
}
}

Target​

Los Target definen elementos de UI usando selectores de Playwright:

import net.serenitybdd.screenplay.playwright.Target;

public class TodoList {

public static final Target NEW_TODO_INPUT =
Target.the("new todo input")
.locatedBy("[placeholder='What needs to be done?']");

public static final Target TODO_ITEMS =
Target.the("todo items")
.locatedBy(".todo-list li");

public static Target todoItemCalled(String todoText) {
return Target.the("todo item '" + todoText + "'")
.locatedBy(".todo-list li:has-text('" + todoText + "')");
}

public static Target checkboxFor(String todoText) {
return Target.the("checkbox for '" + todoText + "'")
.locatedBy(".todo-list li:has-text('" + todoText + "') .toggle");
}
}

Los Target soportan:

  • Selectores parametrizados: Target.the("item {0}").locatedBy("[data-id='{0}']").of("123")
  • Target anidados: button.inside(form)
  • Soporte para frames: Target.the("editor").inFrame("#iframe").locatedBy("#content")

Escribiendo pruebas​

Estructura basica de prueba​

Aqui hay un ejemplo completo de una prueba Screenplay con Playwright:

import net.serenitybdd.junit5.SerenityJUnit5Extension;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.ensure.Ensure;
import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("When adding todos")
class WhenAddingTodosTest {

Actor toby;

@BeforeEach
void setUp() {
toby = Actor.named("Toby");
toby.can(BrowseTheWebWithPlaywright.usingTheDefaultConfiguration());
}

// No se necesita @AfterEach - la limpieza ocurre automaticamente!

@Test
@DisplayName("should add a single todo item")
void shouldAddSingleTodoItem() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
Ensure.that(TheVisibleTodos.count()).isEqualTo(1),
Ensure.that(TheTodoItem.called("Buy milk").exists()).isTrue()
);
}

@Test
@DisplayName("should add multiple todo items")
void shouldAddMultipleTodoItems() {
toby.attemptsTo(
OpenTodoMvcApp.onTheHomePage(),
AddATodoItem.called("Buy milk"),
AddATodoItem.called("Walk the dog"),
AddATodoItem.called("Do laundry"),
Ensure.that(TheVisibleTodos.displayed())
.containsExactly("Buy milk", "Walk the dog", "Do laundry")
);
}
}

Usando Ensure para aserciones​

La clase Ensure proporciona aserciones fluidas que se integran con el modelo basado en Task de Screenplay:

toby.attemptsTo(
// Comparaciones numericas
Ensure.that(TheRemainingCount.value()).isEqualTo(3),
Ensure.that(TheVisibleTodos.count()).isGreaterThan(0),

// Verificaciones booleanas
Ensure.that(TheTodoItem.called("Buy milk").isCompleted()).isTrue(),
Ensure.that(TheClearCompletedButton.isVisible()).isFalse(),

// Comparaciones de cadenas
Ensure.that(TheCurrentFilter.selected()).isEqualTo("All"),

// Aserciones de colecciones
Ensure.that(TheVisibleTodos.displayed())
.containsExactly("Buy milk", "Walk the dog"),
Ensure.that(TheVisibleTodos.displayed())
.contains("Buy milk")
);

Interaction integradas​

El modulo serenity-screenplay-playwright proporciona muchas Interaction integradas:

// Abrir una URL
actor.attemptsTo(Open.url("https://example.com"));

// Navegar atras/adelante
actor.attemptsTo(Navigate.back());
actor.attemptsTo(Navigate.forward());

// Refrescar la pagina
actor.attemptsTo(Navigate.refresh());

Clic​

// Clic simple
actor.attemptsTo(Click.on(TodoList.SUBMIT_BUTTON));

// Doble clic
actor.attemptsTo(DoubleClick.on(TodoList.TODO_LABEL));

// Clic derecho
actor.attemptsTo(RightClick.on(TodoList.CONTEXT_MENU_TRIGGER));

Interacciones de formulario​

// Ingresar texto
actor.attemptsTo(Enter.theValue("Hello").into(TodoList.INPUT_FIELD));

// Limpiar un campo
actor.attemptsTo(Clear.field(TodoList.INPUT_FIELD));

// Presionar teclas
actor.attemptsTo(Press.keys("Enter"));
actor.attemptsTo(Press.keys("Control+a"));

// Marcar/desmarcar checkboxes
actor.attemptsTo(Check.checkbox(TodoList.AGREE_CHECKBOX));
actor.attemptsTo(Uncheck.checkbox(TodoList.AGREE_CHECKBOX));

// Seleccionar de dropdown
actor.attemptsTo(SelectFromOptions.byVisibleText("Option 1").from(TodoList.DROPDOWN));
actor.attemptsTo(SelectFromOptions.byValue("opt1").from(TodoList.DROPDOWN));

Acciones de raton​

// Pasar el raton sobre un elemento
actor.attemptsTo(Hover.over(TodoList.DELETE_BUTTON));

// Arrastrar y soltar
actor.attemptsTo(Drag.from(SOURCE).to(TARGET));

// Desplazar al elemento visible
actor.attemptsTo(ScrollIntoView.element(TodoList.FOOTER));

Esperas​

// Esperar por un elemento
actor.attemptsTo(WaitFor.the(TodoList.LOADING_SPINNER).toDisappear());

Ejecucion de JavaScript​

// Ejecutar JavaScript
actor.attemptsTo(
ExecuteJavaScript.withScript("window.scrollTo(0, document.body.scrollHeight)")
);

Manejo de dialogos​

// Aceptar alerta
actor.attemptsTo(HandleDialog.byAccepting());

// Descartar alerta
actor.attemptsTo(HandleDialog.byDismissing());

// Ingresar texto en prompt
actor.attemptsTo(HandleDialog.byEntering("my response"));

Aserciones especificas de Playwright​

La clase Ensure tambien proporciona aserciones especificas de Playwright con reintento automatico:

import net.serenitybdd.screenplay.playwright.assertions.Ensure;

actor.attemptsTo(
// Visibilidad de elemento
Ensure.that(TodoList.MAIN_SECTION).isVisible(),
Ensure.that(TodoList.LOADING_SPINNER).isHidden(),

// Contenido de texto
Ensure.that(TodoList.HEADER).hasText("todos"),
Ensure.that(TodoList.HEADER).containsText("todo"),

// Conteo de elementos
Ensure.that(TodoList.TODO_ITEMS).hasCount(3),

// Atributos y clases
Ensure.that(TodoList.TODO_ITEM).hasClass("completed"),
Ensure.that(TodoList.INPUT).hasAttribute("placeholder", "What needs to be done?"),

// Estado del elemento
Ensure.that(TodoList.SUBMIT_BUTTON).isEnabled(),
Ensure.that(TodoList.CHECKBOX).isChecked(),

// URL y titulo de pagina
Ensure.thatTheCurrentUrl().contains("/dashboard"),
Ensure.thatThePageTitle().isEqualTo("My App")
);

Caracteristicas avanzadas​

Acceso directo a la API de Playwright​

Para escenarios avanzados, puedes acceder a la API de Playwright directamente:

Page page = BrowseTheWebWithPlaywright.as(actor).getCurrentPage();

// Usar la API nativa de Playwright
page.locator(".my-element").click();

// Acceder al contexto del navegador
BrowserContext context = page.context();

// Intercepcion de red
page.route("**/api/**", route -> {
route.fulfill(new Route.FulfillOptions()
.setBody("{\"mocked\": true}")
.setContentType("application/json"));
});

Task personalizados​

Crea Task especificos del dominio para tu aplicacion:

public class CompleteCheckout implements Task {

private final PaymentDetails paymentDetails;

public CompleteCheckout(PaymentDetails paymentDetails) {
this.paymentDetails = paymentDetails;
}

@Override
@Step("{0} completes checkout with #paymentDetails")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
EnterShippingAddress.forCurrentUser(),
EnterPaymentDetails.using(paymentDetails),
Click.on(CheckoutPage.PLACE_ORDER_BUTTON),
WaitFor.the(CheckoutPage.CONFIRMATION_MESSAGE).toAppear()
);
}

public static CompleteCheckout using(PaymentDetails details) {
return new CompleteCheckout(details);
}
}

Question personalizadas​

Crea Question reutilizables para tu dominio:

public class TheOrderTotal {

public static Question<BigDecimal> displayed() {
return Question.about("the order total").answeredBy(actor -> {
String totalText = BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(".order-total")
.textContent();
return new BigDecimal(totalText.replace("$", ""));
});
}
}

// Uso
actor.attemptsTo(
Ensure.that(TheOrderTotal.displayed()).isGreaterThan(new BigDecimal("100.00"))
);

Multiples paginas/pestanas​

Maneja multiples pestanas del navegador:

// Abrir una nueva pagina
BrowseTheWebWithPlaywright.as(actor).openNewPage();

// Cambiar entre paginas
BrowseTheWebWithPlaywright.as(actor).switchToPage(0);
BrowseTheWebWithPlaywright.as(actor).switchToPageWithTitle("Dashboard");

// Cerrar pagina actual
BrowseTheWebWithPlaywright.as(actor).closeCurrentPage();

Gestion de cookies​

Gestiona cookies del navegador:

// Obtener todas las cookies
List<Cookie> cookies = BrowseTheWebWithPlaywright.as(actor).getCookies();

// Agregar una cookie
Cookie cookie = new Cookie("session", "abc123")
.setDomain("example.com")
.setPath("/");
BrowseTheWebWithPlaywright.as(actor).addCookie(cookie);

// Limpiar cookies
BrowseTheWebWithPlaywright.as(actor).clearCookies();

Manejo de frames​

Interactua con elementos dentro de iframes:

// Definir un Target dentro de un frame
Target EDITOR_CONTENT = Target.the("editor content")
.inFrame("#editor-iframe")
.locatedBy("#content");

// Interactua normalmente
actor.attemptsTo(
Enter.theValue("Hello World").into(EDITOR_CONTENT)
);

Capturas de pantalla​

Toma capturas de pantalla explicitamente:

// El metodo notifyScreenChange() captura una pantalla
BrowseTheWebWithPlaywright.as(actor).notifyScreenChange();

// O toma una captura explicitamente
ScreenshotAndHtmlSource screenshot = BrowseTheWebWithPlaywright.as(actor).takeScreenShot();

Configuracion​

Opciones de navegador​

Configura las opciones del navegador Playwright via propiedades del sistema o programaticamente:

# serenity.properties
playwright.browsertype=chromium
playwright.headless=true
playwright.channel=chrome
playwright.baseurl=https://staging.example.com
playwright.slowmo=200

O programaticamente con opciones de lanzamiento explicitas:

actor.can(
BrowseTheWebWithPlaywright
.withOptions(new BrowserType.LaunchOptions().setHeadless(false))
.withBrowserType("firefox")
);

Uso de un OptionsFactory​

Si ya tienes un OptionsFactory de Playwright (por ejemplo, el mismo que usas con @UsePlaywright), puedes reutilizarlo con Screenplay a traves de withOptions(OptionsFactory). Esto aplica el conjunto completo de Options: tipo de navegador, opciones de lanzamiento, opciones de contexto, testIdAttribute, wsEndpoint, etc.:

public class ChromeHeadlessOptions implements OptionsFactory {
@Override
public Options getOptions() {
return new Options()
.setHeadless(true)
.setChannel("chrome")
.setContextOptions(
new Browser.NewContextOptions()
.setViewportSize(1920, 1080)
)
.setTestIdAttribute("data-test");
}
}

// En la configuracion del test
actor.can(
BrowseTheWebWithPlaywright.withOptions(new ChromeHeadlessOptions())
);

Las propiedades de Serenity (playwright.headless, playwright.browsertype, playwright.channel, playwright.baseurl, playwright.slowmo) se utilizan como valores por defecto cuando la opcion correspondiente no esta definida en el factory.

Configuracion de capturas de pantalla​

Controla la captura de pantallas:

# serenity.conf
serenity {
take.screenshots = FOR_EACH_ACTION # o FOR_FAILURES, DISABLED
}

Ejemplo completo: Conjunto de pruebas TodoMVC​

Aqui hay un ejemplo completo mostrando todos los conceptos juntos:

Target de UI​

public class TodoList {

public static final Target NEW_TODO_INPUT =
Target.the("new todo input")
.locatedBy("[placeholder='What needs to be done?']");

public static final Target TODO_ITEMS =
Target.the("todo items")
.locatedBy(".todo-list li");

public static final Target TODO_COUNT =
Target.the("todo count")
.locatedBy(".todo-count");

public static final Target CLEAR_COMPLETED_BUTTON =
Target.the("clear completed button")
.locatedBy(".clear-completed");

public static final Target ALL_FILTER =
Target.the("All filter")
.locatedBy(".filters a:has-text('All')");

public static final Target ACTIVE_FILTER =
Target.the("Active filter")
.locatedBy(".filters a:has-text('Active')");

public static final Target COMPLETED_FILTER =
Target.the("Completed filter")
.locatedBy(".filters a:has-text('Completed')");

public static Target todoItemCalled(String text) {
return Target.the("todo '" + text + "'")
.locatedBy(".todo-list li:has-text('" + text + "')");
}

public static Target checkboxFor(String text) {
return Target.the("checkbox for '" + text + "'")
.locatedBy(".todo-list li:has-text('" + text + "') .toggle");
}

public static Target deleteButtonFor(String text) {
return Target.the("delete button for '" + text + "'")
.locatedBy(".todo-list li:has-text('" + text + "') .destroy");
}
}

Task​

// AddATodoItem.java
public class AddATodoItem implements Task {
private final List<String> items;

public AddATodoItem(List<String> items) {
this.items = items;
}

@Override
@Step("{0} adds todo items: #items")
public <T extends Actor> void performAs(T actor) {
for (String item : items) {
actor.attemptsTo(
Enter.theValue(item).into(TodoList.NEW_TODO_INPUT),
Press.keys("Enter")
);
}
}

public static AddATodoItem called(String item) {
return new AddATodoItem(List.of(item));
}

public static AddATodoItem withItems(String... items) {
return new AddATodoItem(Arrays.asList(items));
}
}

// Complete.java
public class Complete implements Task {
private final String todoItem;

public Complete(String todoItem) {
this.todoItem = todoItem;
}

@Override
@Step("{0} completes the todo item '#todoItem'")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Click.on(TodoList.checkboxFor(todoItem))
);
}

public static Complete todoItem(String item) {
return new Complete(item);
}
}

// Delete.java
public class Delete implements Task {
private final String todoItem;

public Delete(String todoItem) {
this.todoItem = todoItem;
}

@Override
@Step("{0} deletes the todo item '#todoItem'")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Hover.over(TodoList.todoItemCalled(todoItem)),
Click.on(TodoList.deleteButtonFor(todoItem))
);
}

public static Delete theTodoItem(String item) {
return new Delete(item);
}
}

// FilterTodos.java
public class FilterTodos {

public static Task toShowAll() {
return Task.where("{0} filters to show all todos",
Click.on(TodoList.ALL_FILTER)
);
}

public static Task toShowActive() {
return Task.where("{0} filters to show active todos",
Click.on(TodoList.ACTIVE_FILTER)
);
}

public static Task toShowCompleted() {
return Task.where("{0} filters to show completed todos",
Click.on(TodoList.COMPLETED_FILTER)
);
}
}

Question​

// TheVisibleTodos.java
public class TheVisibleTodos {

public static Question<Collection<String>> displayed() {
return Question.about("the visible todos").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(".todo-list li label")
.allTextContents()
);
}

public static Question<Integer> count() {
return Question.about("visible todo count").answeredBy(
actor -> BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(".todo-list li")
.count()
);
}
}

// TheRemainingCount.java
public class TheRemainingCount {

public static Question<Integer> value() {
return Question.about("the remaining count").answeredBy(actor -> {
String text = BrowseTheWebWithPlaywright.as(actor)
.getCurrentPage()
.locator(".todo-count")
.textContent();
Matcher matcher = Pattern.compile("(\\d+)").matcher(text);
return matcher.find() ? Integer.parseInt(matcher.group(1)) : 0;
});
}
}

Clase de prueba​

@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("When managing todos")
class WhenManagingTodosTest {

Actor toby;

@BeforeEach
void setUp() {
toby = Actor.named("Toby");
toby.can(BrowseTheWebWithPlaywright.usingTheDefaultConfiguration());
}

// No se necesita @AfterEach - BrowseTheWebWithPlaywright automaticamente
// limpia los recursos del navegador cuando la prueba termina

@Test
@DisplayName("should add and complete todos")
void shouldAddAndCompleteTodos() {
toby.attemptsTo(
Open.url("https://todomvc.com/examples/react/dist/"),
AddATodoItem.withItems("Buy milk", "Walk the dog", "Do laundry"),

Ensure.that(TheVisibleTodos.count()).isEqualTo(3),
Ensure.that(TheRemainingCount.value()).isEqualTo(3),

Complete.todoItem("Buy milk"),

Ensure.that(TheRemainingCount.value()).isEqualTo(2),

FilterTodos.toShowCompleted(),

Ensure.that(TheVisibleTodos.displayed()).containsExactly("Buy milk"),

FilterTodos.toShowActive(),

Ensure.that(TheVisibleTodos.displayed())
.containsExactly("Walk the dog", "Do laundry")
);
}

@Test
@DisplayName("should delete a todo item")
void shouldDeleteTodoItem() {
toby.attemptsTo(
Open.url("https://todomvc.com/examples/react/dist/"),
AddATodoItem.withItems("Item 1", "Item 2", "Item 3"),

Delete.theTodoItem("Item 2"),

Ensure.that(TheVisibleTodos.displayed())
.containsExactly("Item 1", "Item 3")
);
}
}

Mejores practicas​

1. Manten los Task enfocados en el negocio​

Los Task deben expresar que el usuario intenta lograr, no como lo logra:

// Bien - expresa intencion
actor.attemptsTo(
AddItemToCart.called("Blue T-Shirt"),
ProceedToCheckout.withStandardShipping()
);

// Evitar - demasiado tecnico
actor.attemptsTo(
Click.on(ProductPage.ADD_TO_CART_BUTTON),
Click.on(Header.CART_ICON),
Click.on(CartPage.CHECKOUT_BUTTON)
);

2. Usa nombres significativos para Target​

Los nombres de Target aparecen en los reportes, asi que hazlos descriptivos:

// Bien
Target SUBMIT_ORDER_BUTTON = Target.the("submit order button")
.locatedBy("#checkout-submit");

// Evitar
Target BUTTON = Target.the("button").locatedBy("#checkout-submit");

3. Crea Question especificas del dominio​

Las Question deben devolver objetos de dominio significativos:

// Bien
Question<Money> orderTotal = TheOrderTotal.displayed();
Question<List<CartItem>> cartItems = TheCartContents.items();

// Evitar
Question<String> total = Text.of(OrderPage.TOTAL);

4. Aprovecha Ensure para aserciones legibles​

Usa Ensure.that() para aserciones que se lean naturalmente:

actor.attemptsTo(
Ensure.that(TheOrderStatus.displayed()).isEqualTo(OrderStatus.CONFIRMED),
Ensure.that(TheDeliveryDate.shown()).isAfter(LocalDate.now())
);

Siguientes pasos​