diff --git a/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/async/BasicAsyncTestingTest.java b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/async/BasicAsyncTestingTest.java new file mode 100644 index 0000000000..7bbbe8d367 --- /dev/null +++ b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/async/BasicAsyncTestingTest.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2017 Lightbend Inc. + */ +package jdocs.akka.typed.testing.async; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.Actor; +import akka.testkit.typed.javadsl.TestProbe; +import akka.testkit.typed.TestKit; +import org.junit.AfterClass; +import org.junit.Test; + +//#test-header +public class BasicAsyncTestingTest extends TestKit { + public BasicAsyncTestingTest() { + super(ActorSystem.create(Actor.empty(), "BasicAsyncTestingTest")); + } +//#test-header + + //#under-test + public static class Ping { + private String msg; + private ActorRef replyTo; + + public Ping(String msg, ActorRef replyTo) { + this.msg = msg; + this.replyTo = replyTo; + } + } + public static class Pong { + private String msg; + + public Pong(String msg) { + this.msg = msg; + } + } + + Behavior echoActor = Actor.immutable((ctx, ping) -> { + ping.replyTo.tell(new Pong(ping.msg)); + return Actor.same(); + }); + //#under-test + + //#test-shutdown + @AfterClass + public void cleanup() { + this.shutdown(); + } + //#test-shutdown + + @Test + public void testVerifyingAResponse() { + //#test + TestProbe probe = new TestProbe<>(system(), testkitSettings()); + ActorRef pinger = actorOf(echoActor, "ping"); + pinger.tell(new Ping("hello", probe.ref())); + probe.expectMsg(new Pong("hello")); + //#test + } +} diff --git a/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java new file mode 100644 index 0000000000..8917b6f1fd --- /dev/null +++ b/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java @@ -0,0 +1,114 @@ +package jdocs.akka.typed.testing.sync; + +//#imports +import akka.actor.typed.*; +import akka.actor.typed.javadsl.*; +import akka.testkit.typed.*; +//#imports +import org.junit.Test; +import org.scalatest.junit.JUnitSuite; + +public class BasicSyncTestingTest extends JUnitSuite { + + //#child + public static Behavior childActor = Actor.immutable((ctx, msg) -> Actor.same()); + //#child + + //#under-test + interface Command { } + public static class CreateAChild implements Command { + private final String childName; + public CreateAChild(String childName) { + this.childName = childName; + } + } + public static class CreateAnAnonymousChild implements Command { } + public static class SayHelloToChild implements Command { + private final String childName; + public SayHelloToChild(String childName) { + this.childName = childName; + } + } + public static class SayHelloToAnonymousChild implements Command { } + public static class SayHello implements Command { + private final ActorRef who; + + public SayHello(ActorRef who) { + this.who = who; + } + } + + public static Behavior myBehaviour = Actor.immutable(Command.class) + .onMessage(CreateAChild.class, (ctx, msg) -> { + ctx.spawn(childActor, msg.childName); + return Actor.same(); + }) + .onMessage(CreateAnAnonymousChild.class, (ctx, msg) -> { + ctx.spawnAnonymous(childActor); + return Actor.same(); + }) + .onMessage(SayHelloToChild.class, (ctx, msg) -> { + ActorRef child = ctx.spawn(childActor, msg.childName); + child.tell("hello"); + return Actor.same(); + }) + .onMessage(SayHelloToAnonymousChild.class, (ctx, msg) -> { + ActorRef child = ctx.spawnAnonymous(childActor); + child.tell("hello stranger"); + return Actor.same(); + }).onMessage(SayHello.class, (ctx, msg) -> { + msg.who.tell("hello"); + return Actor.same(); + }).build(); + //#under-test + + + @Test + public void testSpawning() { + //#test-child + BehaviorTestkit test = BehaviorTestkit.create(myBehaviour); + test.run(new CreateAChild("child")); + test.expectEffect(new Effect.Spawned(childActor, "child", Props.empty())); + //#test-child + } + + @Test + public void testSpawningAnonymous() { + //#test-anonymous-child + BehaviorTestkit test = BehaviorTestkit.create(myBehaviour); + test.run(new CreateAnAnonymousChild()); + test.expectEffect(new Effect.SpawnedAnonymous(childActor, Props.empty())); + //#test-anonymous-child + } + + @Test + public void testRecodingMessageSend() { + //#test-message + BehaviorTestkit test = BehaviorTestkit.create(myBehaviour); + TestInbox inbox = new TestInbox(); + test.run(new SayHello(inbox.ref())); + inbox.expectMsg("hello"); + //#test-message + } + + @Test + public void testMessageToChild() { + //#test-child-message + BehaviorTestkit testKit = BehaviorTestkit.create(myBehaviour); + testKit.run(new SayHelloToChild("child")); + TestInbox childInbox = testKit.childInbox("child"); + childInbox.expectMsg("hello"); + //#test-child-message + } + + @Test + public void testMessageToAnonymousChild() { + //#test-child-message-anonymous + BehaviorTestkit testKit = BehaviorTestkit.create(myBehaviour); + testKit.run(new SayHelloToAnonymousChild()); + // Anonymous actors are created as: $a $b etc + TestInbox childInbox = testKit.childInbox("$a"); + childInbox.expectMsg("hello stranger"); + //#test-child-message-anonymous + } +} diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/ActorContextSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/ActorContextSpec.scala index ab84b1e010..18ecbcae0d 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/ActorContextSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/ActorContextSpec.scala @@ -266,7 +266,7 @@ abstract class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( | lifecycle = off | autoreceive = off | } - | typed.loggers = ["akka.typed.testkit.TestEventListener"] + | typed.loggers = ["akka.testkit.typed.TestEventListener"] |}""".stripMargin)) { import ActorContextSpec._ diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/BehaviorSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/BehaviorSpec.scala index ac57827fc0..85a7f9a6ba 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/BehaviorSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/BehaviorSpec.scala @@ -10,7 +10,7 @@ import akka.japi.pf.{ FI, PFBuilder } import java.util.function.{ Function ⇒ F1 } import akka.Done -import akka.typed.testkit.{ EffectfulActorContext, Inbox } +import akka.testkit.typed.{ BehaviorTestkit, TestInbox } object BehaviorSpec { sealed trait Command { @@ -68,23 +68,23 @@ object BehaviorSpec { def checkAux(signal: Signal, aux: Aux): Unit = () def checkAux(command: Command, aux: Aux): Unit = () - case class Init(behv: Behavior[Command], inbox: Inbox[Event], aux: Aux) { + case class Init(behv: Behavior[Command], inbox: TestInbox[Event], aux: Aux) { def mkCtx(): Setup = { - val ctx = new EffectfulActorContext("ctx", behv, 1000, system) - val msgs = inbox.receiveAll() - Setup(ctx, inbox, aux) + val testkit = BehaviorTestkit(behv) + inbox.receiveAll() + Setup(testkit, inbox, aux) } } - case class Setup(ctx: EffectfulActorContext[Command], inbox: Inbox[Event], aux: Aux) + case class Setup(testKit: BehaviorTestkit[Command], inbox: TestInbox[Event], aux: Aux) def init(): Init = { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val (behv, aux) = behavior(inbox.ref) Init(behv, inbox, aux) } def init(factory: ActorRef[Event] ⇒ (Behavior[Command], Aux)): Init = { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val (behv, aux) = factory(inbox.ref) Init(behv, inbox, aux) } @@ -94,20 +94,20 @@ object BehaviorSpec { implicit class Check(val setup: Setup) { def check(signal: Signal): Setup = { - setup.ctx.signal(signal) + setup.testKit.signal(signal) setup.inbox.receiveAll() should ===(GotSignal(signal) :: Nil) checkAux(signal, setup.aux) setup } def check(command: Command): Setup = { - setup.ctx.run(command) - setup.inbox.receiveAll() should ===(command.expectedResponse(setup.ctx)) + setup.testKit.run(command) + setup.inbox.receiveAll() should ===(command.expectedResponse(setup.testKit.ctx)) checkAux(command, setup.aux) setup } def check2(command: Command): Setup = { - setup.ctx.run(command) - val expected = command.expectedResponse(setup.ctx) + setup.testKit.run(command) + val expected = command.expectedResponse(setup.testKit.ctx) setup.inbox.receiveAll() should ===(expected ++ expected) checkAux(command, setup.aux) setup @@ -118,7 +118,7 @@ object BehaviorSpec { } trait Siphon extends Common { - override type Aux = Inbox[Command] + override type Aux = TestInbox[Command] override def checkAux(command: Command, aux: Aux): Unit = { aux.receiveAll() should ===(command :: Nil) @@ -126,7 +126,7 @@ object BehaviorSpec { } trait SignalSiphon extends Common { - override type Aux = Inbox[Either[Signal, Command]] + override type Aux = TestInbox[Either[Signal, Command]] override def checkAux(command: Command, aux: Aux): Unit = { aux.receiveAll() should ===(Right(command) :: Nil) @@ -142,25 +142,25 @@ object BehaviorSpec { case (ctx, GetSelf) ⇒ monitor ! Self(ctx.self) SActor.same - case (ctx, Miss) ⇒ + case (_, Miss) ⇒ monitor ! Missed SActor.unhandled - case (ctx, Ignore) ⇒ + case (_, Ignore) ⇒ monitor ! Ignored SActor.same - case (ctx, Ping) ⇒ + case (_, Ping) ⇒ monitor ! Pong mkFull(monitor, state) - case (ctx, Swap) ⇒ + case (_, Swap) ⇒ monitor ! Swapped mkFull(monitor, state.next) - case (ctx, GetState()) ⇒ + case (_, GetState()) ⇒ monitor ! state SActor.same - case (ctx, Stop) ⇒ SActor.stopped - case (_, _) ⇒ SActor.unhandled + case (_, Stop) ⇒ SActor.stopped + case (_, _) ⇒ SActor.unhandled } onSignal { - case (ctx, signal) ⇒ + case (_, signal) ⇒ monitor ! GotSignal(signal) SActor.same } @@ -220,15 +220,15 @@ object BehaviorSpec { } "must react to Terminated" in { - mkCtx().check(Terminated(Inbox("x").ref)(null)) + mkCtx().check(Terminated(TestInbox("x").ref)(null)) } "must react to Terminated after a message" in { - mkCtx().check(GetSelf).check(Terminated(Inbox("x").ref)(null)) + mkCtx().check(GetSelf).check(Terminated(TestInbox("x").ref)(null)) } "must react to a message after Terminated" in { - mkCtx().check(Terminated(Inbox("x").ref)(null)).check(GetSelf) + mkCtx().check(Terminated(TestInbox("x").ref)(null)).check(GetSelf) } } } @@ -250,10 +250,10 @@ object BehaviorSpec { } trait Unhandled extends Common { - "Unahndled" must { + "Unhandled" must { "must return Unhandled" in { - val Setup(ctx, inbox, aux) = mkCtx() - Behavior.interpretMessage(ctx.currentBehavior, ctx, Miss) should be(Behavior.UnhandledBehavior) + val Setup(testKit, inbox, aux) = mkCtx() + Behavior.interpretMessage(testKit.currentBehavior, testKit.ctx, Miss) should be(Behavior.UnhandledBehavior) inbox.receiveAll() should ===(Missed :: Nil) checkAux(Miss, aux) } @@ -263,16 +263,16 @@ object BehaviorSpec { trait Stoppable extends Common { "Stopping" must { "must stop" in { - val Setup(ctx, inbox, aux) = mkCtx() - ctx.run(Stop) - ctx.currentBehavior should be(Behavior.StoppedBehavior) + val Setup(testkit, _, aux) = mkCtx() + testkit.run(Stop) + testkit.currentBehavior should be(Behavior.StoppedBehavior) checkAux(Stop, aux) } } } trait Become extends Common with Unhandled { - private implicit val inbox = Inbox[State]("state") + private implicit val inbox = TestInbox[State]("state") "Becoming" must { "must be in state A" in { @@ -308,15 +308,15 @@ object BehaviorSpec { } "react to Terminated after swap" in { - mkCtx().check(Swap).check(Terminated(Inbox("x").ref)(null)) + mkCtx().check(Swap).check(Terminated(TestInbox("x").ref)(null)) } "react to Terminated after a message after swap" in { - mkCtx().check(Swap).check(GetSelf).check(Terminated(Inbox("x").ref)(null)) + mkCtx().check(Swap).check(GetSelf).check(Terminated(TestInbox("x").ref)(null)) } "react to a message after Terminated after swap" in { - mkCtx().check(Swap).check(Terminated(Inbox("x").ref)(null)).check(GetSelf) + mkCtx().check(Swap).check(Terminated(TestInbox("x").ref)(null)).check(GetSelf) } } } @@ -368,7 +368,7 @@ class ImmutableBehaviorSpec extends TypedSpec with Messages with BecomeWithLifec case (_, Stop) ⇒ SActor.stopped case (_, _: AuxPing) ⇒ SActor.unhandled } onSignal { - case (ctx, signal) ⇒ + case (_, signal) ⇒ monitor ! GotSignal(signal) SActor.same } @@ -405,7 +405,7 @@ class ImmutableWithSignalScalaBehaviorSpec extends TypedSpec with Messages with case _: AuxPing ⇒ SActor.unhandled } } onSignal { - case (ctx, sig) ⇒ + case (_, sig) ⇒ monitor ! GotSignal(sig) SActor.same } @@ -485,17 +485,17 @@ class WidenedScalaBehaviorSpec extends ImmutableWithSignalScalaBehaviorSpec with import SActor.BehaviorDecorators override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Command]("widenedListener") + val inbox = TestInbox[Command]("widenedListener") super.behavior(monitor)._1.widen[Command] { case c ⇒ inbox.ref ! c; c } → inbox } } class DeferredScalaBehaviorSpec extends ImmutableWithSignalScalaBehaviorSpec { - override type Aux = Inbox[Done] + override type Aux = TestInbox[Done] override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Done]("deferredListener") - (SActor.deferred(ctx ⇒ { + val inbox = TestInbox[Done]("deferredListener") + (SActor.deferred(_ ⇒ { inbox.ref ! Done super.behavior(monitor)._1 }), inbox) @@ -507,7 +507,7 @@ class DeferredScalaBehaviorSpec extends ImmutableWithSignalScalaBehaviorSpec { class TapScalaBehaviorSpec extends ImmutableWithSignalScalaBehaviorSpec with Reuse with SignalSiphon { override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Either[Signal, Command]]("tapListener") + val inbox = TestInbox[Either[Signal, Command]]("tapListener") (SActor.tap((_, msg) ⇒ inbox.ref ! Right(msg), (_, sig) ⇒ inbox.ref ! Left(sig), super.behavior(monitor)._1), inbox) } } @@ -544,7 +544,7 @@ class ImmutableWithSignalJavaBehaviorSpec extends TypedSpec with Messages with B case Stop ⇒ SActor.stopped case _: AuxPing ⇒ SActor.unhandled }), - fs((ctx, sig) ⇒ { + fs((_, sig) ⇒ { monitor ! GotSignal(sig) SActor.same })) @@ -582,7 +582,7 @@ class ImmutableJavaBehaviorSpec extends TypedSpec with Messages with Become with class WidenedJavaBehaviorSpec extends ImmutableWithSignalJavaBehaviorSpec with Reuse with Siphon { override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Command]("widenedListener") + val inbox = TestInbox[Command]("widenedListener") JActor.widened(super.behavior(monitor)._1, pf(_.`match`(classOf[Command], fi(x ⇒ { inbox.ref ! x x @@ -591,11 +591,11 @@ class WidenedJavaBehaviorSpec extends ImmutableWithSignalJavaBehaviorSpec with R } class DeferredJavaBehaviorSpec extends ImmutableWithSignalJavaBehaviorSpec { - override type Aux = Inbox[Done] + override type Aux = TestInbox[Done] override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Done]("deferredListener") - (JActor.deferred(df(ctx ⇒ { + val inbox = TestInbox[Done]("deferredListener") + (JActor.deferred(df(_ ⇒ { inbox.ref ! Done super.behavior(monitor)._1 })), inbox) @@ -607,7 +607,7 @@ class DeferredJavaBehaviorSpec extends ImmutableWithSignalJavaBehaviorSpec { class TapJavaBehaviorSpec extends ImmutableWithSignalJavaBehaviorSpec with Reuse with SignalSiphon { override def behavior(monitor: ActorRef[Event]): (Behavior[Command], Aux) = { - val inbox = Inbox[Either[Signal, Command]]("tapListener") + val inbox = TestInbox[Either[Signal, Command]]("tapListener") (JActor.tap( pc((_, msg) ⇒ inbox.ref ! Right(msg)), ps((_, sig) ⇒ inbox.ref ! Left(sig)), diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/DeferredSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/DeferredSpec.scala index 6f156c3e70..e3ef98ddac 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/DeferredSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/DeferredSpec.scala @@ -3,14 +3,12 @@ */ package akka.actor.typed -import scala.concurrent.Await import scala.concurrent.duration._ import scala.util.control.NoStackTrace import akka.actor.typed.scaladsl.Actor import akka.actor.typed.scaladsl.Actor.BehaviorDecorators -import akka.actor.typed.scaladsl.AskPattern._ -import akka.typed.testkit.{ EffectfulActorContext, Inbox, TestKitSettings } -import akka.typed.testkit.scaladsl._ +import akka.testkit.typed.{ BehaviorTestkit, TestInbox, TestKitSettings } +import akka.testkit.typed.scaladsl._ object DeferredSpec { sealed trait Command @@ -21,7 +19,7 @@ object DeferredSpec { case object Started extends Event def target(monitor: ActorRef[Event]): Behavior[Command] = - Actor.immutable((ctx, cmd) ⇒ cmd match { + Actor.immutable((_, cmd) ⇒ cmd match { case Ping ⇒ monitor ! Pong Actor.same @@ -92,7 +90,7 @@ class DeferredSpec extends TypedSpec with StartSupport { probe.expectMsg(Started) } - "must undefer underlying when wrapped by widen" in { + "must un-defer underlying when wrapped by widen" in { val probe = TestProbe[Event]("evt") val behv = Actor.deferred[Command] { _ ⇒ probe.ref ! Started @@ -108,7 +106,7 @@ class DeferredSpec extends TypedSpec with StartSupport { probe.expectMsg(Pong) } - "must undefer underlying when wrapped by monitor" in { + "must un-defer underlying when wrapped by monitor" in { // monitor is implemented with tap, so this is testing both val probe = TestProbe[Event]("evt") val monitorProbe = TestProbe[Command]("monitor") @@ -132,22 +130,22 @@ class DeferredStubbedSpec extends TypedSpec { import DeferredSpec._ - def mkCtx(behv: Behavior[Command]): EffectfulActorContext[Command] = - new EffectfulActorContext("ctx", behv, 1000, system) + def mkCtx(behv: Behavior[Command]): BehaviorTestkit[Command] = + BehaviorTestkit(behv, "ctx") "must create underlying deferred behavior immediately" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = Actor.deferred[Command] { _ ⇒ inbox.ref ! Started target(inbox.ref) } - val ctx = mkCtx(behv) + mkCtx(behv) // it's supposed to be created immediately (not waiting for first message) inbox.receiveMsg() should ===(Started) } "must stop when exception from factory" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val exc = new RuntimeException("simulated exc from factory") with NoStackTrace val behv = Actor.deferred[Command] { _ ⇒ inbox.ref ! Started diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/RestarterSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/RestarterSpec.scala index 739571869d..8dbdcd442e 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/RestarterSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/RestarterSpec.scala @@ -5,10 +5,10 @@ package akka.actor.typed import scala.concurrent.duration._ import akka.actor.typed.scaladsl.Actor._ -import akka.typed.testkit.{ EffectfulActorContext, Inbox, TestKitSettings } +import akka.testkit.typed.{ BehaviorTestkit, TestInbox, TestKitSettings } import scala.util.control.NoStackTrace -import akka.typed.testkit.scaladsl._ +import akka.testkit.typed.scaladsl._ object RestarterSpec { @@ -68,12 +68,12 @@ class RestarterSpec extends TypedSpec { import RestarterSpec._ - def mkCtx(behv: Behavior[Command]): EffectfulActorContext[Command] = - new EffectfulActorContext("ctx", behv, 1000, system) + def mkCtx(behv: Behavior[Command]): BehaviorTestkit[Command] = + BehaviorTestkit(behv, "ctx") "A restarter" must { "receive message" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = supervise(target(inbox.ref)).onFailure[Throwable](SupervisorStrategy.restart) val ctx = mkCtx(behv) ctx.run(Ping) @@ -81,7 +81,7 @@ class RestarterSpec extends TypedSpec { } "stop when no supervise" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = target(inbox.ref) val ctx = mkCtx(behv) intercept[Exc3] { @@ -91,7 +91,7 @@ class RestarterSpec extends TypedSpec { } "stop when unhandled exception" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = supervise(target(inbox.ref)).onFailure[Exc1](SupervisorStrategy.restart) val ctx = mkCtx(behv) intercept[Exc3] { @@ -101,7 +101,7 @@ class RestarterSpec extends TypedSpec { } "restart when handled exception" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = supervise(target(inbox.ref)).onFailure[Exc1](SupervisorStrategy.restart) val ctx = mkCtx(behv) ctx.run(NextState) @@ -115,7 +115,7 @@ class RestarterSpec extends TypedSpec { } "resume when handled exception" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = supervise(target(inbox.ref)).onFailure[Exc1](SupervisorStrategy.resume) val ctx = mkCtx(behv) ctx.run(NextState) @@ -128,7 +128,7 @@ class RestarterSpec extends TypedSpec { } "support nesting to handle different exceptions" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = supervise( supervise( @@ -159,7 +159,7 @@ class RestarterSpec extends TypedSpec { } "not catch fatal error" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = supervise(target(inbox.ref)).onFailure[Throwable](SupervisorStrategy.restart) val ctx = mkCtx(behv) intercept[StackOverflowError] { @@ -169,7 +169,7 @@ class RestarterSpec extends TypedSpec { } "stop after restart retries limit" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val strategy = SupervisorStrategy.restartWithLimit(maxNrOfRetries = 2, withinTimeRange = 1.minute) val behv = supervise(target(inbox.ref)).onFailure[Exc1](strategy) val ctx = mkCtx(behv) @@ -184,7 +184,7 @@ class RestarterSpec extends TypedSpec { } "reset retry limit after withinTimeRange" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val withinTimeRange = 2.seconds val strategy = SupervisorStrategy.restartWithLimit(maxNrOfRetries = 2, withinTimeRange) val behv = supervise(target(inbox.ref)).onFailure[Exc1](strategy) @@ -206,7 +206,7 @@ class RestarterSpec extends TypedSpec { } "stop at first exception when restart retries limit is 0" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val strategy = SupervisorStrategy.restartWithLimit(maxNrOfRetries = 0, withinTimeRange = 1.minute) val behv = supervise(target(inbox.ref)).onFailure[Exc1](strategy) val ctx = mkCtx(behv) @@ -217,12 +217,12 @@ class RestarterSpec extends TypedSpec { } "create underlying deferred behavior immediately" in { - val inbox = Inbox[Event]("evt") + val inbox = TestInbox[Event]("evt") val behv = supervise(deferred[Command] { _ ⇒ inbox.ref ! Started target(inbox.ref) }).onFailure[Exc1](SupervisorStrategy.restart) - val ctx = mkCtx(behv) + mkCtx(behv) // it's supposed to be created immediately (not waiting for first message) inbox.receiveMsg() should ===(Started) } diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TimerSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TimerSpec.scala index d3bc10773a..d501fdbc3c 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TimerSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TimerSpec.scala @@ -12,8 +12,8 @@ import scala.util.control.NoStackTrace import akka.actor.typed.scaladsl.Actor import akka.actor.typed.scaladsl.TimerScheduler -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl._ +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl._ class TimerSpec extends TypedSpec( """ diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TypedSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TypedSpec.scala index 3551a38186..907446c11d 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TypedSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/TypedSpec.scala @@ -22,13 +22,12 @@ import akka.actor.typed.scaladsl.Actor._ import org.scalatest.concurrent.ScalaFutures import org.scalactic.TypeCheckedTripleEquals import org.scalactic.CanEqual -import org.junit.runner.RunWith import scala.util.control.NonFatal import akka.actor.typed.scaladsl.AskPattern import scala.util.control.NoStackTrace -import akka.typed.testkit.{ Inbox, TestKitSettings } +import akka.testkit.typed.{ TestInbox, TestKitSettings } import org.scalatest.time.Span /** @@ -52,7 +51,7 @@ trait StartSupport { def start[T](behv: Behavior[T]): ActorRef[T] = { import akka.actor.typed.scaladsl.AskPattern._ - import akka.typed.testkit.scaladsl._ + import akka.testkit.typed.scaladsl._ implicit val testSettings = TestKitSettings(system) Await.result(system ? TypedSpec.Create(behv, nextName()), 3.seconds.dilated) } @@ -145,7 +144,7 @@ abstract class TypedSpec(val config: Config) extends TypedSpecSetup { /** * Group assertion that ensures that the given inboxes are empty. */ - def assertEmpty(inboxes: Inbox[_]*): Unit = { + def assertEmpty(inboxes: TestInbox[_]*): Unit = { inboxes foreach (i ⇒ withClue(s"inbox $i had messages")(i.hasMessages should be(false))) } @@ -163,8 +162,6 @@ abstract class TypedSpec(val config: Config) extends TypedSpecSetup { object TypedSpec { - import akka.{ typed ⇒ t } - sealed abstract class Start case object Start extends Start diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ActorSystemSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ActorSystemSpec.scala index 86f7e08493..ebd81d0235 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ActorSystemSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ActorSystemSpec.scala @@ -6,9 +6,7 @@ package internal import akka.actor.InvalidMessageException import akka.actor.typed.scaladsl.Actor -import akka.actor.typed.scaladsl.Actor._ -import akka.typed.testkit.Inbox -import org.scalactic.ConversionCheckedTripleEquals +import akka.testkit.typed.TestInbox import org.scalatest._ import org.scalatest.concurrent.{ Eventually, ScalaFutures } @@ -16,7 +14,8 @@ import scala.concurrent.duration._ import scala.concurrent.{ Future, Promise } import scala.util.control.NonFatal -class ActorSystemSpec extends WordSpec with Matchers with BeforeAndAfterAll with ScalaFutures with Eventually with ConversionCheckedTripleEquals { +class ActorSystemSpec extends WordSpec with Matchers with BeforeAndAfterAll + with ScalaFutures with Eventually { override implicit val patienceConfig = PatienceConfig(1.second) def system[T](behavior: Behavior[T], name: String) = ActorSystem(behavior, name) @@ -37,46 +36,56 @@ class ActorSystemSpec extends WordSpec with Matchers with BeforeAndAfterAll with } "An ActorSystem" must { - "must start the guardian actor and terminate when it terminates" in { - val t = withSystem("a", immutable[Probe] { case (_, p) ⇒ p.replyTo ! p.msg; stopped }, doTerminate = false) { sys ⇒ - val inbox = Inbox[String]("a") - sys ! Probe("hello", inbox.ref) - eventually { - inbox.hasMessages should ===(true) + "start the guardian actor and terminate when it terminates" in { + val t = withSystem( + "a", + Actor.immutable[Probe] { case (_, p) ⇒ p.replyTo ! p.msg; Actor.stopped }, doTerminate = false) { sys ⇒ + val inbox = TestInbox[String]("a") + sys ! Probe("hello", inbox.ref) + eventually { + inbox.hasMessages should ===(true) + } + inbox.receiveAll() should ===("hello" :: Nil) } - inbox.receiveAll() should ===("hello" :: Nil) - } val p = t.ref.path p.name should ===("/") p.address.system should ===(suite + "-a") } - "must terminate the guardian actor" in { - val inbox = Inbox[String]("terminate") + // see issue #24172 + "shutdown if guardian shuts down immediately" in { + pending + withSystem("shutdown", Actor.stopped[String], doTerminate = false) { sys: ActorSystem[String] ⇒ + sys.whenTerminated.futureValue + } + } + + "terminate the guardian actor" in { + val inbox = TestInbox[String]("terminate") val sys = system( - immutable[Probe] { - case (_, _) ⇒ unhandled + Actor.immutable[Probe] { + case (_, _) ⇒ Actor.unhandled } onSignal { - case (ctx, PostStop) ⇒ + case (_, PostStop) ⇒ inbox.ref ! "done" - same + Actor.same }, "terminate") sys.terminate().futureValue inbox.receiveAll() should ===("done" :: Nil) } - "must log to the event stream" in { + "log to the event stream" in { pending } - "must have a name" in { + "have a name" in { withSystem("name", Actor.empty[String]) { sys ⇒ sys.name should ===(suite + "-name") } } - "must report its uptime" in { + "report its uptime" in { withSystem("uptime", Actor.empty[String]) { sys ⇒ sys.uptime should be < 1L Thread.sleep(1000) @@ -84,7 +93,7 @@ class ActorSystemSpec extends WordSpec with Matchers with BeforeAndAfterAll with } } - "must have a working thread factory" in { + "have a working thread factory" in { withSystem("thread", Actor.empty[String]) { sys ⇒ val p = Promise[Int] sys.threadFactory.newThread(new Runnable { @@ -94,14 +103,14 @@ class ActorSystemSpec extends WordSpec with Matchers with BeforeAndAfterAll with } } - "must be able to run Futures" in { + "be able to run Futures" in { withSystem("futures", Actor.empty[String]) { sys ⇒ val f = Future(42)(sys.executionContext) f.futureValue should ===(42) } } - "must not allow null messages" in { + "not allow null messages" in { withSystem("null-messages", Actor.empty[String]) { sys ⇒ intercept[InvalidMessageException] { sys ! null diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/receptionist/LocalReceptionistSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/receptionist/LocalReceptionistSpec.scala index 02de884188..1cdf09f726 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/receptionist/LocalReceptionistSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/receptionist/LocalReceptionistSpec.scala @@ -7,10 +7,10 @@ import akka.actor.typed._ import akka.actor.typed.receptionist.Receptionist._ import akka.actor.typed.scaladsl.Actor import akka.actor.typed.scaladsl.AskPattern._ -import akka.typed.testkit.EffectfulActorContext -import akka.typed.testkit.Inbox -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.BehaviorTestkit +import akka.testkit.typed.TestInbox +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl.TestProbe import org.scalatest.concurrent.Eventually import scala.concurrent.duration._ @@ -26,7 +26,7 @@ class LocalReceptionistSpec extends TypedSpec with Eventually with StartSupport val behaviorB = Actor.empty[ServiceB] case object Stop extends ServiceA with ServiceB - val stoppableBehavior = Actor.immutable[Any] { (ctx, msg) ⇒ + val stoppableBehavior = Actor.immutable[Any] { (_, msg) ⇒ msg match { case Stop ⇒ Behavior.stopped case _ ⇒ Behavior.same @@ -44,29 +44,29 @@ class LocalReceptionistSpec extends TypedSpec with Eventually with StartSupport "A local receptionist" must { "must register a service" in { - val ctx = new EffectfulActorContext("register", behavior, 1000, system) - val a = Inbox[ServiceA]("a") - val r = Inbox[Registered[_]]("r") + val ctx = new BehaviorTestkit("register", behavior) + val a = TestInbox[ServiceA]("a") + val r = TestInbox[Registered[_]]("r") ctx.run(Register(ServiceKeyA, a.ref)(r.ref)) - ctx.getEffect() // watching however that is implemented + ctx.retrieveEffect() // watching however that is implemented r.receiveMsg() should be(Registered(ServiceKeyA, a.ref)) - val q = Inbox[Listing[ServiceA]]("q") + val q = TestInbox[Listing[ServiceA]]("q") ctx.run(Find(ServiceKeyA)(q.ref)) - ctx.getAllEffects() should be(Nil) + ctx.retrieveAllEffects() should be(Nil) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a.ref))) assertEmpty(a, r, q) } "must register two services" in { - val ctx = new EffectfulActorContext("registertwo", behavior, 1000, system) - val a = Inbox[ServiceA]("a") - val r = Inbox[Registered[_]]("r") + val ctx = new BehaviorTestkit("registertwo", behavior) + val a = TestInbox[ServiceA]("a") + val r = TestInbox[Registered[_]]("r") ctx.run(Register(ServiceKeyA, a.ref)(r.ref)) r.receiveMsg() should be(Registered(ServiceKeyA, a.ref)) - val b = Inbox[ServiceB]("b") + val b = TestInbox[ServiceB]("b") ctx.run(Register(ServiceKeyB, b.ref)(r.ref)) r.receiveMsg() should be(Registered(ServiceKeyB, b.ref)) - val q = Inbox[Listing[_]]("q") + val q = TestInbox[Listing[_]]("q") ctx.run(Find(ServiceKeyA)(q.ref)) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a.ref))) ctx.run(Find(ServiceKeyB)(q.ref)) @@ -75,15 +75,15 @@ class LocalReceptionistSpec extends TypedSpec with Eventually with StartSupport } "must register two services with the same key" in { - val ctx = new EffectfulActorContext("registertwosame", behavior, 1000, system) - val a1 = Inbox[ServiceA]("a1") - val r = Inbox[Registered[_]]("r") + val ctx = new BehaviorTestkit("registertwosame", behavior) + val a1 = TestInbox[ServiceA]("a1") + val r = TestInbox[Registered[_]]("r") ctx.run(Register(ServiceKeyA, a1.ref)(r.ref)) r.receiveMsg() should be(Registered(ServiceKeyA, a1.ref)) - val a2 = Inbox[ServiceA]("a2") + val a2 = TestInbox[ServiceA]("a2") ctx.run(Register(ServiceKeyA, a2.ref)(r.ref)) r.receiveMsg() should be(Registered(ServiceKeyA, a2.ref)) - val q = Inbox[Listing[_]]("q") + val q = TestInbox[Listing[_]]("q") ctx.run(Find(ServiceKeyA)(q.ref)) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a1.ref, a2.ref))) ctx.run(Find(ServiceKeyB)(q.ref)) diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ImmutablePartialSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ImmutablePartialSpec.scala index 70ddd82094..29b0a296a9 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ImmutablePartialSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/ImmutablePartialSpec.scala @@ -4,8 +4,8 @@ package akka.actor.typed package scaladsl -import akka.typed.testkit.{ EffectfulActorContext, TestKitSettings } -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.{ BehaviorTestkit, TestKitSettings } +import akka.testkit.typed.scaladsl.TestProbe import scala.concurrent.duration.DurationInt class ImmutablePartialSpec extends TypedSpec with StartSupport { @@ -22,7 +22,7 @@ class ImmutablePartialSpec extends TypedSpec with StartSupport { probe.ref ! Command2 Actor.same } - val context = new EffectfulActorContext("ctx", behavior, 42, null) + val context = new BehaviorTestkit("ctx", behavior) context.run(Command1) context.currentBehavior shouldBe behavior diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/OnSignalSpec.scala b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/OnSignalSpec.scala index 5acaac379b..1f9e0008d0 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/OnSignalSpec.scala +++ b/akka-actor-typed-tests/src/test/scala/akka/actor/typed/scaladsl/OnSignalSpec.scala @@ -5,8 +5,8 @@ package akka.actor.typed package scaladsl import akka.Done -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl.TestProbe final class OnSignalSpec extends TypedSpec with StartSupport { diff --git a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/async/BasicAsyncTestingSpec.scala b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/async/BasicAsyncTestingSpec.scala new file mode 100644 index 0000000000..96ed4053ec --- /dev/null +++ b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/async/BasicAsyncTestingSpec.scala @@ -0,0 +1,45 @@ +package docs.akka.typed.testing.async + +import akka.actor.typed.scaladsl.Actor +import akka.actor.typed._ +import akka.testkit.typed.TestKit +import akka.testkit.typed.scaladsl._ +import org.scalatest._ + +object BasicAsyncTestingSpec { + //#under-test + case class Ping(msg: String, response: ActorRef[Pong]) + case class Pong(msg: String) + + val echoActor = Actor.immutable[Ping] { (_, msg) ⇒ + msg match { + case Ping(m, replyTo) ⇒ + replyTo ! Pong(m) + Actor.same + } + } + //#under-test +} + +//#test-header +class BasicAsyncTestingSpec extends TestKit(ActorSystem(Actor.empty, "BasicTestingSpec")) + with WordSpecLike with BeforeAndAfterAll { + //#test-header + + import BasicAsyncTestingSpec._ + + "A testkit" must { + "support verifying a response" in { + //#test + val probe = TestProbe[Pong]() + val pinger = actorOf(echoActor, "ping") + pinger ! Ping("hello", probe.ref) + probe.expectMsg(Pong("hello")) + //#test + } + } + + //#test-shutdown + override def afterAll(): Unit = shutdown() + //#test-shutdown +} diff --git a/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala new file mode 100644 index 0000000000..8b4abba9b8 --- /dev/null +++ b/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala @@ -0,0 +1,99 @@ +package docs.akka.typed.testing.sync + +//#imports +import akka.actor.typed._ +import akka.actor.typed.scaladsl._ +import akka.testkit.typed._ +import akka.testkit.typed.Effect._ +//#imports +import org.scalatest.{ Matchers, WordSpec } + +object BasicSyncTestingSpec { + //#child + val childActor = Actor.immutable[String] { (_, _) ⇒ + Actor.same[String] + } + //#child + + //#under-test + sealed trait Cmd + case object CreateAnonymousChild extends Cmd + case class CreateChild(childName: String) extends Cmd + case class SayHelloToChild(childName: String) extends Cmd + case object SayHelloToAnonymousChild extends Cmd + case class SayHello(who: ActorRef[String]) extends Cmd + + val myBehaviour = Actor.immutablePartial[Cmd] { + case (ctx, CreateChild(name)) ⇒ + ctx.spawn(childActor, name) + Actor.same + case (ctx, CreateAnonymousChild) ⇒ + ctx.spawnAnonymous(childActor) + Actor.same + case (ctx, SayHelloToChild(childName)) ⇒ + val child: ActorRef[String] = ctx.spawn(childActor, childName) + child ! "hello" + Actor.same + case (ctx, SayHelloToAnonymousChild) ⇒ + val child: ActorRef[String] = ctx.spawnAnonymous(childActor) + child ! "hello stranger" + Actor.same + case (_, SayHello(who)) ⇒ + who ! "hello" + Actor.same + //#under-test + } + +} + +class BasicSyncTestingSpec extends WordSpec with Matchers { + + import BasicSyncTestingSpec._ + + "Typed actor synchronous testing" must { + + "record spawning" in { + //#test-child + val testKit = BehaviorTestkit(myBehaviour) + testKit.run(CreateChild("child")) + testKit.expectEffect(Spawned(childActor, "child")) + //#test-child + } + + "record spawning anonymous" in { + //#test-anonymous-child + val testKit = BehaviorTestkit(myBehaviour) + testKit.run(CreateAnonymousChild) + testKit.expectEffect(SpawnedAnonymous(childActor)) + //#test-anonymous-child + } + + "record message sends" in { + //#test-message + val testKit = BehaviorTestkit(myBehaviour) + val inbox = TestInbox[String]() + testKit.run(SayHello(inbox.ref)) + inbox.expectMsg("hello") + //#test-message + } + + "send a message to a spawned child" in { + //#test-child-message + val testKit = BehaviorTestkit(myBehaviour) + testKit.run(SayHelloToChild("child")) + val childInbox = testKit.childInbox[String]("child") + childInbox.expectMsg("hello") + //#test-child-message + } + + "send a message to an anonymous spawned child" in { + //#test-child-message-anonymous + val testKit = BehaviorTestkit(myBehaviour) + testKit.run(SayHelloToAnonymousChild) + // Anonymous actors are created as: $a $b etc + val childInbox = testKit.childInbox[String]("$a") + childInbox.expectMsg("hello stranger") + //#test-child-message-anonymous + } + } +} diff --git a/akka-actor/src/main/java/akka/annotation/ApiMayChange.java b/akka-actor/src/main/java/akka/annotation/ApiMayChange.java index c39784e9eb..462f4669a3 100644 --- a/akka-actor/src/main/java/akka/annotation/ApiMayChange.java +++ b/akka-actor/src/main/java/akka/annotation/ApiMayChange.java @@ -10,7 +10,7 @@ import java.lang.annotation.*; * Marks APIs that are meant to evolve towards becoming stable APIs, but are not stable APIs yet. * * Evolving interfaces MAY change from one patch release to another (i.e. 2.4.10 to 2.4.11) without up-front notice. - * A best-effort approach is taken to not cause more breakage than really neccessary, and usual deprecation techniques + * A best-effort approach is taken to not cause more breakage than really necessary, and usual deprecation techniques * are utilised while evolving these APIs, however there is NO strong guarantee regarding the source or binary * compatibility of APIs marked using this annotation. * diff --git a/akka-cluster-sharding-typed/src/test/scala/akka/cluster/sharding/typed/ClusterShardingPersistenceSpec.scala b/akka-cluster-sharding-typed/src/test/scala/akka/cluster/sharding/typed/ClusterShardingPersistenceSpec.scala index dabd7ec383..89cf849d7c 100644 --- a/akka-cluster-sharding-typed/src/test/scala/akka/cluster/sharding/typed/ClusterShardingPersistenceSpec.scala +++ b/akka-cluster-sharding-typed/src/test/scala/akka/cluster/sharding/typed/ClusterShardingPersistenceSpec.scala @@ -10,8 +10,8 @@ import org.scalatest.concurrent.ScalaFutures import scala.concurrent.duration._ import akka.actor.typed.Behavior -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl.TestProbe import akka.persistence.typed.scaladsl.PersistentActor import akka.persistence.typed.scaladsl.PersistentActor.PersistNothing diff --git a/akka-cluster-sharding-typed/src/test/scala/akka/cluster/sharding/typed/ClusterShardingSpec.scala b/akka-cluster-sharding-typed/src/test/scala/akka/cluster/sharding/typed/ClusterShardingSpec.scala index e1f58112c4..a889cc0e78 100644 --- a/akka-cluster-sharding-typed/src/test/scala/akka/cluster/sharding/typed/ClusterShardingSpec.scala +++ b/akka-cluster-sharding-typed/src/test/scala/akka/cluster/sharding/typed/ClusterShardingSpec.scala @@ -10,8 +10,8 @@ import akka.cluster.typed.Cluster import akka.actor.typed.internal.adapter.ActorSystemAdapter import akka.actor.typed.scaladsl.Actor import akka.actor.typed.scaladsl.adapter._ -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl.TestProbe import com.typesafe.config.ConfigFactory import org.scalatest.concurrent.ScalaFutures diff --git a/akka-cluster-typed/src/test/java/akka/cluster/typed/ClusterApiTest.java b/akka-cluster-typed/src/test/java/akka/cluster/typed/ClusterApiTest.java index 9959bfaffb..07efea4852 100644 --- a/akka-cluster-typed/src/test/java/akka/cluster/typed/ClusterApiTest.java +++ b/akka-cluster-typed/src/test/java/akka/cluster/typed/ClusterApiTest.java @@ -2,8 +2,8 @@ package akka.cluster.typed; import akka.cluster.ClusterEvent; import akka.actor.typed.ActorSystem; -import akka.typed.testkit.TestKitSettings; -import akka.typed.testkit.javadsl.TestProbe; +import akka.testkit.typed.TestKitSettings; +import akka.testkit.typed.javadsl.TestProbe; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import org.junit.Test; diff --git a/akka-cluster-typed/src/test/scala/akka/cluster/ddata/typed/scaladsl/ReplicatorSpec.scala b/akka-cluster-typed/src/test/scala/akka/cluster/ddata/typed/scaladsl/ReplicatorSpec.scala index d8f0df3d68..bdc21c2cb1 100644 --- a/akka-cluster-typed/src/test/scala/akka/cluster/ddata/typed/scaladsl/ReplicatorSpec.scala +++ b/akka-cluster-typed/src/test/scala/akka/cluster/ddata/typed/scaladsl/ReplicatorSpec.scala @@ -8,8 +8,8 @@ import akka.actor.typed.{ ActorRef, ActorSystem, Behavior, StartSupport, TypedSp import akka.actor.typed.scaladsl.Actor import akka.actor.typed.scaladsl.AskPattern._ import akka.actor.typed.scaladsl.adapter._ -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl._ +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl._ import akka.cluster.Cluster import akka.cluster.ddata.{ GCounter, GCounterKey, ReplicatedData } import akka.cluster.ddata.typed.scaladsl.Replicator._ diff --git a/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterApiSpec.scala b/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterApiSpec.scala index 0b7bb3a96f..7abefc7289 100644 --- a/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterApiSpec.scala +++ b/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterApiSpec.scala @@ -8,8 +8,8 @@ import akka.cluster.MemberStatus import akka.actor.typed.TypedSpec import akka.actor.typed.internal.adapter.ActorSystemAdapter import akka.actor.typed.scaladsl.adapter._ -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl.TestProbe import com.typesafe.config.ConfigFactory import org.scalatest.concurrent.ScalaFutures diff --git a/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterSingletonApiSpec.scala b/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterSingletonApiSpec.scala index 69754b9384..9e35683009 100644 --- a/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterSingletonApiSpec.scala +++ b/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterSingletonApiSpec.scala @@ -9,8 +9,8 @@ import akka.actor.ExtendedActorSystem import akka.serialization.SerializerWithStringManifest import akka.actor.typed.scaladsl.Actor import akka.actor.typed.scaladsl.adapter._ -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl.TestProbe import akka.actor.typed.{ ActorRef, ActorRefResolver, Props, TypedSpec } import com.typesafe.config.ConfigFactory import org.scalatest.concurrent.ScalaFutures diff --git a/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterSingletonPersistenceSpec.scala b/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterSingletonPersistenceSpec.scala index 734bd5694a..cef9a29b29 100644 --- a/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterSingletonPersistenceSpec.scala +++ b/akka-cluster-typed/src/test/scala/akka/cluster/typed/ClusterSingletonPersistenceSpec.scala @@ -10,8 +10,8 @@ import akka.actor.typed.Props import akka.actor.typed.TypedSpec import akka.persistence.typed.scaladsl.PersistentActor import akka.persistence.typed.scaladsl.PersistentActor.{ CommandHandler, Effect } -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl.TestProbe import com.typesafe.config.ConfigFactory import org.scalatest.concurrent.ScalaFutures diff --git a/akka-cluster-typed/src/test/scala/akka/cluster/typed/internal/receptionist/ClusterReceptionistSpec.scala b/akka-cluster-typed/src/test/scala/akka/cluster/typed/internal/receptionist/ClusterReceptionistSpec.scala index ac8c57bf4a..66d069f32d 100644 --- a/akka-cluster-typed/src/test/scala/akka/cluster/typed/internal/receptionist/ClusterReceptionistSpec.scala +++ b/akka-cluster-typed/src/test/scala/akka/cluster/typed/internal/receptionist/ClusterReceptionistSpec.scala @@ -13,8 +13,8 @@ import akka.actor.typed.internal.adapter.ActorSystemAdapter import akka.actor.typed.receptionist.Receptionist import akka.actor.typed.scaladsl.Actor import akka.actor.typed.scaladsl.adapter._ -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl.TestProbe +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl.TestProbe import com.typesafe.config.ConfigFactory import scala.concurrent.Await diff --git a/akka-docs/src/main/paradox/actors-typed.md b/akka-docs/src/main/paradox/actors-typed.md index c3ece8bf2a..6b279d8357 100644 --- a/akka-docs/src/main/paradox/actors-typed.md +++ b/akka-docs/src/main/paradox/actors-typed.md @@ -9,7 +9,13 @@ This module is currently marked as @ref:[may change](common/may-change.md) in th @@@ -## Dependency +### Migrating to 2.5.9 + +* `EffectfulActorContext` has been renamed to `BehaviourTestKit` +* `Inbox` has been renamed to `TestInbox` to allign with `TestProbe` +* Separated into modules e.g. `akka-actor-typed` `akka-persistence-typed` along with matching package names + +### Dependency To use typed actors add the following dependency: diff --git a/akka-docs/src/main/paradox/cluster-sharding-typed.md b/akka-docs/src/main/paradox/cluster-sharding-typed.md index 7cd6a9548d..2d9c352a52 100644 --- a/akka-docs/src/main/paradox/cluster-sharding-typed.md +++ b/akka-docs/src/main/paradox/cluster-sharding-typed.md @@ -2,7 +2,7 @@ TODO -## Dependency +### Dependency @@dependency [sbt,Maven,Gradle] { group=com.typesafe.akka diff --git a/akka-docs/src/main/paradox/cluster-typed.md b/akka-docs/src/main/paradox/cluster-typed.md index 3d06925dbc..466cc34fda 100644 --- a/akka-docs/src/main/paradox/cluster-typed.md +++ b/akka-docs/src/main/paradox/cluster-typed.md @@ -1,9 +1,5 @@ # Cluster -TODO - -## Dependency - sbt : @@@vars ``` @@ -29,4 +25,6 @@ Maven $akka.version$ ``` - @@@ \ No newline at end of file + @@@ + +TODO diff --git a/akka-docs/src/main/paradox/testing-typed.md b/akka-docs/src/main/paradox/testing-typed.md index e9c43d5b9a..333446eeab 100644 --- a/akka-docs/src/main/paradox/testing-typed.md +++ b/akka-docs/src/main/paradox/testing-typed.md @@ -11,33 +11,165 @@ This module is currently marked as @ref:[may change](common/may-change.md) in th @@@ To use the testkit add the following dependency: -## Dependency +To use the testkit add the following dependency: -sbt -: @@@vars - ``` - "com.typesafe.akka" %% "akka-testkit-typed" % "$akka.version$" - ``` - @@@ +@@dependency [sbt,Maven,Gradle] { + group=com.typesafe.akka + artifact=akka-testkit-typed_2.11 + version=$version$ + scope=test +} + +Testing can either be done asynchronously using a real `ActorSystem` or synchronously on the testing thread using the `BehaviousTestKit`. -Gradle -: @@@vars - ``` - dependencies { - compile group: 'com.typesafe.akka', name: 'akka-testkit-typed_2.11', version: '$akka.version$' - } - ``` - @@@ +For testing logic in a `Behavior` in isolation synchronous testing is preferred. For testing interactions between multiple +actors a more realistic asynchronous test is preferred. -Maven -: @@@vars - ``` - - com.typesafe.akka - akka-testkit-typed_$scala.binary_version$ - $akka.version$ - - ``` - @@@ +Certain `Behavior`s will be hard to test synchronously e.g. if they spawn Future's and you rely on a callback to complete +before observing the effect you want to test. Further support for controlling the scheduler and execution context used +will be added. -TODO \ No newline at end of file +## Synchronous behaviour testing + +The following demonstrates how to test: + +* Spawning child actors +* Spawning child actors anonymously +* Sending a message either as a reply or to another actor +* Sending a message to a child actor + +The examples below require the following imports: + +Scala +: @@snip [BasicSyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala) { #imports } + +Java +: @@snip [BasicSyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java) { #imports } + +Each of the tests are testing an actor that based on the message executes a different effect to be tested: + +Scala +: @@snip [BasicSyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala) { #under-test } + +Java +: @@snip [BasicSyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java) { #under-test } + +For creating a child actor a noop actor is created: + + +Scala +: @@snip [BasicSyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala) { #child } + +Java +: @@snip [BasicSyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java) { #child } + +All of the tests make use of the `BehaviourTestkit` to avoid the need for a real `ActorContext`. Some of the tests +make use of the `TestInbox` which allows the creation of an `ActorRef` that can be used for synchronous testing, similar to the +`TestProbe` used for asynchronous testing. + + +### Spawning children + +With a name: + +Scala +: @@snip [BasicSyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala) { #test-child } + +Java +: @@snip [BasicSyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java) { #test-child } + +Anonymously: + +Scala +: @@snip [BasicSyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala) { #test-anonymous-child } + +Java +: @@snip [BasicSyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java) { #test-anonymous-child } + +### Sending messages + +For testing sending a message a `TestInbox` is created that provides an `ActorRef` and methods to assert against the +messages that have been sent to it. + +Scala +: @@snip [BasicSyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala) { #test-message } + +Java +: @@snip [BasicSyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java) { #test-message } + +Another use case is sending a message to a child actor you can do this by looking up the 'TestInbox' for +a child actor from the 'BehaviorTestKit': + +Scala +: @@snip [BasicSyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala) { #test-child-message } + +Java +: @@snip [BasicSyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java) { #test-child-message } + +For anonymous children the actor names are generated in a deterministic way: + +Scala +: @@snip [BasicSyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/sync/BasicSyncTestingSpec.scala) { #test-child-message-anonymous } + +Java +: @@snip [BasicSyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/sync/BasicSyncTestingTest.java) { #test-child-message-anonymous } + +### Testing other effects + +The `BehaviorTestkit` keeps track other effects you can verify, look at the sub-classes of `akka.testkit.typed.Effect` + + * SpawnedAdapter + * Stopped + * Watched + * Unwatched + * Scheduled + +See the other public methods and API documentation on `BehaviourTestkit` for other types of verification. + +## Asynchronous testing + +Asynchronous testing uses a real `ActorSystem` that allows you to test your Actors in a realistic environment. + +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. + +### Basic example + +Actor under test: + +Scala +: @@snip [BasicAsyncTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/async/BasicAsyncTestingSpec.scala) { #under-test } + +Java +: @@snip [BasicAsyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/async/BasicAsyncTestingTest.java) { #under-test } + +Tests can optionally extend `TestKit` or include the `TestKitBase`. + +Scala +: @@snip [BasicTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/async/BasicAsyncTestingSpec.scala) { #test-header } + +Java +: @@snip [BasicAsyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/async/BasicAsyncTestingTest.java) { #test-header } + +Your test is responsible for shutting down the `ActorSystem` e.g. using `BeforeAndAfterAll` when using ScalaTest + +Scala +: @@snip [BasicTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/async/BasicAsyncTestingSpec.scala) { #test-shutdown } + +Java +: @@snip [BasicAsyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/async/BasicAsyncTestingTest.java) { #test-shutdown } + +The following demonstrates: + +* Creating a typed actor from the `TestKit`'s system using `actorOf` +* Creating a typed `TestProbe` +* Verifying that the actor under test responds via the `TestProbe` + +Scala +: @@snip [BasicTestingSpec.scala]($akka$/akka-actor-typed-tests/src/test/scala/docs/akka/typed/testing/async/BasicAsyncTestingSpec.scala) { #test } + +Java +: @@snip [BasicAsyncTestingTest.java]($akka$/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/testing/async/BasicAsyncTestingTest.java) { #test } + diff --git a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorSpec.scala b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorSpec.scala index a552eca8d9..5c330f7a0f 100644 --- a/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorSpec.scala +++ b/akka-persistence-typed/src/test/scala/akka/persistence/typed/scaladsl/PersistentActorSpec.scala @@ -6,13 +6,10 @@ package akka.persistence.typed.scaladsl import scala.concurrent.duration._ import akka.actor.typed.{ ActorRef, ActorSystem, Behavior, StartSupport, SupervisorStrategy, Terminated, TypedSpec } import akka.actor.typed.scaladsl.Actor -import akka.actor.typed.scaladsl.AskPattern._ -import akka.actor.typed.scaladsl.adapter._ -import akka.typed.testkit.TestKitSettings -import akka.typed.testkit.scaladsl._ +import akka.testkit.typed.TestKitSettings +import akka.testkit.typed.scaladsl._ import com.typesafe.config.ConfigFactory import org.scalatest.concurrent.Eventually -import akka.util.Timeout import akka.persistence.typed.scaladsl.PersistentActor._ object PersistentActorSpec { diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ActorSystemStub.scala b/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ActorSystemStub.scala similarity index 96% rename from akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ActorSystemStub.scala rename to akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ActorSystemStub.scala index 3b19c44b63..c76e212fdc 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ActorSystemStub.scala +++ b/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ActorSystemStub.scala @@ -4,18 +4,21 @@ package akka.actor.typed package internal -import akka.{ actor ⇒ a, event ⇒ e } - -import scala.concurrent._ -import com.typesafe.config.ConfigFactory import java.util.concurrent.ThreadFactory +import akka.annotation.InternalApi import akka.event.Logging import akka.event.typed.{ BusLogging, DefaultLoggingFilter, EventStream } -import akka.event.typed.{ BusLogging, DefaultLoggingFilter } import akka.util.Timeout +import akka.{ actor ⇒ a, event ⇒ e } +import com.typesafe.config.ConfigFactory -private[typed] class ActorSystemStub(val name: String) +import scala.concurrent._ + +/** + * INTERNAL API + */ +@InternalApi private[akka] class ActorSystemStub(val name: String) extends ActorSystem[Nothing] with ActorRef[Nothing] with ActorRefImpl[Nothing] { override val path: a.ActorPath = a.RootActorPath(a.Address("akka", name)) / "user" diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ControlledExecutor.scala b/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ControlledExecutor.scala similarity index 99% rename from akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ControlledExecutor.scala rename to akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ControlledExecutor.scala index 91ade6fcf6..4feb402870 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/ControlledExecutor.scala +++ b/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/ControlledExecutor.scala @@ -3,9 +3,10 @@ */ package akka.actor.typed.internal -import scala.concurrent.ExecutionContextExecutor import java.util.LinkedList +import scala.concurrent.ExecutionContextExecutor + class ControlledExecutor extends ExecutionContextExecutor { private val tasks = new LinkedList[Runnable] diff --git a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/DebugRef.scala b/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/DebugRef.scala similarity index 90% rename from akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/DebugRef.scala rename to akka-testkit-typed/src/main/scala/akka/actor/typed/internal/DebugRef.scala index 71dd9c51e6..fff22e7cf4 100644 --- a/akka-actor-typed-tests/src/test/scala/akka/actor/typed/internal/DebugRef.scala +++ b/akka-testkit-typed/src/main/scala/akka/actor/typed/internal/DebugRef.scala @@ -4,11 +4,17 @@ package akka.actor.typed package internal -import akka.{ actor ⇒ a } import java.util.concurrent.ConcurrentLinkedQueue + +import akka.annotation.InternalApi +import akka.{ actor ⇒ a } + import scala.annotation.tailrec -private[typed] class DebugRef[T](override val path: a.ActorPath, override val isLocal: Boolean) +/** + * INTERNAL API + */ +@InternalApi private[akka] class DebugRef[T](override val path: a.ActorPath, override val isLocal: Boolean) extends ActorRef[T] with ActorRefImpl[T] { private val q = new ConcurrentLinkedQueue[Either[SystemMessage, T]] diff --git a/akka-testkit-typed/src/main/scala/akka/testkit/typed/BehaviourTestkit.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/BehaviourTestkit.scala new file mode 100644 index 0000000000..050500e0db --- /dev/null +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/BehaviourTestkit.scala @@ -0,0 +1,206 @@ +/** + * Copyright (C) 2014-2017 Lightbend Inc. + */ +package akka.testkit.typed + +import java.util.concurrent.ConcurrentLinkedQueue + +import akka.actor.typed.{ ActorRef, Behavior, PostStop, Props, Signal } +import akka.annotation.{ ApiMayChange, InternalApi } + +import scala.annotation.tailrec +import scala.collection.immutable +import scala.util.control.Exception.Catcher +import scala.util.control.NonFatal +import scala.concurrent.duration.{ Duration, FiniteDuration } + +import scala.language.existentials + +/** + * All tracked effects must extend implement this type. It is deliberately + * not sealed in order to allow extensions. + */ +abstract class Effect + +// TODO offer a better Java API for default params that are rarely used e.g. props +@ApiMayChange +object Effect { + + abstract class SpawnedEffect extends Effect + + @SerialVersionUID(1L) final case class Spawned(behavior: Behavior[_], childName: String, props: Props = Props.empty) extends SpawnedEffect + @SerialVersionUID(1L) final case class SpawnedAnonymous(behaviour: Behavior[_], props: Props = Props.empty) extends SpawnedEffect + @SerialVersionUID(1L) final case object SpawnedAdapter extends SpawnedEffect + @SerialVersionUID(1L) final case class Stopped(childName: String) extends Effect + @SerialVersionUID(1L) final case class Watched[T](other: ActorRef[T]) extends Effect + @SerialVersionUID(1L) final case class Unwatched[T](other: ActorRef[T]) extends Effect + @SerialVersionUID(1L) final case class ReceiveTimeoutSet[T](d: Duration, msg: T) extends Effect + @SerialVersionUID(1L) final case class Scheduled[U](delay: FiniteDuration, target: ActorRef[U], msg: U) extends Effect + @SerialVersionUID(1L) case object NoEffects extends Effect + +} + +/** + * INTERNAL API + */ +@InternalApi private[akka] class EffectfulActorContext[T](name: String) extends StubbedActorContext[T](name) { + + import Effect._ + import akka.{ actor ⇒ a } + + /** + * INTERNAL API + */ + @InternalApi private[akka] val effectQueue = new ConcurrentLinkedQueue[Effect] + + override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] = { + val ref = super.spawnAnonymous(behavior, props) + effectQueue.offer(SpawnedAnonymous(behavior, props)) + ref + } + + override def spawnAdapter[U](f: U ⇒ T): ActorRef[U] = { + spawnAdapter(f, "") + } + + override def spawnAdapter[U](f: U ⇒ T, name: String): ActorRef[U] = { + val ref = super.spawnAdapter(f, name) + effectQueue.offer(SpawnedAdapter) + ref + } + override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = { + effectQueue.offer(Spawned(behavior, name, props)) + super.spawn(behavior, name, props) + } + override def stop[U](child: ActorRef[U]): Boolean = { + effectQueue.offer(Stopped(child.path.name)) + super.stop(child) + } + override def watch[U](other: ActorRef[U]): Unit = { + effectQueue.offer(Watched(other)) + super.watch(other) + } + override def unwatch[U](other: ActorRef[U]): Unit = { + effectQueue.offer(Unwatched(other)) + super.unwatch(other) + } + override def setReceiveTimeout(d: FiniteDuration, msg: T): Unit = { + effectQueue.offer(ReceiveTimeoutSet(d, msg)) + super.setReceiveTimeout(d, msg) + } + override def cancelReceiveTimeout(): Unit = { + effectQueue.offer(ReceiveTimeoutSet(Duration.Undefined, null)) + super.cancelReceiveTimeout() + } + override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): a.Cancellable = { + effectQueue.offer(Scheduled(delay, target, msg)) + super.schedule(delay, target, msg) + } +} + +@ApiMayChange +object BehaviorTestkit { + def apply[T](initialBehavior: Behavior[T], name: String): BehaviorTestkit[T] = + new BehaviorTestkit[T](name, initialBehavior) + def apply[T](initialBehavior: Behavior[T]): BehaviorTestkit[T] = + apply(initialBehavior, "testkit") + + /** + * JAVA API + */ + def create[T](initialBehavior: Behavior[T], name: String): BehaviorTestkit[T] = + new BehaviorTestkit[T](name, initialBehavior) + /** + * JAVA API + */ + def create[T](initialBehavior: Behavior[T]): BehaviorTestkit[T] = + apply(initialBehavior, "ctx") +} + +/** + * Used for testing [[Behavior]]s. Stores all effects e.g. Spawning of children, + * watching and offers access to what effects have taken place. + */ +@ApiMayChange +class BehaviorTestkit[T](_name: String, _initialBehavior: Behavior[T]) { + + import Effect._ + + // really this should be private, make so when we port out tests that need it + private[akka] val ctx = new EffectfulActorContext[T](_name) + + /** + * Requests the oldest [[Effect]] or [[NoEffects]] if no effects + * have taken place. The effect is consumed, subsequent calls won't + * will not include this effect. + */ + def retrieveEffect(): Effect = ctx.effectQueue.poll() match { + case null ⇒ NoEffects + case x ⇒ x + } + + def childInbox[U](name: String): TestInbox[U] = { + val inbox = ctx.childInbox[U](name) + assert(inbox.isDefined, s"Child not created: $name. Children created: [${ctx.childrenNames.mkString(",")}]") + inbox.get + } + + def selfInbox(): TestInbox[T] = ctx.selfInbox + + /** + * Requests all the effects. The effects are consumed, subsequent calls will only + * see new effects. + */ + def retrieveAllEffects(): immutable.Seq[Effect] = { + @tailrec def rec(acc: List[Effect]): List[Effect] = ctx.effectQueue.poll() match { + case null ⇒ acc.reverse + case x ⇒ rec(x :: acc) + } + + rec(Nil) + } + + /** + * Asserts that the oldest effect is the expectedEffect. Removing it from + * further assertions. + */ + def expectEffect(expectedEffect: Effect): Unit = { + ctx.effectQueue.poll() match { + case null ⇒ assert(assertion = false, s"expected: $expectedEffect but no effects were recorded") + case effect ⇒ assert(expectedEffect == effect, s"expected: $expectedEffect but for $effect") + } + } + + private var current = Behavior.validateAsInitial(Behavior.undefer(_initialBehavior, ctx)) + + def currentBehavior: Behavior[T] = current + def isAlive: Boolean = Behavior.isAlive(current) + + private def handleException: Catcher[Unit] = { + case NonFatal(e) ⇒ + try Behavior.canonicalize(Behavior.interpretSignal(current, ctx, PostStop), current, ctx) // TODO why canonicalize here? + catch { + case NonFatal(_) ⇒ /* ignore, real is logging */ + } + throw e + } + + /** + * Send the msg to the behavior and record any [[Effect]]s + */ + def run(msg: T): Unit = { + try { + current = Behavior.canonicalize(Behavior.interpretMessage(current, ctx, msg), current, ctx) + } catch handleException + } + + /** + * Send the signal to the beheavior and record any [[Effect]]s + */ + def signal(signal: Signal): Unit = { + try { + current = Behavior.canonicalize(Behavior.interpretSignal(current, ctx, signal), current, ctx) + } catch handleException + } + +} diff --git a/akka-testkit-typed/src/main/scala/akka/typed/testkit/StubbedActorContext.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/StubbedActorContext.scala similarity index 79% rename from akka-testkit-typed/src/main/scala/akka/typed/testkit/StubbedActorContext.scala rename to akka-testkit-typed/src/main/scala/akka/testkit/typed/StubbedActorContext.scala index b8b9934d08..c1181fdb0e 100644 --- a/akka-testkit-typed/src/main/scala/akka/typed/testkit/StubbedActorContext.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/StubbedActorContext.scala @@ -1,4 +1,4 @@ -package akka.typed.testkit +package akka.testkit.typed import akka.actor.InvalidMessageException import akka.{ actor ⇒ untyped } @@ -11,7 +11,7 @@ import scala.collection.immutable.TreeMap import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration.FiniteDuration import akka.annotation.InternalApi -import akka.actor.typed.internal.{ ActorContextImpl, ActorRefImpl } +import akka.actor.typed.internal.{ ActorContextImpl, ActorRefImpl, ActorSystemStub } import scala.annotation.tailrec import scala.util.control.NonFatal @@ -31,7 +31,7 @@ private[akka] final class FunctionRef[-T]( if (msg == null) throw InvalidMessageException("[null] is not an allowed message") if (isAlive) try send(msg, this) catch { - case NonFatal(ex) ⇒ // nothing we can do here + case NonFatal(_) ⇒ // nothing we can do here } else () // we don’t have deadLetters available } @@ -39,12 +39,12 @@ private[akka] final class FunctionRef[-T]( import internal._ override def sendSystem(signal: SystemMessage): Unit = signal match { - case internal.Create() ⇒ // nothing to do - case internal.DeathWatchNotification(ref, cause) ⇒ // we’re not watching, and we’re not a parent either - case internal.Terminate() ⇒ doTerminate() - case internal.Watch(watchee, watcher) ⇒ if (watchee == this && watcher != this) addWatcher(watcher.sorryForNothing) - case internal.Unwatch(watchee, watcher) ⇒ if (watchee == this && watcher != this) remWatcher(watcher.sorryForNothing) - case NoMessage ⇒ // nothing to do + case internal.Create() ⇒ // nothing to do + case internal.DeathWatchNotification(_, _) ⇒ // we’re not watching, and we’re not a parent either + case internal.Terminate() ⇒ doTerminate() + case internal.Watch(watchee, watcher) ⇒ if (watchee == this && watcher != this) addWatcher(watcher.sorryForNothing) + case internal.Unwatch(watchee, watcher) ⇒ if (watchee == this && watcher != this) remWatcher(watcher.sorryForNothing) + case NoMessage ⇒ // nothing to do } override def isLocal = true @@ -105,27 +105,37 @@ private[typed] object WatchableRef { } /** + * INTERNAL API + * * An [[ActorContext]] for synchronous execution of a [[Behavior]] that * provides only stubs for the effects an Actor can perform and replaces * created child Actors by a synchronous Inbox (see `Inbox.sync`). * - * See [[EffectfulActorContext]] for more advanced uses. + * See [[BehaviorTestkit]] for more advanced uses. */ -class StubbedActorContext[T]( - val name: String, - override val mailboxCapacity: Int, - override val system: ActorSystem[Nothing]) extends ActorContextImpl[T] { +@InternalApi private[akka] class StubbedActorContext[T]( + val name: String) extends ActorContextImpl[T] { + + /** + * INTERNAL API + */ + @InternalApi private[akka] val selfInbox = TestInbox[T](name) - val selfInbox = Inbox[T](name) override val self = selfInbox.ref + override val system = new ActorSystemStub("StubbedActorContext") + // Not used for a stubbed actor context + override def mailboxCapacity = 1 - private var _children = TreeMap.empty[String, Inbox[_]] - private val childName = Iterator from 1 map (Helpers.base64(_)) + private var _children = TreeMap.empty[String, TestInbox[_]] + private val childName = Iterator from 0 map (Helpers.base64(_)) override def children: Iterable[ActorRef[Nothing]] = _children.values map (_.ref) + def childrenNames: Iterable[String] = _children.keys + override def child(name: String): Option[ActorRef[Nothing]] = _children get name map (_.ref) + override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] = { - val i = Inbox[U](childName.next()) + val i = TestInbox[U](childName.next()) _children += i.ref.path.name → i i.ref } @@ -134,7 +144,7 @@ class StubbedActorContext[T]( case Some(_) ⇒ throw untyped.InvalidActorNameException(s"actor name $name is already taken") case None ⇒ // FIXME correct child path for the Inbox ref - val i = Inbox[U](name) + val i = TestInbox[U](name) _children += name → i i.ref } @@ -160,6 +170,7 @@ class StubbedActorContext[T]( override def isCancelled = true } + // TODO allow overriding of this override def executionContext: ExecutionContextExecutor = system.executionContext /** @@ -168,7 +179,7 @@ class StubbedActorContext[T]( @InternalApi private[akka] def internalSpawnAdapter[U](f: U ⇒ T, name: String): ActorRef[U] = { val n = if (name != "") s"${childName.next()}-$name" else childName.next() - val i = Inbox[U](n) + val i = TestInbox[U](n) _children += i.ref.path.name → i new FunctionRef[U]( @@ -181,8 +192,8 @@ class StubbedActorContext[T]( * Retrieve the inbox representing the given child actor. The passed ActorRef must be one that was returned * by one of the spawn methods earlier. */ - def childInbox[U](child: ActorRef[U]): Inbox[U] = { - val inbox = _children(child.path.name).asInstanceOf[Inbox[U]] + def childInbox[U](child: ActorRef[U]): TestInbox[U] = { + val inbox = _children(child.path.name).asInstanceOf[TestInbox[U]] if (inbox.ref != child) throw new IllegalArgumentException(s"$child is not a child of $this") inbox } @@ -190,7 +201,7 @@ class StubbedActorContext[T]( /** * Retrieve the inbox representing the child actor with the given name. */ - def childInbox[U](name: String): Inbox[U] = _children(name).asInstanceOf[Inbox[U]] + def childInbox[U](name: String): Option[TestInbox[U]] = _children.get(name).map(_.asInstanceOf[TestInbox[U]]) /** * Remove the given inbox from the list of children, for example after diff --git a/akka-testkit-typed/src/main/scala/akka/typed/testkit/TestEventListener.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestEventListener.scala similarity index 96% rename from akka-testkit-typed/src/main/scala/akka/typed/testkit/TestEventListener.scala rename to akka-testkit-typed/src/main/scala/akka/testkit/typed/TestEventListener.scala index f87df9931d..1c7f6a28a1 100644 --- a/akka-testkit-typed/src/main/scala/akka/typed/testkit/TestEventListener.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestEventListener.scala @@ -1,4 +1,4 @@ -package akka.typed.testkit +package akka.testkit.typed import akka.event.Logging.{ LogEvent, StdOutLogger } import akka.testkit.{ EventFilter, TestEvent ⇒ TE } @@ -16,7 +16,7 @@ import akka.event.typed.Logger * *

  * akka.typed {
- *   loggers = ["akka.typed.testkit.TestEventListener"]
+ *   loggers = ["akka.testkit.typed.TestEventListener"]
  * }
  * 
