Merge pull request #16192 from ktoso/doc-push-over-finish-line-actor-child-testing-ktoso

=doc #13043 Adding section on testing parent-child relationships
This commit is contained in:
Konrad Malawski 2014-11-03 10:01:10 +01:00
commit 7dfeda0824
4 changed files with 446 additions and 0 deletions

View file

@ -0,0 +1,202 @@
/**
* Copyright (C) 2014 Typesafe Inc. <http://www.typesafe.com>
*/
package docs.testkit;
import static org.junit.Assert.*;
import akka.actor.*;
import akka.japi.Creator;
import akka.japi.Function;
import akka.testkit.AkkaJUnitActorSystemResource;
import akka.testkit.TestActorRef;
import akka.testkit.TestProbe;
import com.typesafe.config.ConfigFactory;
import org.junit.ClassRule;
import org.junit.Test;
public class ParentChildTest {
@ClassRule
public static AkkaJUnitActorSystemResource actorSystemResource =
new AkkaJUnitActorSystemResource("TestKitDocTest",
ConfigFactory.parseString("akka.loggers = [akka.testkit.TestEventListener]"));
private final ActorSystem system = actorSystemResource.getSystem();
//#test-example
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);
}
}
}
//#test-example
static
//#test-dependentchild
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);
}
}
}
//#test-dependentchild
static
//#test-dependentparent
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);
}
}
}
//#test-dependentparent
static
//#test-dependentparent-generic
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);
}
}
}
//#test-dependentparent-generic
@Test
public void testingWithoutParent() {
TestProbe probe = new TestProbe(system);
ActorRef child = system.actorOf(Props.create(DependentChild.class, probe.ref()));
probe.send(child, "ping");
probe.expectMsg("pong");
}
@Test
public void testingWithCustomProps() {
TestProbe probe = new TestProbe(system);
Props childProps = Props.create(MockedChild.class);
TestActorRef<DependentParent> parent = TestActorRef.create(system, Props.create(DependentParent.class, childProps));
probe.send(parent, "pingit");
// test some parent state change
assertTrue(parent.underlyingActor().ponged == true || parent.underlyingActor().ponged == false);
}
@Test
public void testingWithChildProbe() throws Exception {
final TestProbe probe = new TestProbe(system);
//#child-maker-test
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));
//#child-maker-test
probe.send(parent, "pingit");
probe.expectMsg("ping");
}
public void exampleProdActorFactoryFunction() throws Exception {
//#child-maker-prod
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));
//#child-maker-prod
}
static
//#test-fabricated-parent-creator
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());
}
}
};
}
}
//#test-fabricated-parent-creator
@Test
public void fabricatedParentTestsItsChildResponses() throws Exception {
// didn't put final on these in order to make the parent fit in one line in the html docs
//#test-fabricated-parent
TestProbe proxy = new TestProbe(system);
ActorRef parent = system.actorOf(Props.create(new FabricatedParentCreator(proxy)));
proxy.send(parent, "ping");
proxy.expectMsg("pong");
//#test-fabricated-parent
}
}

View file

@ -462,6 +462,58 @@ do not react to each other's deadlines or to the deadline set in an enclosing
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.
Broadly, there are three approaches to improve testability of parent-child
relationships:
1. when creating a child, pass an explicit reference to its parent
2. when creating a parent, tell the parent how to create its child
3. create a fabricated parent when testing
For example, the structure of the code you want to test may follow this pattern:
.. includecode:: code/docs/testkit/ParentChildTest.java#test-example
Using dependency-injection
^^^^^^^^^^^^^^^^^^^^^^^^^^
The first option is to avoid use of the :meth:`context.parent` function and create
a child with a custom parent by passing an explicit reference to its parent instead.
.. includecode:: code/docs/testkit/ParentChildTest.java#test-dependentchild
Alternatively, you can tell the parent how to create its child. There are two ways
to do this: by giving it a :class:`Props` object or by giving it a function which takes care of creating the child actor:
.. includecode:: code/docs/testkit/ParentChildTest.java#test-dependentparent
.. includecode:: code/docs/testkit/ParentChildTest.java#test-dependentparent-generic
Creating the :class:`Actor` is straightforward and the function may look like this in your test code:
.. includecode:: code/docs/testkit/ParentChildTest.java#child-maker-test
And like this in your application code:
.. includecode:: code/docs/testkit/ParentChildTest.java#child-maker-prod
Using a fabricated parent
^^^^^^^^^^^^^^^^^^^^^^^^^
If you prefer to avoid modifying the parent or child constructor you can
create a fabricated parent in your test. This, however, does not enable you to test
the parent actor in isolation.
.. includecode:: code/docs/testkit/ParentChildTest.java#test-fabricated-parent-creator
.. includecode:: code/docs/testkit/ParentChildTest.java#test-fabricated-parent
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 the fabricated parent is often sufficient.
.. _Java-CallingThreadDispatcher:
CallingThreadDispatcher

View file

