For testing, a special actor system called TestSystem provides an additional testing feature. It allows intercepting outgoing messages of the actor to be tested. Therefore, an actor’s behavior can be tested (black box testing). How does the actor react to an ingoing message? Each actor, which should be tested, must implement the interface ActorTest. This interface defines a method called test, which returns a list of predefined testing stories [1] for that actor. The testing stories of this actor can then be executed by the TestSystem. The testing stories are derived from behavior-driven development (BDD).
<dependency>
<groupId>io.actor4j</groupId>
<artifactId>actor4j-testing</artifactId>
<version>${actor4j.version}</version>
<scope>test</scope>
</dependency>
The following testing story will send a message “Hello World!” to the actor itself. The actor will respond to this message with “Hello World Again!”. The testing actor system can intercept this message (see method test).
public class MyActor extends Actor implements ActorTest {
@Override
public void receive(ActorMessage<?> message) {
if (message.value() instanceof String) {
logger().info(String.format(
"Received String message: %s", message.valueAsString()));
tell("Hello World Again!", 0, message.source());
}
else
unhandled(message);
}
@Override
public List<Story> test() {
TestSystem system = (TestSystem)getSystem();
AtomicReference<String> actual = new AtomicReference<>();
Story story = new Story();
story.scenario()
.annotate("Scenario: Sending and receiving a message String")
.annotate("Given a request message")
.given(() -> send(ActorMessage.create("Hello World!", 0, system.SYSTEM_ID(), self())))
.annotate("When the responded message was received")
.when(() -> {
try {
actual.set(system.awaitMessage(5, TimeUnit.SECONDS).valueAsString());
} catch (InterruptedException | TimeoutException e) {
throw new AssertionError("No response", e);
}
})
.annotate("Then the responded received String message will be \"Hello World Again!\"")
.then(() -> outcome(actual.get()).shouldBe("Hello World Again!"))
.and(() -> system.assertNoMessages(200, TimeUnit.MILLISECONDS));
return List.of(story);
}
}
The stories are executed within a JUnit test:
public class MyActorFeature {
protected TestSystem system;
@Before
public void before() {
system = TestSystem.create();
system.addActor(() -> new MyActor());
system.start();
}
@After
public void after() {
system.shutdownWithActors(true);
}
@Test(timeout=5000)
public void test() {
system.testAllActors();
}
}
/* Full examples under https://github.com/relvaner/actor4j/tree/master/actor4j-examples */
testAllActors() executes the stories of every actor that implements ActorTest, one actor after the other. testActor(id) executes only the stories of one actor. A failing step fails the JUnit test. All stories are still executed; the first failure is thrown, further failures are attached as suppressed exceptions.
While an actor is under test, every message it sends to another actor is redirected to an internal pseudo actor. The steps fetch these messages with awaitMessage, in the order they were sent.
tell/send by id or alias, priority, unsafe sends — everything the actor under test sends to someone else.SYSTEM_ID), all messages between other actors, and directives such as STOP or RESTART, also when the actor under test triggers them (supervision of its children keeps working).The redirection ends as soon as the stories of the actor are finished, also when a step throws.
| Method | Meaning | Use when |
|---|---|---|
assertNoMessages() |
snapshot: no message is there right now | the last expected message was already awaited, so nothing can still be on its way (e.g. the actor sends a final done message), or at the start of a story |
assertNoMessages(timeout, unit) |
no message arrives within the timeout | you want to show that the actor does not answer, or does not send anything more |
Neither method consumes a message: a message found by them can still be fetched with awaitMessage.
The stories are executed on the caller’s thread (the JUnit thread), not on the actor’s thread. This is necessary, because a step that awaits a response would otherwise block the actor itself. As a consequence, steps should send messages and check messages, and should not read or write fields of the actor directly; the actor processes messages on its own thread at the same time. To check the internal state, let the actor answer a state query. The response is intercepted like any other message:
public class CounterActor extends Actor implements ActorTest {
public static final int INCREMENT = 1;
public static final int STATE = 2;
protected long counter;
@Override
public void receive(ActorMessage<?> message) {
if (message.tag()==INCREMENT)
counter++;
else if (message.tag()==STATE)
tell(counter, STATE, message.source());
}
@Override
public List<Story> test() {
TestSystem system = (TestSystem)getSystem();
Story story = new Story();
story.scenario()
.given(() -> {
for (int i=0; i<3; i++)
send(ActorMessage.create(null, INCREMENT, system.SYSTEM_ID(), self()));
})
.when(() -> send(ActorMessage.create(null, STATE, system.SYSTEM_ID(), self())))
.then(() -> {
try {
outcome(system.awaitMessage(5, TimeUnit.SECONDS).value()).shouldBe(3L);
} catch (InterruptedException | TimeoutException e) {
throw new AssertionError("No state", e);
}
});
return List.of(story);
}
}
If a field must be read directly, declare it volatile or use an Atomic* type.
Scenario keeps the current example in static fields, so stories must not be executed in parallel (e.g. parallel JUnit execution with several TestSystem instances in one JVM).awaitMessage returns the next intercepted message; there is no filter by tag or sender yet.