From dd8c920280f62da0631f7ed2ca890548604eb3d2 Mon Sep 17 00:00:00 2001 From: Marcus Ljungblad Date: Tue, 17 Dec 2013 11:23:18 -0500 Subject: [PATCH] =doc #13043 Adding section on testing parent-child relationships --- .../code/docs/testkit/ParentChildTest.java | 202 ++++++++++++++++++ akka-docs/rst/java/testing.rst | 52 +++++ .../code/docs/testkit/ParentChildSpec.scala | 135 ++++++++++++ akka-docs/rst/scala/testing.rst | 57 +++++ 4 files changed, 446 insertions(+) create mode 100644 akka-docs/rst/java/code/docs/testkit/ParentChildTest.java create mode 100644 akka-docs/rst/scala/code/docs/testkit/ParentChildSpec.scala diff --git a/akka-docs/rst/java/code/docs/testkit/ParentChildTest.java b/akka-docs/rst/java/code/docs/testkit/ParentChildTest.java new file mode 100644 index 0000000000..8cc52c1be6 --- /dev/null +++ b/akka-docs/rst/java/code/docs/testkit/ParentChildTest.java @@ -0,0 +1,202 @@ +/** + * Copyright (C) 2014 Typesafe Inc. + */ +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 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 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 maker = new Function() { + @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 maker = new Function() { + @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 { + 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 + } + +} diff --git a/akka-docs/rst/java/testing.rst b/akka-docs/rst/java/testing.rst index 3c49e184d6..b54e88ff89 100644 --- a/akka-docs/rst/java/testing.rst +++ b/akka-docs/rst/java/testing.rst @@ -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 diff --git a/akka-docs/rst/scala/code/docs/testkit/ParentChildSpec.scala b/akka-docs/rst/scala/code/docs/testkit/ParentChildSpec.scala new file mode 100644 index 0000000000..e4f785e4c3 --- /dev/null +++ b/akka-docs/rst/scala/code/docs/testkit/ParentChildSpec.scala @@ -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 +} \ No newline at end of file diff --git a/akka-docs/rst/scala/testing.rst b/akka-docs/rst/scala/testing.rst index 0995320fbb..5cbaaa8010 100644 --- a/akka-docs/rst/scala/testing.rst +++ b/akka-docs/rst/scala/testing.rst @@ -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