Activities Overview

Activity Description
See The See activity is used to assert the result of a task.
Retry The Retry activity is used to repeat a task until it succeeds or the timeout is reached.
Map Transform a task’s result inline with .map() or via the static API.map() activity.
Sleep The Sleep activity is used to pause the execution of the test for a specified amount of time.

Data Validation with See

Validating Activity Results

Activities can be validated by calling .is() directly on them. This is the recommended approach for checking task results in the screenplay pattern.

The .is() method is available on activities that return a result:

  • Task<PT, RT> - validates the return type RT
  • SupplierTask<RT> - validates the return type RT

Not available on:

  • BasicInteraction - returns Void (no result to validate)
  • ConsumerTask<PT> - returns Void (no result to validate)
  • Interaction<PT, RT> - use See.ifThe() for custom Interaction subclasses

Note: Interaction class doesn’t have .is() due to type erasure conflicts with the See class which extends Interaction. For custom Interaction subclasses, use See.ifThe(myInteraction).is(matcher) instead.

Methods:

type method description
Activity method .is( SeeAssertion ) Recommended: Validate activity result directly. Accepts a SeeAssertion to check the result for conditions.
  .forAsLongAs(Duration x) Sets the timeout for which validation is repeated until the assertion succeeds.
  .every( Duration x ) Repeat the validation every x seconds until the assertion succeeds or timeout is reached.
static See.ifThe( Activity ) Alternative verbose form. Wraps an activity explicitly (rarely needed).
static See.ifResult()... Special case: validates the result passed from the previous activity.

Best Practice: Use .is() directly on activities for clearer test flow. The See.ifThe() wrapper adds unnecessary verbosity in most cases.

SeeAssertion

The SeeAssertion is a functional interface that is used to check the result of a task. It is used in the See activity and has the following declaration:

@FunctionalInterface
public interface SeeAssertion<T1> {
  Either<ActivityError, Void> affirm(T1 actual);
}

Two of the most simplest implementations are:

// assertion is always true -> Either.Right means the assertion was successful
actualValue -> Either.right(null);

and

// assertion is always false -> Either.Left means the assertion failed
actualValue -> Either.left(new ActivityError("Assertion failed for actual " + actualValue));

A more meaningful example would be:

// assertion that the actual value is greater than 5
SeeAssertion<Integer> expectedToBeGreaterThanFive = actualValue -> {
  if (actualValue > 5) {
    return Either.right(null);
  } else {
    return Either.left(new ActivityError("Value is not greater than 5"));
  }
};

And then used directly on an activity:

actor.attemptsTo(
  TaskReturningRandomInteger.between(1,10)
    .is(expectedToBeGreaterThanFive));

Expected SeeAssertion

To facilitate the creation of assertions, the Expected class is provided. It contains a set of static methods that return SeeAssertion functions.

Methods:

method description
Expected.to.pass( Predicate ) Accepts a predicate and checks if the result passes the predicate.
Expected.to.pass( Predicate, String reason) Accepts a predicate and checks if the result passes the predicate. In case of an error the reason will be printed in the error message.
Expected.not.to.pass( Predicate ) Accepts a predicate and checks if the result DOES NOT pass the predicate.
Expected.to.equal( String ) Accepts an object and checks if the result equals the object.
Expected.to.be.equal( String ) same as above, the .to.be is just language candy
Expected.not.to.equal( String ) Accepts an object and checks if the result equals the object.
Expected.not.to.be.equal( String ) same as above, the .to.be is just language candy

Example with Task: The task TaskReturningRandomInteger.between(1,10) is executed and the result is checked if it is greater than five.

actor.attemptsTo(
  TaskReturningRandomInteger.between(1,10)
    .is(Expected.to.pass(randomInteger -> randomInteger >= 5)));

or by defining a predicate:

Predicate<Integer> toBeGreaterThanFive = randomInteger -> randomInteger >= 5;

actor.attemptsTo(
  TaskReturningRandomInteger.between(1,10)
    .is(Expected.to.pass(toBeGreaterThanFive)));

or if you want to use the same predicate to check for lower than 5:

actor.attemptsTo(
  TaskReturningRandomInteger.between(1,10)
    .is(Expected.not.to.pass(toBeGreaterThanFive)));

Note: The verbose form See.ifThe(task).is(...) also works but is less concise.

Assertion Chaining

You can chain multiple assertions by calling .is() multiple times. All assertions will be checked and the validation fails if any assertion fails. If multiple assertions fail, the error message will contain all failed assertions.

actor.attemptsTo(
  // the random integer has to be greater than 5 AND a prime number
  // for multiple assertions the reason parameter is useful to identify which assertion failed
  TaskReturningRandomInteger.between(1,10)
    .is(Expected.to.pass(toBeGreaterThanFive, "check if greater than 5"))
    .is(Expected.to.pass(isPrimeNumber, "check if prime number")));

Assertion Polling

Activity validation can be repeated until the assertion is successful or the timeout is reached. The forAsLongAs() method is used to set the timeout. The every() method is used to set the interval at which the assertion is checked.

The RandomNumber task is executed and the result will be checked if the random integer is greater than 5 every second for 10 seconds:

actor.attemptsTo(
  TaskReturningRandomInteger.between(1,10)
    .is(Expected.to.pass(toBeGreaterThanFive))
    .forAsLongAs(Duration.ofSeconds(10))
    .every(Duration.ofSeconds(1)));

The default timeout is 0 seconds so the validation is executed only once. The default retry interval is 1 second (so it could be omitted in the example above).

This feature is useful for checking database entries, or waiting for a specific state in the application (e.g. waiting for a GET call to return a specific value).


Retry

The Retry activity is used to repeat a task until it succeeds or the timeout is reached. The Retry activity is useful for tasks that are not deterministic, like waiting for a specific state in the application.