*/ diff --git a/akka-testkit-typed/src/main/scala/akka/typed/testkit/Inbox.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestInbox.scala similarity index 52% rename from akka-testkit-typed/src/main/scala/akka/typed/testkit/Inbox.scala rename to akka-testkit-typed/src/main/scala/akka/testkit/typed/TestInbox.scala index 8e048eb53c..355ad446bc 100644 --- a/akka-testkit-typed/src/main/scala/akka/typed/testkit/Inbox.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestInbox.scala @@ -1,23 +1,26 @@ /** * Copyright (C) 2014-2017 Lightbend Inc. */ -package akka.typed.testkit +package akka.testkit.typed import java.util.concurrent.{ ConcurrentLinkedQueue, ThreadLocalRandom } import akka.actor.{ Address, RootActorPath } import akka.actor.typed.ActorRef +import akka.annotation.ApiMayChange import scala.annotation.tailrec import scala.collection.immutable /** - * Utility for receiving messages outside of an actor. No methods are provided - * for synchronously awaiting a message, this is primarily useful for synchronous - * tests of behaviors that send messages to other actors, where an Inbox’s ActorRef - * can conveniently be used as a stub. + * Utility for use as an [[ActorRef]] when synchronously testing [[akka.actor.typed.Behavior]] + * to be used along with [[BehaviorTestkit]]. + * + * See [[akka.testkit.typed.scaladsl.TestProbe]] for asynchronous testing. */ -class Inbox[T](name: String) { +@ApiMayChange +class TestInbox[T](name: String) { + def this() = this("inbox") private val q = new ConcurrentLinkedQueue[T] @@ -27,22 +30,40 @@ class Inbox[T](name: String) { new FunctionRef[T](path, (msg, self) ⇒ q.add(msg), (self) ⇒ ()) } + /** + * Get and remove the oldest message + */ def receiveMsg(): T = q.poll() match { case null ⇒ throw new NoSuchElementException(s"polling on an empty inbox: $name") case x ⇒ x } + /** + * Assert and remove the the oldest message. + */ + def expectMsg(expectedMessage: T): TestInbox[T] = { + q.poll() match { + case null ⇒ assert(assertion = false, s"expected msg: $expectedMessage but no messages were received") + case message ⇒ assert(message == expectedMessage, s"expected: $expectedMessage but received $message") + } + this + } + def receiveAll(): immutable.Seq[T] = { @tailrec def rec(acc: List[T]): List[T] = q.poll() match { case null ⇒ acc.reverse case x ⇒ rec(x :: acc) } + rec(Nil) } def hasMessages: Boolean = q.peek() != null + + // TODO expectNoMsg etc } -object Inbox { - def apply[T](name: String): Inbox[T] = new Inbox(name) +@ApiMayChange +object TestInbox { + def apply[T](name: String = "inbox"): TestInbox[T] = new TestInbox(name) } diff --git a/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestKit.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestKit.scala new file mode 100644 index 0000000000..4f46ffdbc7 --- /dev/null +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestKit.scala @@ -0,0 +1,51 @@ +package akka.testkit.typed + +import akka.actor.typed.{ ActorRef, ActorSystem, Behavior } +import akka.annotation.ApiMayChange +import akka.util.Timeout + +import scala.concurrent.duration._ +import scala.concurrent.{ Await, TimeoutException } + +/** + * Testkit for typed actors. Extending this removes some boiler plate when testing + * typed actors. + * + * If a test can't extend then use the [[TestKitBase]] trait + * + * @param _system The [ActorSystem] for the test + */ +@ApiMayChange +class TestKit(_system: ActorSystem[_]) extends TestKitBase { + implicit val system = _system +} + +@ApiMayChange +trait TestKitBase { + def system: ActorSystem[_] + implicit def testkitSettings = TestKitSettings(system) + + def shutdown(): Unit = { + shutdown(system, 5.seconds) + } + + def shutdown( + actorSystem: ActorSystem[_], + duration: Duration, + verifySystemShutdown: Boolean = false): Unit = { + system.terminate() + try Await.ready(actorSystem.whenTerminated, duration) catch { + case _: TimeoutException ⇒ + val msg = "Failed to stop [%s] within [%s] \n%s".format(actorSystem.name, duration, + actorSystem.printTree) + if (verifySystemShutdown) throw new RuntimeException(msg) + else println(msg) + } + } + + // The only current impl of a typed actor system returns a Future.successful currently + // hence the hardcoded timeouts + def actorOf[T](behaviour: Behavior[T], name: String): ActorRef[T] = + Await.result(system.systemActorOf(behaviour, name)(Timeout(20.seconds)), 21.seconds) +} + diff --git a/akka-testkit-typed/src/main/scala/akka/typed/testkit/TestKitSettings.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestKitSettings.scala similarity index 97% rename from akka-testkit-typed/src/main/scala/akka/typed/testkit/TestKitSettings.scala rename to akka-testkit-typed/src/main/scala/akka/testkit/typed/TestKitSettings.scala index 837be00799..e7ee93d7ae 100644 --- a/akka-testkit-typed/src/main/scala/akka/typed/testkit/TestKitSettings.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/TestKitSettings.scala @@ -1,7 +1,7 @@ /** * Copyright (C) 2017 Lightbend Inc. */ -package akka.typed.testkit +package akka.testkit.typed import com.typesafe.config.Config import scala.concurrent.duration.FiniteDuration diff --git a/akka-testkit-typed/src/main/scala/akka/typed/testkit/javadsl/TestProbe.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/javadsl/TestProbe.scala similarity index 79% rename from akka-testkit-typed/src/main/scala/akka/typed/testkit/javadsl/TestProbe.scala rename to akka-testkit-typed/src/main/scala/akka/testkit/typed/javadsl/TestProbe.scala index 5df96acf32..82539c258a 100644 --- a/akka-testkit-typed/src/main/scala/akka/typed/testkit/javadsl/TestProbe.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/javadsl/TestProbe.scala @@ -1,15 +1,15 @@ /** * Copyright (C) 2009-2017 Lightbend Inc. */ -package akka.typed.testkit.javadsl +package akka.testkit.typed.javadsl import akka.actor.typed.ActorSystem -import akka.typed.testkit.TestKitSettings +import akka.testkit.typed.TestKitSettings /** * Java API: */ -class TestProbe[M](name: String, system: ActorSystem[_], settings: TestKitSettings) extends akka.typed.testkit.scaladsl.TestProbe[M](name)(system, settings) { +class TestProbe[M](name: String, system: ActorSystem[_], settings: TestKitSettings) extends akka.testkit.typed.scaladsl.TestProbe[M](name)(system, settings) { def this(system: ActorSystem[_], settings: TestKitSettings) = this("testProbe", system, settings) diff --git a/akka-testkit-typed/src/main/scala/akka/typed/testkit/scaladsl/TestProbe.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/scaladsl/TestProbe.scala similarity index 99% rename from akka-testkit-typed/src/main/scala/akka/typed/testkit/scaladsl/TestProbe.scala rename to akka-testkit-typed/src/main/scala/akka/testkit/typed/scaladsl/TestProbe.scala index e7c068f7c5..9e25320b51 100644 --- a/akka-testkit-typed/src/main/scala/akka/typed/testkit/scaladsl/TestProbe.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/scaladsl/TestProbe.scala @@ -1,7 +1,7 @@ /** * Copyright (C) 2017 Lightbend Inc. */ -package akka.typed.testkit.scaladsl +package akka.testkit.typed.scaladsl import scala.concurrent.duration._ import java.util.concurrent.BlockingDeque @@ -18,7 +18,7 @@ import akka.util.PrettyDuration.PrettyPrintableDuration import scala.concurrent.Await import com.typesafe.config.Config -import akka.typed.testkit.TestKitSettings +import akka.testkit.typed.TestKitSettings import akka.util.BoxedType import scala.annotation.tailrec diff --git a/akka-testkit-typed/src/main/scala/akka/typed/testkit/scaladsl/package.scala b/akka-testkit-typed/src/main/scala/akka/testkit/typed/scaladsl/package.scala similarity index 93% rename from akka-testkit-typed/src/main/scala/akka/typed/testkit/scaladsl/package.scala rename to akka-testkit-typed/src/main/scala/akka/testkit/typed/scaladsl/package.scala index 652454e124..b9aa7bb895 100644 --- a/akka-testkit-typed/src/main/scala/akka/typed/testkit/scaladsl/package.scala +++ b/akka-testkit-typed/src/main/scala/akka/testkit/typed/scaladsl/package.scala @@ -1,7 +1,7 @@ /** * Copyright (C) 2009-2017 Lightbend Inc. */ -package akka.typed.testkit +package akka.testkit.typed import scala.concurrent.duration.{ Duration, FiniteDuration } import scala.reflect.ClassTag @@ -18,7 +18,7 @@ package object scaladsl { * * {{{ * import scala.concurrent.duration._ - * import akka.typed.testkit.scaladsl._ + * import akka.testkit.typed.scaladsl._ * 10.milliseconds.dilated * }}} * diff --git a/akka-testkit-typed/src/main/scala/akka/typed/testkit/Effects.scala b/akka-testkit-typed/src/main/scala/akka/typed/testkit/Effects.scala deleted file mode 100644 index b13aa51fa3..0000000000 --- a/akka-testkit-typed/src/main/scala/akka/typed/testkit/Effects.scala +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (C) 2014-2017 Lightbend Inc. - */ -package akka.typed.testkit - -import java.util.concurrent.ConcurrentLinkedQueue - -import akka.actor.typed.{ ActorContext, ActorRef, ActorSystem, Behavior, PostStop, Props, Signal } - -import scala.annotation.tailrec -import scala.collection.immutable -import scala.util.control.Exception.Catcher -import scala.util.control.NonFatal -import scala.concurrent.duration.{ Duration, FiniteDuration } - -/** - * All tracked effects must extend implement this type. It is deliberately - * not sealed in order to allow extensions. - */ -abstract class Effect - -object Effect { - - abstract class SpawnedEffect extends Effect - - @SerialVersionUID(1L) final case class Spawned(childName: String, props: Props) extends SpawnedEffect - @SerialVersionUID(1L) final case class SpawnedAnonymous(props: Props) extends SpawnedEffect - @SerialVersionUID(1L) final case object SpawnedAdapter extends SpawnedEffect - @SerialVersionUID(1L) final case class Stopped(childName: String) extends Effect - @SerialVersionUID(1L) final case class Watched[T](other: ActorRef[T]) extends Effect - @SerialVersionUID(1L) final case class Unwatched[T](other: ActorRef[T]) extends Effect - @SerialVersionUID(1L) final case class ReceiveTimeoutSet[T](d: Duration, msg: T) extends Effect - @SerialVersionUID(1L) final case class Messaged[U](other: ActorRef[U], msg: U) extends Effect - @SerialVersionUID(1L) final case class Scheduled[U](delay: FiniteDuration, target: ActorRef[U], msg: U) extends Effect - @SerialVersionUID(1L) case object EmptyEffect extends Effect -} - -/** - * An [[ActorContext]] for testing purposes that records the effects performed - * on it and otherwise stubs them out like a [[StubbedActorContext]]. - */ -class EffectfulActorContext[T](_name: String, _initialBehavior: Behavior[T], _mailboxCapacity: Int, _system: ActorSystem[Nothing]) - extends StubbedActorContext[T](_name, _mailboxCapacity, _system) { - import Effect._ - import akka.{ actor ⇒ a } - - private val effectQueue = new ConcurrentLinkedQueue[Effect] - def getEffect(): Effect = effectQueue.poll() match { - case null ⇒ throw new NoSuchElementException(s"polling on an empty effect queue: $name") - case x ⇒ x - } - def getAllEffects(): immutable.Seq[Effect] = { - @tailrec def rec(acc: List[Effect]): List[Effect] = effectQueue.poll() match { - case null ⇒ acc.reverse - case x ⇒ rec(x :: acc) - } - rec(Nil) - } - def hasEffects: Boolean = effectQueue.peek() != null - - private var current = Behavior.validateAsInitial(Behavior.undefer(_initialBehavior, this)) - - def currentBehavior: Behavior[T] = current - def isAlive: Boolean = Behavior.isAlive(current) - - private def handleException: Catcher[Unit] = { - case NonFatal(e) ⇒ - try Behavior.canonicalize(Behavior.interpretSignal(current, this, PostStop), current, this) // TODO why canonicalize here? - catch { case NonFatal(ex) ⇒ /* ignore, real is logging */ } - throw e - } - - def run(msg: T): Unit = { - try { - current = Behavior.canonicalize(Behavior.interpretMessage(current, this, msg), current, this) - } catch handleException - } - - def signal(signal: Signal): Unit = { - try { - current = Behavior.canonicalize(Behavior.interpretSignal(current, this, signal), current, this) - } catch handleException - } - - override def spawnAnonymous[U](behavior: Behavior[U], props: Props = Props.empty): ActorRef[U] = { - val ref = super.spawnAnonymous(behavior, props) - effectQueue.offer(SpawnedAnonymous(props)) - ref - } - - override def spawnAdapter[U](f: U ⇒ T): ActorRef[U] = { - spawnAdapter(f, "") - } - - override def spawnAdapter[U](f: U ⇒ T, name: String): ActorRef[U] = { - val ref = super.spawnAdapter(f, name) - effectQueue.offer(SpawnedAdapter) - ref - } - override def spawn[U](behavior: Behavior[U], name: String, props: Props = Props.empty): ActorRef[U] = { - effectQueue.offer(Spawned(name, props)) - super.spawn(behavior, name, props) - } - override def stop[U](child: ActorRef[U]): Boolean = { - effectQueue.offer(Stopped(child.path.name)) - super.stop(child) - } - override def watch[U](other: ActorRef[U]): Unit = { - effectQueue.offer(Watched(other)) - super.watch(other) - } - override def unwatch[U](other: ActorRef[U]): Unit = { - effectQueue.offer(Unwatched(other)) - super.unwatch(other) - } - override def setReceiveTimeout(d: FiniteDuration, msg: T): Unit = { - effectQueue.offer(ReceiveTimeoutSet(d, msg)) - super.setReceiveTimeout(d, msg) - } - override def cancelReceiveTimeout(): Unit = { - effectQueue.offer(ReceiveTimeoutSet(Duration.Undefined, null)) - super.cancelReceiveTimeout() - } - override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): a.Cancellable = { - effectQueue.offer(Scheduled(delay, target, msg)) - super.schedule(delay, target, msg) - } -} diff --git a/akka-testkit-typed/src/test/scala/akka/typed/testkit/EffectfulActorContextSpec.scala b/akka-testkit-typed/src/test/scala/akka/testkit/typed/BehaviorTestkitSpec.scala similarity index 54% rename from akka-testkit-typed/src/test/scala/akka/typed/testkit/EffectfulActorContextSpec.scala rename to akka-testkit-typed/src/test/scala/akka/testkit/typed/BehaviorTestkitSpec.scala index c8f4afe581..73659e43d8 100644 --- a/akka-testkit-typed/src/test/scala/akka/typed/testkit/EffectfulActorContextSpec.scala +++ b/akka-testkit-typed/src/test/scala/akka/testkit/typed/BehaviorTestkitSpec.scala @@ -2,16 +2,16 @@ * Copyright (C) 2014-2017 Lightbend Inc. */ -package akka.typed.testkit +package akka.testkit.typed import akka.actor.typed.scaladsl.Actor -import akka.typed.testkit.Effect.{ Spawned, SpawnedAdapter, SpawnedAnonymous } -import akka.typed.testkit.EffectfulActorContextSpec.Father -import akka.typed.testkit.EffectfulActorContextSpec.Father._ -import akka.actor.typed.{ ActorSystem, Behavior, Props } +import akka.testkit.typed.Effect.{ Spawned, SpawnedAdapter, SpawnedAnonymous } +import akka.testkit.typed.BehaviorTestkitSpec.{ Child, Father } +import akka.testkit.typed.BehaviorTestkitSpec.Father._ +import akka.actor.typed.{ Behavior, Props } import org.scalatest.{ FlatSpec, Matchers } -object EffectfulActorContextSpec { +object BehaviorTestkitSpec { object Father { case class Reproduce(times: Int) @@ -67,7 +67,7 @@ object EffectfulActorContextSpec { sealed trait Action - def initial: Behavior[Action] = Actor.immutable[Action] { (_, msg) ⇒ + val initial: Behavior[Action] = Actor.immutable[Action] { (_, msg) ⇒ msg match { case _ ⇒ Actor.empty @@ -78,61 +78,56 @@ object EffectfulActorContextSpec { } -class EffectfulActorContextSpec extends FlatSpec with Matchers { +//TODO WordSpec +class BehaviorTestkitSpec extends FlatSpec with Matchers { private val props = Props.empty - "EffectfulActorContext's spawn" should "create children when no props specified" in { - val system = ActorSystem.create(Father.init(), "father-system") - val ctx = new EffectfulActorContext[Father.Command]("father-test", Father.init(), 100, system) + "BehaviourTestkit's spawn" should "create children when no props specified" in { + val ctx = BehaviorTestkit[Father.Command](Father.init()) ctx.run(SpawnChildren(2)) - val effects = ctx.getAllEffects() - effects should contain only (Spawned("child0", Props.empty), Spawned("child1", Props.empty)) + val effects = ctx.retrieveAllEffects() + effects should contain only (Spawned(Child.initial, "child0"), Spawned(Child.initial, "child1", Props.empty)) } it should "create children when props specified and record effects" in { - val system = ActorSystem.create(Father.init(), "father-system") - val ctx = new EffectfulActorContext[Father.Command]("father-test", Father.init(), 100, system) + val ctx = BehaviorTestkit[Father.Command](Father.init()) ctx.run(SpawnChildrenWithProps(2, props)) - val effects = ctx.getAllEffects() - effects should contain only (Spawned("child0", props), Spawned("child1", props)) + val effects = ctx.retrieveAllEffects() + effects should contain only (Spawned(Child.initial, "child0", props), Spawned(Child.initial, "child1", props)) } - "EffectfulActorContext's spawnAnonymous" should "create children when no props specified and record effects" in { - val system = ActorSystem.create(Father.init(), "father-system") - val ctx = new EffectfulActorContext[Father.Command]("father-test", Father.init(), 100, system) + "BehaviourTestkit's spawnAnonymous" should "create children when no props specified and record effects" in { + val ctx = BehaviorTestkit[Father.Command](Father.init()) ctx.run(SpawnAnonymous(2)) - val effects = ctx.getAllEffects() - effects shouldBe Seq(SpawnedAnonymous(Props.empty), SpawnedAnonymous(Props.empty)) + val effects = ctx.retrieveAllEffects() + effects shouldBe Seq(SpawnedAnonymous(Child.initial, Props.empty), SpawnedAnonymous(Child.initial, Props.empty)) } it should "create children when props specified and record effects" in { - val system = ActorSystem.create(Father.init(), "father-system") - val ctx = new EffectfulActorContext[Father.Command]("father-test", Father.init(), 100, system) + val ctx = BehaviorTestkit[Father.Command](Father.init()) ctx.run(SpawnAnonymousWithProps(2, props)) - val effects = ctx.getAllEffects() - effects shouldBe Seq(SpawnedAnonymous(props), SpawnedAnonymous(props)) + val effects = ctx.retrieveAllEffects() + effects shouldBe Seq(SpawnedAnonymous(Child.initial, props), SpawnedAnonymous(Child.initial, props)) } - "EffectfulActorContext's spawnAdapter" should "create adapters without name and record effects" in { - val system = ActorSystem.create(Father.init(), "father-system") - val ctx = new EffectfulActorContext[Father.Command]("father-test", Father.init(), 100, system) + "BehaviourTestkit's spawnAdapter" should "create adapters without name and record effects" in { + val ctx = BehaviorTestkit[Father.Command](Father.init()) ctx.run(SpawnAdapter) - val effects = ctx.getAllEffects() + val effects = ctx.retrieveAllEffects() effects shouldBe Seq(SpawnedAdapter) } it should "create adapters with name and record effects" in { - val system = ActorSystem.create(Father.init(), "father-system") - val ctx = new EffectfulActorContext[Father.Command]("father-test", Father.init(), 100, system) + val ctx = BehaviorTestkit[Father.Command](Father.init()) ctx.run(SpawnAdapterWithName("adapter")) - val effects = ctx.getAllEffects() + val effects = ctx.retrieveAllEffects() effects shouldBe Seq(SpawnedAdapter) } }