diff --git a/akka-docs/rst/java/code/docs/testkit/ParentChildTest.java b/akka-docs/rst/java/code/docs/testkit/ParentChildTest.java index 1fcc747bc4..1408f1c033 100644 --- a/akka-docs/rst/java/code/docs/testkit/ParentChildTest.java +++ b/akka-docs/rst/java/code/docs/testkit/ParentChildTest.java @@ -4,14 +4,16 @@ 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.JavaTestKit; import akka.testkit.TestActorRef; import akka.testkit.TestProbe; + import com.typesafe.config.ConfigFactory; + import org.junit.ClassRule; import org.junit.Test; @@ -187,6 +189,17 @@ public class ParentChildTest { } //#test-fabricated-parent-creator + @Test + public void testProbeParentTest() throws Exception { + //#test-TestProbe-parent + JavaTestKit parent = new JavaTestKit(system); + ActorRef child = parent.childActorOf(Props.create(Child.class)); + + parent.send(child, "ping"); + parent.expectMsgEquals("pong"); + //#test-TestProbe-parent + } + @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 diff --git a/akka-docs/rst/java/testing.rst b/akka-docs/rst/java/testing.rst index 05f574c90d..20417ab6dd 100644 --- a/akka-docs/rst/java/testing.rst +++ b/akka-docs/rst/java/testing.rst @@ -470,25 +470,51 @@ 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: +There are several approaches to improve testability of a child actor that +needs to refer to its parent: 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 +2. create the child with a ``TestProbe`` as parent 3. create a fabricated parent when testing +Conversely, a parent's binding to its child can be lessened as follows: + +4. 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: .. includecode:: code/docs/testkit/ParentChildTest.java#test-example -Using dependency-injection -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Introduce child to its parent +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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 +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. + +.. includecode:: code/docs/testkit/ParentChildTest.java#test-TestProbe-parent + +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. + +.. includecode:: code/docs/testkit/ParentChildTest.java#test-fabricated-parent-creator +.. includecode:: code/docs/testkit/ParentChildTest.java#test-fabricated-parent + +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 :class:`Props` object or by giving it a function which takes care of creating the child actor: @@ -503,19 +529,10 @@ 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. +responsible for the Actor creation, but using TestProbe or having a fabricated parent is often sufficient. .. _Java-CallingThreadDispatcher: diff --git a/akka-docs/rst/scala/code/docs/testkit/ParentChildSpec.scala b/akka-docs/rst/scala/code/docs/testkit/ParentChildSpec.scala index 2219ca10ad..fa87dec678 100644 --- a/akka-docs/rst/scala/code/docs/testkit/ParentChildSpec.scala +++ b/akka-docs/rst/scala/code/docs/testkit/ParentChildSpec.scala @@ -121,6 +121,17 @@ class ParentChildSpec extends WordSpec with Matchers with TestKitBase with Befor } } + //#test-TestProbe-parent + "A TestProbe serving as parent" should { + "test its child responses" in { + val parent = TestProbe() + val child = parent.childActorOf(Props[Child]) + parent.send(child, "ping") + parent.expectMsg("pong") + } + } + //#test-TestProbe-parent + //#test-fabricated-parent "A fabricated parent" should { "test its child responses" in { diff --git a/akka-docs/rst/scala/testing.rst b/akka-docs/rst/scala/testing.rst index 7401614c50..7b9dcc8737 100644 --- a/akka-docs/rst/scala/testing.rst +++ b/akka-docs/rst/scala/testing.rst @@ -548,26 +548,51 @@ 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: +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: -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 +1. when creating a child, pass an explicit reference to its parent +2. create the child with a ``TestProbe`` as parent 3. create a fabricated parent when testing +Conversely, a parent's binding to its child can be lessened as follows: + +4. 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: .. includecode:: code/docs/testkit/ParentChildSpec.scala#test-example -Using dependency-injection -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Introduce child to its parent +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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 +Create the child using TestProbe +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``TestProbe`` 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.parent` to +end up in the test probe. + +.. includecode:: code/docs/testkit/ParentChildSpec.scala##test-TestProbe-parent + +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 + +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 :class:`Props` object or by giving it a function which takes care of creating the child actor: @@ -581,14 +606,6 @@ 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 diff --git a/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java b/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java index 7dabcaebac..ce0a98097d 100644 --- a/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java +++ b/akka-testkit/src/main/java/akka/testkit/JavaTestKit.java @@ -4,9 +4,12 @@ package akka.testkit; import akka.actor.Terminated; +import scala.Option; import scala.runtime.AbstractFunction0; import akka.actor.ActorRef; import akka.actor.ActorSystem; +import akka.actor.Props; +import akka.actor.SupervisorStrategy; import akka.event.Logging; import akka.event.Logging.LogEvent; import akka.japi.JavaPartialFunction; @@ -684,4 +687,43 @@ public class JavaTestKit { public void shutdown(ActorSystem actorSystem, Boolean verifySystemShutdown) { shutdown(actorSystem, null, verifySystemShutdown); } + + /** + * Spawns an actor as a child of this test actor, and returns the child's ActorRef. + * @param props Props to create the child actor + * @param name Actor name for the child actor + * @param supervisorStrategy Strategy should decide what to do with failures in the actor. + */ + public ActorRef childActorOf(Props props, String name, SupervisorStrategy supervisorStrategy) { + return p.childActorOf(props, name, supervisorStrategy); + } + + /** + * Spawns an actor as a child of this test actor, and returns the child's ActorRef. + * The actor will have an auto-generated name. + * @param props Props to create the child actor + * @param supervisorStrategy Strategy should decide what to do with failures in the actor. + */ + public ActorRef childActorOf(Props props, SupervisorStrategy supervisorStrategy) { + return p.childActorOf(props, supervisorStrategy); + } + + /** + * Spawns an actor as a child of this test actor, and returns the child's ActorRef. + * The actor will be supervised using {@link SupervisorStrategy.stoppingStrategy}. + * @param props Props to create the child actor + * @param name Actor name for the child actor + */ + public ActorRef childActorOf(Props props, String name) { + return p.childActorOf(props, name); + } + + /** + * Spawns an actor as a child of this test actor, and returns the child's ActorRef. + * The actor will have an auto-generated name and will be supervised using {@link SupervisorStrategy.stoppingStrategy}. + * @param props Props to create the child actor + */ + public ActorRef childActorOf(Props props) { + return p.childActorOf(props); + } } diff --git a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala index 2016816ae6..fe106986ac 100644 --- a/akka-testkit/src/main/scala/akka/testkit/TestKit.scala +++ b/akka-testkit/src/main/scala/akka/testkit/TestKit.scala @@ -41,6 +41,12 @@ object TestActor { final case class Watch(ref: ActorRef) extends NoSerializationVerificationNeeded final case class UnWatch(ref: ActorRef) extends NoSerializationVerificationNeeded final case class SetAutoPilot(ap: AutoPilot) extends NoSerializationVerificationNeeded + final case class Spawn(props: Props, name: Option[String] = None, strategy: Option[SupervisorStrategy] = None) extends NoSerializationVerificationNeeded { + def apply(context: ActorRefFactory): ActorRef = name match { + case Some(n) ⇒ context.actorOf(props, n) + case None ⇒ context.actorOf(props) + } + } trait Message { def msg: AnyRef @@ -54,6 +60,31 @@ object TestActor { val FALSE = (x: Any) ⇒ false + /** INTERNAL API */ + private[TestActor] class DelegatingSupervisorStrategy extends SupervisorStrategy { + import SupervisorStrategy._ + + private var delegates = Map.empty[ActorRef, SupervisorStrategy] + + private def delegate(child: ActorRef) = delegates.get(child).getOrElse(stoppingStrategy) + + def update(child: ActorRef, supervisor: SupervisorStrategy): Unit = delegates += (child → supervisor) + + override def decider = defaultDecider // not actually invoked + + override def handleChildTerminated(context: ActorContext, child: ActorRef, children: Iterable[ActorRef]): Unit = { + delegates -= child + } + + override def processFailure(context: ActorContext, restart: Boolean, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]): Unit = { + delegates(child).processFailure(context, restart, child, cause, stats, children) + } + + override def handleFailure(context: ActorContext, child: ActorRef, cause: Throwable, stats: ChildRestartStats, children: Iterable[ChildRestartStats]): Boolean = { + delegates(child).handleFailure(context, child, cause, stats, children) + } + } + // make creator serializable, for VerifySerializabilitySpec def props(queue: BlockingDeque[Message]): Props = Props(classOf[TestActor], queue) } @@ -61,6 +92,8 @@ object TestActor { class TestActor(queue: BlockingDeque[TestActor.Message]) extends Actor { import TestActor._ + override val supervisorStrategy: DelegatingSupervisorStrategy = new DelegatingSupervisorStrategy + var ignore: Ignore = None var autopilot: AutoPilot = NoAutoPilot @@ -70,6 +103,10 @@ class TestActor(queue: BlockingDeque[TestActor.Message]) extends Actor { case Watch(ref) ⇒ context.watch(ref) case UnWatch(ref) ⇒ context.unwatch(ref) case SetAutoPilot(pilot) ⇒ autopilot = pilot + case spawn: Spawn ⇒ + val actor = spawn(context) + for (s ← spawn.strategy) supervisorStrategy(actor) = s + queue.offerLast(RealMessage(actor, self)) case x: AnyRef ⇒ autopilot = autopilot.run(sender(), x) match { case KeepRunning ⇒ autopilot @@ -102,7 +139,7 @@ class TestActor(queue: BlockingDeque[TestActor.Message]) extends Actor { */ trait TestKitBase { - import TestActor.{ Message, RealMessage, NullMessage } + import TestActor.{ Message, RealMessage, NullMessage, Spawn } implicit val system: ActorSystem val testKitSettings = TestKitExtension(system) @@ -688,6 +725,46 @@ trait TestKitBase { TestKit.shutdownActorSystem(actorSystem, duration, verifySystemShutdown) } + /** + * Spawns an actor as a child of this test actor, and returns the child's ActorRef. + * @param props Props to create the child actor + * @param name Actor name for the child actor + * @param supervisorStrategy Strategy should decide what to do with failures in the actor. + */ + def childActorOf(props: Props, name: String, supervisorStrategy: SupervisorStrategy): ActorRef = { + testActor ! Spawn(props, Some(name), Some(supervisorStrategy)) + expectMsgType[ActorRef] + } + + /** + * Spawns an actor as a child of this test actor with an auto-generated name, and returns the child's ActorRef. + * @param props Props to create the child actor + * @param supervisorStrategy Strategy should decide what to do with failures in the actor. + */ + def childActorOf(props: Props, supervisorStrategy: SupervisorStrategy): ActorRef = { + testActor ! Spawn(props, None, Some(supervisorStrategy)) + expectMsgType[ActorRef] + } + + /** + * Spawns an actor as a child of this test actor with a stopping supervisor strategy, and returns the child's ActorRef. + * @param props Props to create the child actor + * @param name Actor name for the child actor + */ + def childActorOf(props: Props, name: String): ActorRef = { + testActor ! Spawn(props, Some(name), None) + expectMsgType[ActorRef] + } + + /** + * Spawns an actor as a child of this test actor with an auto-generated name and stopping supervisor strategy, returning the child's ActorRef. + * @param props Props to create the child actor + */ + def childActorOf(props: Props): ActorRef = { + testActor ! Spawn(props, None, None) + expectMsgType[ActorRef] + } + private def format(u: TimeUnit, d: Duration) = "%.3f %s".format(d.toUnit(u), u.toString.toLowerCase) } diff --git a/akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala b/akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala index 1a81181a97..a8862a16b6 100644 --- a/akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala +++ b/akka-testkit/src/test/scala/akka/testkit/TestProbeSpec.scala @@ -7,6 +7,7 @@ import scala.concurrent.{ Await } import scala.concurrent.duration._ import akka.pattern.ask import scala.util.Try +import java.util.concurrent.atomic.AtomicInteger class TestProbeSpec extends AkkaSpec with DefaultTimeout { @@ -39,6 +40,38 @@ class TestProbeSpec extends AkkaSpec with DefaultTimeout { probe1.expectMsg(0 millis, "some hint here", "world") } + "create a child when invoking actorOf" in { + val probe = TestProbe() + val child = probe.childActorOf(TestActors.echoActorProps) + child.path.parent should be(probe.ref.path) + + val namedChild = probe.childActorOf(TestActors.echoActorProps, "actorName") + namedChild.path.name should be("actorName") + } + + "restart a failing child if the given supervisor says so" in { + val restarts = new AtomicInteger(0) + + class FailingActor extends Actor { + override def receive = msg ⇒ msg match { + case _ ⇒ + throw new RuntimeException("simulated failure") + } + + override def postRestart(reason: Throwable): Unit = { + restarts.incrementAndGet() + } + } + + val probe = TestProbe() + val child = probe.childActorOf(Props(new FailingActor), SupervisorStrategy.defaultStrategy) + + awaitAssert { + child ! "hello" + restarts.get() should be > (1) + } + } + def assertFailureMessageContains(expectedHint: String)(block: ⇒ Unit) { Try { block diff --git a/project/MiMa.scala b/project/MiMa.scala index beee48c78a..2744b0c362 100644 --- a/project/MiMa.scala +++ b/project/MiMa.scala @@ -951,7 +951,12 @@ object MiMa extends AutoPlugin { // #21025 new orElse flow op ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.stream.scaladsl.FlowOps.orElseGraph"), ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.stream.scaladsl.FlowOps.orElse"), - ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.stream.scaladsl.FlowOpsMat.orElseMat") + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.stream.scaladsl.FlowOpsMat.orElseMat"), + + // #21201 adding childActorOf to TestActor / TestKit / TestProbe + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.testkit.TestKitBase.childActorOf$default$3"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.testkit.TestKitBase.childActorOf$default$2"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.testkit.TestKitBase.childActorOf") ) ) }