diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/TestKitUtils.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/TestKitUtils.scala index e8457c7198..1a56fa2468 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/TestKitUtils.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/internal/TestKitUtils.scala @@ -21,6 +21,7 @@ private[akka] object ActorTestKitGuardian { final case class SpawnActor[T](name: String, behavior: Behavior[T], replyTo: ActorRef[ActorRef[T]], props: Props) extends TestKitCommand final case class SpawnActorAnonymous[T](behavior: Behavior[T], replyTo: ActorRef[ActorRef[T]], props: Props) extends TestKitCommand final case class StopActor[T](ref: ActorRef[T], replyTo: ActorRef[Ack.type]) extends TestKitCommand + final case class ActorStopped[T](replyTo: ActorRef[Ack.type]) extends TestKitCommand final case object Ack @@ -32,7 +33,10 @@ private[akka] object ActorTestKitGuardian { reply ! context.spawnAnonymous(behavior, props) Behaviors.same case (context, StopActor(ref, reply)) ⇒ + context.watchWith(ref, ActorStopped(reply)) context.stop(ref) + Behaviors.same + case (_, ActorStopped(reply)) ⇒ reply ! Ack Behaviors.same } diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/javadsl/ActorTestKit.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/javadsl/ActorTestKit.scala index 99e3d0ebe8..b107a3af67 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/javadsl/ActorTestKit.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/javadsl/ActorTestKit.scala @@ -151,6 +151,14 @@ final class ActorTestKit private[akka] (delegate: akka.actor.testkit.typed.scala * for the spawned actor, note that spawning actors with the same name in multiple test cases will cause failures. */ def spawn[T](behavior: Behavior[T], name: String, props: Props): ActorRef[T] = delegate.spawn(behavior, name, props) + /** + * Stop the actor under test and wait until it terminates. + */ + def stop[T](ref: ActorRef[T]): Unit = delegate.stop(ref) + /** + * Stop the actor under test and wait `max` until it terminates. + */ + def stop[T](ref: ActorRef[T], max: Duration): Unit = delegate.stop(ref, max.asScala) /** * Shortcut for creating a new test probe for the testkit actor system diff --git a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/scaladsl/ActorTestKit.scala b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/scaladsl/ActorTestKit.scala index 667eb089fb..4f83efffd5 100644 --- a/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/scaladsl/ActorTestKit.scala +++ b/akka-actor-testkit-typed/src/main/scala/akka/actor/testkit/typed/scaladsl/ActorTestKit.scala @@ -4,6 +4,8 @@ package akka.actor.testkit.typed.scaladsl +import java.util.concurrent.TimeoutException + import akka.actor.typed.scaladsl.AskPattern._ import akka.actor.typed.{ ActorRef, ActorSystem, Behavior, Props } import akka.annotation.{ ApiMayChange, InternalApi } @@ -173,6 +175,16 @@ final class ActorTestKit private[akka] (val name: String, val config: Config, se def spawn[T](behavior: Behavior[T], name: String, props: Props): ActorRef[T] = Await.result(internalSystem ? (ActorTestKitGuardian.SpawnActor(name, behavior, _, props)), timeout.duration) + /** + * Stop the actor under test and wait until it terminates. + */ + def stop[T](ref: ActorRef[T], max: FiniteDuration = timeout.duration): Unit = try { + Await.result(internalSystem ? { x: ActorRef[ActorTestKitGuardian.Ack.type] ⇒ ActorTestKitGuardian.StopActor(ref, x) }, max) + } catch { + case _: TimeoutException ⇒ + assert(false, s"timeout ($max) during stop() waiting for actor [${ref.path}] to stop") + } + /** * Shortcut for creating a new test probe for the testkit actor system * @tparam M the type of messages the probe should accept diff --git a/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/AsyncTestingExampleTest.java b/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/AsyncTestingExampleTest.java index 3ee2de9712..0026d831eb 100644 --- a/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/AsyncTestingExampleTest.java +++ b/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/AsyncTestingExampleTest.java @@ -12,6 +12,9 @@ import akka.actor.testkit.typed.javadsl.TestProbe; import org.junit.AfterClass; import org.junit.Test; +import java.time.Duration; +import java.util.Objects; + import static org.junit.Assert.assertEquals; //#test-header @@ -35,6 +38,19 @@ public class AsyncTestingExampleTest { public Pong(String message) { this.message = message; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Pong)) return false; + Pong pong = (Pong) o; + return message.equals(pong.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } } Behavior echoActor = Behaviors.receive((context, ping) -> { @@ -45,7 +61,7 @@ public class AsyncTestingExampleTest { //#test-shutdown @AfterClass - public void cleanup() { + public static void cleanup() { testKit.shutdownTestKit(); } //#test-shutdown @@ -70,6 +86,23 @@ public class AsyncTestingExampleTest { probe.expectMessage(new Pong("hello")); } + @Test + public void testStoppingActors() { + TestProbe probe = testKit.createTestProbe(); + //#test-stop-actors + ActorRef pinger1 = testKit.spawn(echoActor, "pinger"); + pinger1.tell(new Ping("hello", probe.ref())); + probe.expectMessage(new Pong("hello")); + testKit.stop(pinger1); + + // Immediately creating an actor with the same name + ActorRef pinger2 = testKit.spawn(echoActor, "pinger"); + pinger2.tell(new Ping("hello", probe.ref())); + probe.expectMessage(new Pong("hello")); + testKit.stop(pinger2, Duration.ofSeconds(10)); + //#test-stop-actors + } + @Test public void systemNameShouldComeFromTestClass() { assertEquals(testKit.system().name(), "AsyncTestingExampleTest"); diff --git a/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/AsyncTestingExampleSpec.scala b/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/AsyncTestingExampleSpec.scala index 02b2ccbc22..94031c2012 100644 --- a/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/AsyncTestingExampleSpec.scala +++ b/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/AsyncTestingExampleSpec.scala @@ -5,10 +5,11 @@ package docs.akka.actor.testkit.typed.scaladsl import akka.actor.testkit.typed.scaladsl.ActorTestKit -import akka.actor.typed._ +import akka.actor.typed.{ PostStop, _ } import akka.actor.typed.scaladsl.Behaviors import org.scalatest.BeforeAndAfterAll import org.scalatest.WordSpec +import scala.concurrent.duration._ object AsyncTestingExampleSpec { //#under-test @@ -50,6 +51,23 @@ class AsyncTestingExampleSpec extends WordSpec with BeforeAndAfterAll { pinger ! Ping("hello", probe.ref) probe.expectMessage(Pong("hello")) } + + "be able to stop actors under test" in { + // Will fail with 'name not unique' exception if the first actor is not fully stopped + val probe = testKit.createTestProbe[Pong]() + //#test-stop-actors + val pinger1 = testKit.spawn(echoActor, "pinger") + pinger1 ! Ping("hello", probe.ref) + probe.expectMessage(Pong("hello")) + testKit.stop(pinger1) // Uses default timeout + + // Immediately creating an actor with the same name + val pinger2 = testKit.spawn(echoActor, "pinger") + pinger2 ! Ping("hello", probe.ref) + probe.expectMessage(Pong("hello")) + testKit.stop(pinger2, 10.seconds) // Custom timeout + //#test-stop-actors + } } //#test-shutdown diff --git a/akka-docs/src/main/paradox/typed/testing.md b/akka-docs/src/main/paradox/typed/testing.md index 16db994ec4..d1c78b9949 100644 --- a/akka-docs/src/main/paradox/typed/testing.md +++ b/akka-docs/src/main/paradox/typed/testing.md @@ -202,6 +202,16 @@ Java Note that you can add `import testKit._` to get access to the `spawn` and `createTestProbe` methods at the top level without prefixing them with `testKit`. +#### Stopping actors +The method will wait until the actor stops or throw an assertion error in case of a timeout. + +Scala +: @@snip [AsyncTestingExampleSpec.scala](/akka-actor-testkit-typed/src/test/scala/docs/akka/actor/testkit/typed/scaladsl/AsyncTestingExampleSpec.scala) { #test-stop-actors } + +Java +: @@snip [AsyncTestingExampleTest.java](/akka-actor-testkit-typed/src/test/java/jdocs/akka/actor/testkit/typed/javadsl/AsyncTestingExampleTest.java) { #test-stop-actors } + + ### Test framework integration @@@ div { .group-java }