@ -0,0 +1,135 @@
package docs.testkit
import org.scalatest.WordSpec
import org.scalatest.Matchers
import akka.testkit.TestKitBase
import akka.actor.ActorSystem
import akka.actor.Props
import akka.actor.Actor
import akka.actor.ActorRef
import akka.testkit.ImplicitSender
import akka.testkit.TestProbe
import akka.testkit.TestActorRef
import akka.actor.ActorRefFactory
/**
* Parent-Child examples
*/
//#test-example
class Parent extends Actor {
val child = context.actorOf(Props[Child], "child")
var ponged = false
def receive = {
case "pingit" => child ! "ping"
case "pong" => ponged = true
}
}
class Child extends Actor {
def receive = {
case "ping" => context.parent ! "pong"
}
}
//#test-example
//#test-dependentchild
class DependentChild(parent: ActorRef) extends Actor {
def receive = {
case "ping" => parent ! "pong"
}
}
//#test-dependentchild
//#test-dependentparent
class DependentParent(childProps: Props) extends Actor {
val child = context.actorOf(childProps, "child")
var ponged = false
def receive = {
case "pingit" => child ! "ping"
case "pong" => ponged = true
}
}
class GenericDependentParent(childMaker: ActorRefFactory => ActorRef) extends Actor {
val child = childMaker(context)
var ponged = false
def receive = {
case "pingit" => child ! "ping"
case "pong" => ponged = true
}
}
//#test-dependentparent
/**
* Test specification
*/
class MockedChild extends Actor {
def receive = {
case "ping" => sender ! "pong"
}
}
class ParentChildSpec extends WordSpec with Matchers with TestKitBase {
implicit lazy val system = ActorSystem()
"A DependentChild" should {
"be tested without its parent" in {
val probe = TestProbe()
val child = system.actorOf(Props(classOf[DependentChild], probe.ref))
probe.send(child, "ping")
probe.expectMsg("pong")
}
}
"A DependentParent" should {
"be tested with custom props" in {
val probe = TestProbe()
val parent = TestActorRef(new DependentParent(Props[MockedChild]))
probe.send(parent, "pingit")
// test some parent state change
parent.underlyingActor.ponged should (be(true) or be(false))
}
}
"A GenericDependentParent" should {
"be tested with a child probe" in {
val probe = TestProbe()
//#child-maker-test
val maker = (_: ActorRefFactory) => probe.ref
val parent = system.actorOf(Props(classOf[GenericDependentParent], maker))
//#child-maker-test
probe.send(parent, "pingit")
probe.expectMsg("ping")
}
"demonstrate production version of child creator" in {
//#child-maker-prod
val maker = (f: ActorRefFactory) => f.actorOf(Props[Child])
val parent = system.actorOf(Props(classOf[GenericDependentParent], maker))
//#child-maker-prod
}
}
//#test-fabricated-parent
"A fabricated parent" should {
"test its child responses" in {
val proxy = TestProbe()
val parent = system.actorOf(Props(new Actor {
val child = context.actorOf(Props[Child], "child")
def receive = {
case x if sender == child => proxy.ref forward x
case x => child forward x
}
}))
proxy.send(parent, "ping")
proxy.expectMsg("pong")
}
}
//#test-fabricated-parent
}

View file

@ -526,6 +526,63 @@ do not react to each other's deadlines or to the deadline set in an enclosing
Here, the ``expectMsg`` 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.
Broadly, there are three approaches to improve testability of parent-child
relationships:
1. when creating a child, pass an explicit reference to its parent
2. when creating a parent, tell the parent how to create its child
3. create a fabricated parent when testing
For example, the structure of the code you want to test may follow this pattern:
.. includecode:: code/docs/testkit/ParentChildSpec.scala#test-example
Using dependency-injection
^^^^^^^^^^^^^^^^^^^^^^^^^^
The first option is to avoid use of the :meth:`context.parent` function and create
a child with a custom parent by passing an explicit reference to its parent instead.
.. includecode:: code/docs/testkit/ParentChildSpec.scala#test-dependentchild
Alternatively, you can tell the parent how to create its child. There are two ways
to do this: by giving it a :class:`Props` object or by giving it a function which takes care of creating the child actor:
.. includecode:: code/docs/testkit/ParentChildSpec.scala#test-dependentparent
Creating the :class:`Props` is straightforward and the function may look like this in your test code:
.. includecode:: code/docs/testkit/ParentChildSpec.scala#child-maker-test
And like this in your application code:
.. includecode:: code/docs/testkit/ParentChildSpec.scala#child-maker-prod
Using a fabricated parent
^^^^^^^^^^^^^^^^^^^^^^^^^
If you prefer to avoid modifying the parent or child constructor you can
create a fabricated parent in your test. This, however, does not enable you to test
the parent actor in isolation.
.. includecode:: code/docs/testkit/ParentChildSpec.scala#test-fabricated-parent
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 the fabricated parent is often sufficient.
.. includecode:: code/docs/testkit/ParentChildSpec.scala#test-fabricated-parent
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 the partial function,
but the fabricated parent is often sufficient.
.. _Scala-CallingThreadDispatcher:
CallingThreadDispatcher