Testing Actor Systems
As with any piece of software, automated tests are a very important part of the development cycle. The actor model presents a different view on how units of code are delimited and how they interact, which has an influence on how to perform tests.
Akka comes with a dedicated module akka-testkit
for supporting tests at
different levels, which fall into two clearly distinct categories:
- Testing isolated pieces of code without involving the actor model, meaning without multiple threads; this implies completely deterministic behavior concerning the ordering of events and no concurrency concerns and will be called Unit Testing in the following.
- Testing (multiple) encapsulated actors including multi-threaded scheduling; this implies non-deterministic order of events but shielding from concurrency concerns by the actor model and will be called Integration Testing in the following.
There are of course variations on the granularity of tests in both categories, where unit testing reaches down to white-box tests and integration testing can encompass functional tests of complete actor networks. The important distinction lies in whether concurrency concerns are part of the test or not. The tools offered are described in detail in the following sections.
注釈
Be sure to add the module akka-testkit
to your dependencies.
Synchronous Unit Testing with TestActorRef
Testing the business logic inside Actor
classes can be divided into
two parts: first, each atomic operation must work in isolation, then sequences
of incoming events must be processed correctly, even in the presence of some
possible variability in the ordering of events. The former is the primary use
case for single-threaded unit testing, while the latter can only be verified in
integration tests.
Normally, the ActorRef
shields the underlying Actor
instance
from the outside, the only communications channel is the actor's mailbox. This
restriction is an impediment to unit testing, which led to the inception of the
TestActorRef
. This special type of reference is designed specifically
for test purposes and allows access to the actor in two ways: either by
obtaining a reference to the underlying actor instance, or by invoking or
querying the actor's behaviour (receive
). Each one warrants its own
section below.
注釈
It is highly recommended to stick to traditional behavioural testing (using messaging
to ask the Actor to reply with the state you want to run assertions against),
instead of using TestActorRef
whenever possible.
警告
Due to the synchronous nature of TestActorRef
it will not work with some support
traits that Akka provides as they require asynchronous behaviours to function properly.
Examples of traits that do not mix well with test actor refs are PersistentActor
and AtLeastOnceDelivery provided by Akka Persistence.
Obtaining a Reference to an Actor
Having access to the actual Actor
object allows application of all
traditional unit testing techniques on the contained methods. Obtaining a
reference is done like this:
static class MyActor extends UntypedActor {
public void onReceive(Object o) throws Exception {
if (o.equals("say42")) {
getSender().tell(42, getSelf());
} else if (o instanceof Exception) {
throw (Exception) o;
}
}
public boolean testMe() { return true; }
}
@Test
public void demonstrateTestActorRef() {
final Props props = Props.create(MyActor.class);
final TestActorRef<MyActor> ref = TestActorRef.create(system, props, "testA");
final MyActor actor = ref.underlyingActor();
assertTrue(actor.testMe());
}
Since TestActorRef
is generic in the actor type it returns the
underlying actor with its proper static type. From this point on you may bring
any unit testing tool to bear on your actor as usual.
Testing the Actor's Behavior
When the dispatcher invokes the processing behavior of an actor on a message,
it actually calls apply
on the current behavior registered for the
actor. This starts out with the return value of the declared receive
method, but it may also be changed using become
and unbecome
in
response to external messages. All of this contributes to the overall actor
behavior and it does not lend itself to easy testing on the Actor
itself. Therefore the TestActorRef
offers a different mode of
operation to complement the Actor
testing: it supports all operations
also valid on normal ActorRef
. Messages sent to the actor are
processed synchronously on the current thread and answers may be sent back as
usual. This trick is made possible by the CallingThreadDispatcher
described below (see CallingThreadDispatcher); this dispatcher is set
implicitly for any actor instantiated into a TestActorRef
.
final Props props = Props.create(MyActor.class);
final TestActorRef<MyActor> ref = TestActorRef.create(system, props, "testB");
final Future<Object> future = akka.pattern.Patterns.ask(ref, "say42", 3000);
assertTrue(future.isCompleted());
assertEquals(42, Await.result(future, Duration.Zero()));
As the TestActorRef
is a subclass of LocalActorRef
with a few
special extras, also aspects like supervision and restarting work properly, but
beware that execution is only strictly synchronous as long as all actors
involved use the CallingThreadDispatcher
. As soon as you add elements
which include more sophisticated scheduling you leave the realm of unit testing
as you then need to think about asynchronicity again (in most cases the problem
will be to wait until the desired effect had a chance to happen).
One more special aspect which is overridden for single-threaded tests is the
receiveTimeout
, as including that would entail asynchronous queuing of
ReceiveTimeout
messages, violating the synchronous contract.
注釈
To summarize: TestActorRef
overwrites two fields: it sets the
dispatcher to CallingThreadDispatcher.global
and it sets the
receiveTimeout
to None.
The Way In-Between: Expecting Exceptions
If you want to test the actor behavior, including hotswapping, but without
involving a dispatcher and without having the TestActorRef
swallow
any thrown exceptions, then there is another mode available for you: just use
the receive
method on TestActorRef
, which will be forwarded to the
underlying actor:
final Props props = Props.create(MyActor.class);
final TestActorRef<MyActor> ref = TestActorRef.create(system, props, "myActor");
try {
ref.receive(new Exception("expected"));
fail("expected an exception to be thrown");
} catch (Exception e) {
assertEquals("expected", e.getMessage());
}
Use Cases
You may of course mix and match both modi operandi of TestActorRef
as
suits your test needs:
- one common use case is setting up the actor into a specific internal state before sending the test message
- another is to verify correct internal state transitions after having sent the test message
Feel free to experiment with the possibilities, and if you find useful patterns, don't hesitate to let the Akka forums know about them! Who knows, common operations might even be worked into nice DSLs.
Asynchronous Integration Testing with JavaTestKit
When you are reasonably sure that your actor's business logic is correct, the next step is verifying that it works correctly within its intended environment. The definition of the environment depends of course very much on the problem at hand and the level at which you intend to test, ranging for functional/integration tests to full system tests. The minimal setup consists of the test procedure, which provides the desired stimuli, the actor under test, and an actor receiving replies. Bigger systems replace the actor under test with a network of actors, apply stimuli at varying injection points and arrange results to be sent from different emission points, but the basic principle stays the same in that a single procedure drives the test.
The JavaTestKit
class contains a collection of tools which makes this
common task easy.
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.actor.UntypedActor;
import akka.testkit.JavaTestKit;
import scala.concurrent.duration.Duration;
public class TestKitSampleTest {
public static class SomeActor extends UntypedActor {
ActorRef target = null;
public void onReceive(Object msg) {
if (msg.equals("hello")) {
getSender().tell("world", getSelf());
if (target != null) target.forward(msg, getContext());
} else if (msg instanceof ActorRef) {
target = (ActorRef) msg;
getSender().tell("done", getSelf());
}
}
}
static ActorSystem system;
@BeforeClass
public static void setup() {
system = ActorSystem.create();
}
@AfterClass
public static void teardown() {
JavaTestKit.shutdownActorSystem(system);
system = null;
}
@Test
public void testIt() {
/*
* Wrap the whole test procedure within a testkit constructor
* if you want to receive actor replies or use Within(), etc.
*/
new JavaTestKit(system) {{
final Props props = Props.create(SomeActor.class);
final ActorRef subject = system.actorOf(props);
// can also use JavaTestKit “from the outside”
final JavaTestKit probe = new JavaTestKit(system);
// “inject” the probe by passing it to the test subject
// like a real resource would be passed in production
subject.tell(probe.getRef(), getRef());
// await the correct response
expectMsgEquals(duration("1 second"), "done");
// the run() method needs to finish within 3 seconds
new Within(duration("3 seconds")) {
protected void run() {
subject.tell("hello", getRef());
// This is a demo: would normally use expectMsgEquals().
// Wait time is bounded by 3-second deadline above.
new AwaitCond() {
protected boolean cond() {
return probe.msgAvailable();
}
};
// response must have been enqueued to us before probe
expectMsgEquals(Duration.Zero(), "world");
// check that the probe we injected earlier got the msg
probe.expectMsgEquals(Duration.Zero(), "hello");
Assert.assertEquals(getRef(), probe.getLastSender());
// Will wait for the rest of the 3 seconds
expectNoMsg();
}
};
}};
}
}
The JavaTestKit
contains an actor named testActor
which is the
entry point for messages to be examined with the various expectMsg...
assertions detailed below. The test actor’s reference is obtained using the
getRef
method as demonstrated above. The testActor
may also
be passed to other actors as usual, usually subscribing it as notification
listener. There is a whole set of examination methods, e.g. receiving all
consecutive messages matching certain criteria, receiving a whole sequence of
fixed messages or classes, receiving nothing for some time, etc.
The ActorSystem passed in to the constructor of JavaTestKit is accessible via the
getSystem
method.
注釈
Remember to shut down the actor system after the test is finished (also in case of failure) so that all actors—including the test actor—are stopped.
Built-In Assertions
The above mentioned expectMsgEquals
is not the only method for
formulating assertions concerning received messages, the full set is this:
final String hello = expectMsgEquals("hello");
final Object any = expectMsgAnyOf("hello", "world");
final Object[] all = expectMsgAllOf("hello", "world");
final int i = expectMsgClass(Integer.class);
final Number j = expectMsgAnyClassOf(Integer.class, Long.class);
expectNoMsg();
final Object[] two = receiveN(2);
In these examples, the maximum durations you will find mentioned below are left
out, in which case they use the default value from configuration item
akka.test.single-expect-default
which itself defaults to 3 seconds (or they
obey the innermost enclosing Within
as detailed below). The full signatures are:
public <T> T expectMsgEquals(Duration max, T msg)
The given message object must be received within the specified time; the object will be returned.
public Object expectMsgAnyOf(Duration max, Object... msg)
An object must be received within the given time, and it must be equal (compared with
equals()
) to at least one of the passed reference objects; the received object will be returned.
public Object[] expectMsgAllOf(Duration max, Object... msg)
A number of objects matching the size of the supplied object array must be received within the given time, and for each of the given objects there must exist at least one among the received ones which equals it (compared with
equals()
). The full sequence of received objects is returned in the order received.
public <T> T expectMsgClass(Duration max, Class<T> c)
An object which is an instance of the given
Class
must be received within the allotted time frame; the object will be returned. Note that this does a conformance check, if you need the class to be equal you need to verify that afterwards.
public <T> T expectMsgAnyClassOf(Duration max, Class<? extends T>... c)
An object must be received within the given time, and it must be an instance of at least one of the supplied
Class
objects; the received object will be returned. Note that this does a conformance check, if you need the class to be equal you need to verify that afterwards.注釈
Because of a limitation in Java’s type system it may be necessary to add
@SuppressWarnings("unchecked")
when using this method.
public void expectNoMsg(Duration max)
No message must be received within the given time. This also fails if a message has been received before calling this method which has not been removed from the queue using one of the other methods.
Object[] receiveN(int n, Duration max)
n
messages must be received within the given time; the received messages are returned.
For cases which require more refined conditions there are constructs which take code blocks:
ExpectMsg<T>
new JavaTestKit(system) {{ getRef().tell(42, ActorRef.noSender()); final String out = new ExpectMsg<String>("match hint") { // do not put code outside this method, will run afterwards protected String match(Object in) { if (in instanceof Integer) { return "match"; } else { throw noMatch(); } } }.get(); // this extracts the received message assertEquals("match", out); }};The
match(Object in)
method will be evaluated once a message has been received within the allotted time (which may be given as constructor argument). If it throwsnoMatch()
(where it is sufficient to call that method; thethrow
keyword is only needed in cases where the compiler would otherwise complain about wrong return types—Java is lacking Scala’s notion of a type which signifies “will not ever return normally”), then the expectation fails with anAssertionError
, otherwise the matched and possibly transformed object is stored for retrieval using theget
method.ReceiveWhile<T>
new JavaTestKit(system) {{ getRef().tell(42, ActorRef.noSender()); getRef().tell(43, ActorRef.noSender()); getRef().tell("hello", ActorRef.noSender()); final String[] out = new ReceiveWhile<String>(String.class, duration("1 second")) { // do not put code outside this method, will run afterwards protected String match(Object in) { if (in instanceof Integer) { return in.toString(); } else { throw noMatch(); } } }.get(); // this extracts the received messages assertArrayEquals(new String[] {"42", "43"}, out); expectMsgEquals("hello"); }};This construct works like ExpectMsg, but it continually collects messages as long as they match the criteria, and it does not fail when a non-matching one is encountered. Collecting messages also ends when the time is up, when too much time passes between messages or when enough messages have been received.
new ReceiveWhile<String>( // type of array to be created must match ... String.class, // ... this class which is needed to that end duration("100 millis"), // maximum collect time duration("50 millis"), // maximum time between messages 12 // maximum number of messages to collect ) { // match elided ... };The need to specify the
String
result type twice results from the need to create a correctly typed array and Java’s inability to infer the class’s type argument.AwaitCond
new JavaTestKit(system) {{ getRef().tell(42, ActorRef.noSender()); new AwaitCond( duration("1 second"), // maximum wait time duration("100 millis") // interval at which to check the condition ) { // do not put code outside this method, will run afterwards protected boolean cond() { // typically used to wait for something to start up return msgAvailable(); } }; }};This general construct is not connected with the test kit’s message reception, the embedded condition can compute the boolean result from anything in scope.
- AwaitAssert
new JavaTestKit(system) {{ getRef().tell(42, ActorRef.noSender()); new AwaitAssert( duration("1 second"), // maximum wait time duration("100 millis") // interval at which to check the condition ) { // do not put code outside this method, will run afterwards protected void check() { assertEquals(msgAvailable(), true); } }; }};This general construct is not connected with the test kit’s message reception, the embedded assert can check anything in scope.
There are also cases where not all messages sent to the test kit are actually relevant to the test, but removing them would mean altering the actors under test. For this purpose it is possible to ignore certain messages:
IgnoreMsg
new JavaTestKit(system) {{ // ignore all Strings new IgnoreMsg() { protected boolean ignore(Object msg) { return msg instanceof String; } }; getRef().tell("hello", ActorRef.noSender()); getRef().tell(42, ActorRef.noSender()); expectMsgEquals(42); // remove message filter ignoreNoMsg(); getRef().tell("hello", ActorRef.noSender()); expectMsgEquals("hello"); }};
Expecting Log Messages
Since an integration test does not allow to the internal processing of the
participating actors, verifying expected exceptions cannot be done directly.
Instead, use the logging system for this purpose: replacing the normal event
handler with the TestEventListener
and using an EventFilter
allows assertions on log messages, including those which are generated by
exceptions:
new JavaTestKit(system) {{
assertEquals("TestKitDocTest", system.name());
final ActorRef victim = system.actorOf(Props.empty(), "victim");
final int result = new EventFilter<Integer>(ActorKilledException.class) {
protected Integer run() {
victim.tell(Kill.getInstance(), ActorRef.noSender());
return 42;
}
}.from("akka://TestKitDocTest/user/victim").occurrences(1).exec();
assertEquals(42, result);
}};
If a number of occurrences is specific—as demonstrated above—then exec()
will block until that number of matching messages have been received or the
timeout configured in akka.test.filter-leeway
is used up (time starts
counting after the run()
method returns). In case of a timeout the test
fails.
注釈
Be sure to exchange the default logger with the
TestEventListener
in your application.conf
to enable this
function:
akka.loggers = [akka.testkit.TestEventListener]
Timing Assertions
Another important part of functional testing concerns timing: certain events must not happen immediately (like a timer), others need to happen before a deadline. Therefore, all examination methods accept an upper time limit within the positive or negative result must be obtained. Lower time limits need to be checked external to the examination, which is facilitated by a new construct for managing time constraints:
new JavaTestKit(system) {{
getRef().tell(42, ActorRef.noSender());
new Within(Duration.Zero(), Duration.create(1, "second")) {
// do not put code outside this method, will run afterwards
public void run() {
assertEquals((Integer) 42, expectMsgClass(Integer.class));
}
};
}};
The block in Within.run
must complete after a Duration which
is between min
and max
, where the former defaults to zero. The
deadline calculated by adding the max
parameter to the block's start
time is implicitly available within the block to all examination methods, if
you do not specify it, it is inherited from the innermost enclosing
within
block.
It should be noted that if the last message-receiving assertion of the block is
expectNoMsg
or receiveWhile
, the final check of the
within
is skipped in order to avoid false positives due to wake-up
latencies. This means that while individual contained assertions still use the
maximum time bound, the overall block may take arbitrarily longer in this case.
注釈
All times are measured using System.nanoTime
, meaning that they describe
wall time, not CPU time or system time.
Accounting for Slow Test Systems
The tight timeouts you use during testing on your lightning-fast notebook will
invariably lead to spurious test failures on the heavily loaded Jenkins server
(or similar). To account for this situation, all maximum durations are
internally scaled by a factor taken from the 環境設定,
akka.test.timefactor
, which defaults to 1.
You can scale other durations with the same factor by using dilated
method
in JavaTestKit
.
new JavaTestKit(system) {{
final Duration original = duration("1 second");
final Duration stretched = dilated(original);
assertTrue("dilated", stretched.gteq(original));
}};
Using Multiple Probe Actors
When the actors under test are supposed to send various messages to different
destinations, it may be difficult distinguishing the message streams arriving
at the testActor
when using the JavaTestKit
as shown until now.
Another approach is to use it for creation of simple probe actors to be
inserted in the message flows. The functionality is best explained using a
small example:
new JavaTestKit(system) {{
// simple actor which just forwards messages
class Forwarder extends UntypedActor {
final ActorRef target;
@SuppressWarnings("unused")
public Forwarder(ActorRef target) {
this.target = target;
}
public void onReceive(Object msg) {
target.forward(msg, getContext());
}
}
// create a test probe
final JavaTestKit probe = new JavaTestKit(system);
// create a forwarder, injecting the probe’s testActor
final Props props = Props.create(Forwarder.class, this, probe.getRef());
final ActorRef forwarder = system.actorOf(props, "forwarder");
// verify correct forwarding
forwarder.tell(42, getRef());
probe.expectMsgEquals(42);
assertEquals(getRef(), probe.getLastSender());
}};
This simple test verifies an equally simple Forwarder actor by injecting a
probe as the forwarder’s target. Another example would be two actors A and B
which collaborate by A sending messages to B. In order to verify this message
flow, a TestProbe
could be inserted as target of A, using the
forwarding capabilities or auto-pilot described below to include a real B in
the test setup.
If you have many test probes, you can name them to get meaningful actor names in test logs and assertions:
new JavaTestKit(system) {{
final TestProbe worker = new TestProbe(system, "worker");
final TestProbe aggregator = new TestProbe(system, "aggregator");
assertTrue(worker.ref().path().name().startsWith("worker"));
assertTrue(aggregator.ref().path().name().startsWith("aggregator"));
}};
Probes may also be equipped with custom assertions to make your test code even more concise and clear:
new JavaTestKit(system) {{
class MyProbe extends JavaTestKit {
public MyProbe() {
super(system);
}
public void assertHello() {
expectMsgEquals("hello");
}
}
final MyProbe probe = new MyProbe();
probe.getRef().tell("hello", ActorRef.noSender());
probe.assertHello();
}};
You have complete flexibility here in mixing and matching the
JavaTestKit
facilities with your own checks and choosing an intuitive
name for it. In real life your code will probably be a bit more complicated
than the example given above; just use the power!
警告
Any message send from a TestProbe
to another actor which runs on the
CallingThreadDispatcher runs the risk of dead-lock, if that other actor might
also send to this probe. The implementation of TestProbe.watch
and
TestProbe.unwatch
will also send a message to the watchee, which
means that it is dangerous to try watching e.g. TestActorRef
from a
TestProbe
.
Watching Other Actors from Probes
A JavaTestKit
can register itself for DeathWatch of any other actor:
new JavaTestKit(system) {{
final JavaTestKit probe = new JavaTestKit(system);
probe.watch(target);
target.tell(PoisonPill.getInstance(), ActorRef.noSender());
final Terminated msg = probe.expectMsgClass(Terminated.class);
assertEquals(msg.getActor(), target);
}};
Replying to Messages Received by Probes
The probe stores the sender of the last dequeued message (i.e. after its
expectMsg*
reception), which may be retrieved using the
getLastSender
method. This information can also implicitly be used
for having the probe reply to the last received message:
new JavaTestKit(system) {{
final JavaTestKit probe = new JavaTestKit(system);
probe.getRef().tell("hello", getRef());
probe.expectMsgEquals("hello");
probe.reply("world");
expectMsgEquals("world");
assertEquals(probe.getRef(), getLastSender());
}};
Forwarding Messages Received by Probes
The probe can also forward a received message (i.e. after its expectMsg*
reception), retaining the original sender:
new JavaTestKit(system) {{
final JavaTestKit probe = new JavaTestKit(system);
probe.getRef().tell("hello", getRef());
probe.expectMsgEquals("hello");
probe.forward(getRef());
expectMsgEquals("hello");
assertEquals(getRef(), getLastSender());
}};
Auto-Pilot
Receiving messages in a queue for later inspection is nice, but in order to
keep a test running and verify traces later you can also install an
AutoPilot
in the participating test probes (actually in any
TestKit
) which is invoked before enqueueing to the inspection queue.
This code can be used to forward messages, e.g. in a chain A --> Probe -->
B
, as long as a certain protocol is obeyed.
new JavaTestKit(system) {{
final JavaTestKit probe = new JavaTestKit(system);
// install auto-pilot
probe.setAutoPilot(new TestActor.AutoPilot() {
public AutoPilot run(ActorRef sender, Object msg) {
sender.tell(msg, ActorRef.noSender());
return noAutoPilot();
}
});
// first one is replied to directly ...
probe.getRef().tell("hello", getRef());
expectMsgEquals("hello");
// ... but then the auto-pilot switched itself off
probe.getRef().tell("world", getRef());
expectNoMsg();
}};
The run
method must return the auto-pilot for the next message, wrapped
in an Option
; setting it to None
terminates the auto-pilot.
Caution about Timing Assertions
The behavior of within
blocks when using test probes might be perceived
as counter-intuitive: you need to remember that the nicely scoped deadline as
described above is local to each probe. Hence, probes
do not react to each other's deadlines or to the deadline set in an enclosing
JavaTestKit
instance:
new JavaTestKit(system) {{
final JavaTestKit probe = new JavaTestKit(system);
new Within(duration("1 second")) {
public void run() {
probe.expectMsgEquals("hello");
}
};
}};
Here, the expectMsgEquals
call will use the default timeout.
Testing parent-child relationships
The parent of an actor is always the actor that created it. At times this leads to a coupling between the two that may not be straightforward to test. There are several approaches to improve testability of a child actor that needs to refer to its parent:
- when creating a child, pass an explicit reference to its parent
- create the child with a
TestProbe
as parent - create a fabricated parent when testing
Conversely, a parent's binding to its child can be lessened as follows:
- when creating a parent, tell the parent how to create its child
For example, the structure of the code you want to test may follow this pattern:
static class Parent extends UntypedActor {
final ActorRef child = context().actorOf(Props.create(Child.class), "child");
boolean ponged = false;
@Override public void onReceive(Object message) throws Exception {
if ("pingit".equals(message)) {
child.tell("ping", self());
} else if ("pong".equals(message)) {
ponged = true;
} else {
unhandled(message);
}
}
}
static class Child extends UntypedActor {
@Override public void onReceive(Object message) throws Exception {
if ("ping".equals(message)) {
context().parent().tell("pong", self());
} else {
unhandled(message);
}
}
}
Introduce child to its parent
The first option is to avoid use of the context.parent
function and create
a child with a custom parent by passing an explicit reference to its parent instead.
class DependentChild extends UntypedActor {
private final ActorRef parent;
public DependentChild(ActorRef parent) {
this.parent = parent;
}
@Override public void onReceive(Object message) throws Exception {
if ("ping".equals(message)) {
parent.tell("pong", self());
} else {
unhandled(message);
}
}
}
Create the child using JavaTestKit
The JavaTestKit
class can in fact create actors that will run with the test probe as parent.
This will cause any messages the the child actor sends to context().getParent() to
end up in the test probe.
JavaTestKit parent = new JavaTestKit(system);
ActorRef child = parent.childActorOf(Props.create(Child.class));
parent.send(child, "ping");
parent.expectMsgEquals("pong");
Using a fabricated parent
If you prefer to avoid modifying the child constructor you can create a fabricated parent in your test. This, however, does not enable you to test the parent actor in isolation.
class FabricatedParentCreator implements Creator<Actor> {
private final TestProbe proxy;
public FabricatedParentCreator(TestProbe proxy) {
this.proxy = proxy;
}
@Override public Actor create() throws Exception {
return new UntypedActor() {
final ActorRef child = context().actorOf(Props.create(Child.class), "child");
@Override public void onReceive(Object x) throws Exception {
if (sender().equals(child)) {
proxy.ref().forward(x, context());
} else {
child.forward(x, context());
}
}
};
}
}
TestProbe proxy = new TestProbe(system);
ActorRef parent = system.actorOf(Props.create(new FabricatedParentCreator(proxy)));
proxy.send(parent, "ping");
proxy.expectMsg("pong");
Externalize child making from the parent
Alternatively, you can tell the parent how to create its child. There are two ways
to do this: by giving it a Props
object or by giving it a function which takes care of creating the child actor:
class DependentParent extends UntypedActor {
final ActorRef child;
boolean ponged = false;
public DependentParent(Props childProps) {
child = context().actorOf(childProps, "child");
}
@Override public void onReceive(Object message) throws Exception {
if ("pingit".equals(message)) {
child.tell("ping", self());
} else if ("pong".equals(message)) {
ponged = true;
} else {
unhandled(message);
}
}
}
class GenericDependentParent extends UntypedActor {
final ActorRef child;
boolean ponged = false;
public GenericDependentParent(Function<ActorRefFactory, ActorRef> childMaker)
throws Exception {
child = childMaker.apply(context());
}
@Override public void onReceive(Object message) throws Exception {
if ("pingit".equals(message)) {
child.tell("ping", self());
} else if ("pong".equals(message)) {
ponged = true;
} else {
unhandled(message);
}
}
}
Creating the Actor
is straightforward and the function may look like this in your test code:
Function<ActorRefFactory, ActorRef> maker = new Function<ActorRefFactory, ActorRef>() {
@Override public ActorRef apply(ActorRefFactory param) throws Exception {
return probe.ref();
}
};
ActorRef parent = system.actorOf(Props.create(GenericDependentParent.class, maker));
And like this in your application code:
Function<ActorRefFactory, ActorRef> maker = new Function<ActorRefFactory, ActorRef>() {
@Override public ActorRef apply(ActorRefFactory f) throws Exception {
return f.actorOf(Props.create(Child.class));
}
};
ActorRef parent = system.actorOf(Props.create(GenericDependentParent.class, maker));
Which of these methods is the best depends on what is most important to test. The most generic option is to create the parent actor by passing it a function that is responsible for the Actor creation, but using TestProbe or having a fabricated parent is often sufficient.
CallingThreadDispatcher
The CallingThreadDispatcher
serves good purposes in unit testing, as
described above, but originally it was conceived in order to allow contiguous
stack traces to be generated in case of an error. As this special dispatcher
runs everything which would normally be queued directly on the current thread,
the full history of a message's processing chain is recorded on the call stack,
so long as all intervening actors run on this dispatcher.
How to use it
Just set the dispatcher as you normally would:
system.actorOf(
Props.create(MyActor.class)
.withDispatcher(CallingThreadDispatcher.Id()));
How it works
When receiving an invocation, the CallingThreadDispatcher
checks
whether the receiving actor is already active on the current thread. The
simplest example for this situation is an actor which sends a message to
itself. In this case, processing cannot continue immediately as that would
violate the actor model, so the invocation is queued and will be processed when
the active invocation on that actor finishes its processing; thus, it will be
processed on the calling thread, but simply after the actor finishes its
previous work. In the other case, the invocation is simply processed
immediately on the current thread. Futures scheduled via this dispatcher are
also executed immediately.
This scheme makes the CallingThreadDispatcher
work like a general
purpose dispatcher for any actors which never block on external events.
In the presence of multiple threads it may happen that two invocations of an actor running on this dispatcher happen on two different threads at the same time. In this case, both will be processed directly on their respective threads, where both compete for the actor's lock and the loser has to wait. Thus, the actor model is left intact, but the price is loss of concurrency due to limited scheduling. In a sense this is equivalent to traditional mutex style concurrency.
The other remaining difficulty is correct handling of suspend and resume: when
an actor is suspended, subsequent invocations will be queued in thread-local
queues (the same ones used for queuing in the normal case). The call to
resume
, however, is done by one specific thread, and all other threads
in the system will probably not be executing this specific actor, which leads
to the problem that the thread-local queues cannot be emptied by their native
threads. Hence, the thread calling resume
will collect all currently
queued invocations from all threads into its own queue and process them.
Limitations
警告
In case the CallingThreadDispatcher is used for top-level actors, but without going through TestActorRef, then there is a time window during which the actor is awaiting construction by the user guardian actor. Sending messages to the actor during this time period will result in them being enqueued and then executed on the guardian’s thread instead of the caller’s thread. To avoid this, use TestActorRef.
If an actor's behavior blocks on a something which would normally be affected
by the calling actor after having sent the message, this will obviously
dead-lock when using this dispatcher. This is a common scenario in actor tests
based on CountDownLatch
for synchronization:
val latch = new CountDownLatch(1)
actor ! startWorkAfter(latch) // actor will call latch.await() before proceeding
doSomeSetupStuff()
latch.countDown()
The example would hang indefinitely within the message processing initiated on the second line and never reach the fourth line, which would unblock it on a normal dispatcher.
Thus, keep in mind that the CallingThreadDispatcher
is not a
general-purpose replacement for the normal dispatchers. On the other hand it
may be quite useful to run your actor network on it for testing, because if it
runs without dead-locking chances are very high that it will not dead-lock in
production.
警告
The above sentence is unfortunately not a strong guarantee, because your
code might directly or indirectly change its behavior when running on a
different dispatcher. If you are looking for a tool to help you debug
dead-locks, the CallingThreadDispatcher
may help with certain error
scenarios, but keep in mind that it has may give false negatives as well as
false positives.
Thread Interruptions
If the CallingThreadDispatcher sees that the current thread has its
isInterrupted()
flag set when message processing returns, it will throw an
InterruptedException
after finishing all its processing (i.e. all
messages which need processing as described above are processed before this
happens). As tell
cannot throw exceptions due to its contract, this
exception will then be caught and logged, and the thread’s interrupted status
will be set again.
If during message processing an InterruptedException
is thrown then it
will be caught inside the CallingThreadDispatcher’s message handling loop, the
thread’s interrupted flag will be set and processing continues normally.
注釈
The summary of these two paragraphs is that if the current thread is
interrupted while doing work under the CallingThreadDispatcher, then that
will result in the isInterrupted
flag to be true
when the message
send returns and no InterruptedException
will be thrown.
Benefits
To summarize, these are the features with the CallingThreadDispatcher
has to offer:
- Deterministic execution of single-threaded tests while retaining nearly full actor semantics
- Full message processing history leading up to the point of failure in exception stack traces
- Exclusion of certain classes of dead-lock scenarios
Tracing Actor Invocations
The testing facilities described up to this point were aiming at formulating assertions about a system’s behavior. If a test fails, it is usually your job to find the cause, fix it and verify the test again. This process is supported by debuggers as well as logging, where the Akka toolkit offers the following options:
Logging of exceptions thrown within Actor instances
This is always on; in contrast to the other logging mechanisms, this logs at
ERROR
level.Logging of special messages
Actors handle certain special messages automatically, e.g.
Kill
,PoisonPill
, etc. Tracing of these message invocations is enabled by the settingakka.actor.debug.autoreceive
, which enables this on all actors.Logging of the actor lifecycle
Actor creation, start, restart, monitor start, monitor stop and stop may be traced by enabling the setting
akka.actor.debug.lifecycle
; this, too, is enabled uniformly on all actors.
All these messages are logged at DEBUG
level. To summarize, you can enable
full logging of actor activities using this configuration fragment:
akka {
loglevel = "DEBUG"
actor {
debug {
autoreceive = on
lifecycle = on
}
}
}
Configuration
There are several configuration properties for the TestKit module, please refer to the reference configuration.
Contents