Shorthand task.retry() Method

The most concise way to retry a Task or SupplierTask is calling .retry(predicate) directly on the task. This is equivalent to the verbose static form but keeps the code fluent:

// Shorthand (recommended)
actor.attemptsTo(
  GetCurrentTime.now()
    .retry(time -> time > expectedTime)
    .forAsLongAs(Duration.ofSeconds(10))
    .every(Duration.ofMillis(200)));

// Equivalent verbose form
actor.attemptsTo(
  Retry.task(GetCurrentTime.now())
    .until(time -> time > expectedTime, "time exceeds expected")
    .forAsLongAs(Duration.ofSeconds(10))
    .every(Duration.ofMillis(200)));

Full chain — retry, then map, then validate:

After .retry() resolves, you can continue with .map() and .is() to transform and validate the final value:

Either<ActivityError, String> result = actor.attemptsTo(
  CountingTaskSupplier.failsUntil(4)
    .retry(n -> n >= 4)                                  // retry until task returns n >= 4
    .forAsLongAs(Duration.ofSeconds(10))
    .every(Duration.ofMillis(100))
    .map(n -> "count=" + n)                              // transform result
    .is(Expected.to.pass(s -> s.startsWith("count=")))); // validate and return
// result.get() == "count=4"

Static Retry.task() API

Methods:

type method description
static Retry.task( Activity ) Accepts a task as parameter and executes it. The result is checked for conditions.
  .until( Predicate ) .until() accepts a Predicate, checking the result for a specific condition
  .forAsLongAs(Duration) Sets the timeout for which the Retry activity is repeated and the result is checked.
  .every( Duration ) Repeat the Retry activity every x Seconds until the assertion succeeds or timeout is reached.

Defaults:

parameter default value
timeout 5 seconds
retry interval 1 second

The RandomNumber task is executed and the result will be checked if the random integer is greater than 5 every second for 5 seconds.

actor.attemptsTo(
  Retry.task( TaskReturningRandomInteger.between(1,10) )
  .until( randomInteger -> randomInteger > 5 )

The RandomNumber task is executed and the result will be checked if the random integer is greater than 5 every three seconds for the maximum time of 10 seconds.

actor.attemptsTo(
  Retry.task( TaskReturningRandomInteger.between(1,10) )
  .until( randomInteger -> randomInteger > 5 )
  .forAsLongAs(Duration.ofSeconds(10))
  .every(Duration.ofSeconds(3)));

You could create the same behavior with the See activity, but the Retry activity was specifically designed for polling tasks and is therefor simpler to use.


Mapping

The Mapping feature is used to transform the result of a task. There are circumstances where its easier to transform the result of a task with a function then creating a new Task.

Instance .map() Method

The recommended approach is to call .map() directly on a Task or SupplierTask. This keeps the chain fluent and works seamlessly with .is() and .retry().

Single map — type changes from List<Integer> to Integer:

actor.attemptsTo(
  SupplyList.numbers(1, 2, 3)
    .map(l -> l.head())                                  // List<Integer> → Integer
    .is(Expected.to.pass(n -> n == 1, "head is 1")));

Chained maps — each step narrows the type:

Either<ActivityError, String> result = actor.attemptsTo(
  SupplyList.numbers(5, 6, 7)
    .map(l -> l.head())                                  // List<Integer> → Integer
    .map(n -> "value=" + n)                              // Integer → String
    .is(Expected.to.pass(s -> s.equals("value=5"))));
// result.get() == "value=5"

Task with input:

Either<ActivityError, String> result = actor.attemptsTo_(
  AddNumber.of(5)
    .map(n -> "sum=" + n)                                // Integer → String
    .is(Expected.to.pass(s -> s.equals("sum=15"))))
  .using(10);
// result.get() == "sum=15"

Key advantage: .is() validates and returns the mapped result. The return type of actor.attemptsTo() reflects the final mapped type — no extra See.ifResult() step needed.

Full chain with retry:

Either<ActivityError, Integer> result = actor.attemptsTo(
  CountingTask.failsUntil(4)
    .retry(n -> n >= 4)
    .forAsLongAs(Duration.ofSeconds(10))
    .every(Duration.ofMillis(200))
    .map(n -> n * 10)                                    // Integer → Integer
    .is(Expected.to.pass(n -> n == 40, "mapped value is 40")));
// result.get() == 40

Static API.map() Activity

The API class also contains a static map method that accepts a function to which the result of the preceding task is passed. Use this form when you need to transform a result mid-chain between independent activities.

Methods:

type method description
static API.map(Function<IN,OUT>, mapper) Accepts a mapper function and passes the result of the preceding task to it.
static API.map(Function<IN,OUT> mapper, String reason) Same as above but adds the reason to the activity log. It makes the activity log more readable
static API.mapTry(Function<IN,Try<OUT>> mapper) same as above but for functions known to throw an exception.
static API.mapTry(Function<IN,Try<OUT>> mapper, String reason) same as above but again with a reason which is used for the activity log.
import static com.teststeps.thekla4j.core.activities.API.map;

actor.attemptsTo(
  
  TaskReturningRandomInteger.between(1,10),
  
  map( i -> i * 2,  "multiply the integer by 2"),

  See.ifResult()
    .is(Expected.to.pass( isEven, "check if the result is even")));


Sleep

The Sleep activity is used to pause the execution of the test for a specified amount of time. It was implemented to support Development and Debugging of Tests. Its not advisable to use it in production tests.

Methods:

type method description
static Sleep.forA(Duration sleepTime) Stop test execution for the amount of time specified
  .because(String reason) Set a reason for sleeping. It will be taken over to the activity log

This site uses Just the Docs, a documentation theme for Jekyll.