0

I'm trying to unit test my actor's handling of a "Terminated" message for a child actor. The code under test is something like this:

    case Terminated(terminatedActor) =>
      val actorName = terminatedActor.path.name
      if (actorName.startsWith("ChildActor")) {
        doSomething()
      }
      Behaviors.same

In my unit test, I'm trying to do something like this:

    val testInbox = TestInbox[ParentActor.Request](name = "ChildActor1")
    val testKit = BehaviorTestKit(ParentActor())
    testKit.run(Terminated(testInbox.ref))
    assert( *** that doSomething() happened *** )

The unit test code doesn't compile. I get this error in the call to testKit.run():

type mismatch; found : akka.actor.typed.Terminated required: ParentActor.Request

I assume that this is because the Terminated message does not inherit from my ParentActor.Request trait.

Based on a below comment, I changed the unit test to:

    val testInbox = TestInbox[ParentActor.Request](name = "ChildActor1")
    val testKit = BehaviorTestKit(ParentActor())
    testKit.signal(Terminated(testInbox.ref))
    assert( *** that doSomething() happened *** )

This now compiles, but the call to testKit.signal() now throws a DeathPactException, which the docs say means that the actor is not handling the Terminated message, even though my production code definitely does handle it.

Any idea what is wrong?

JoeMjr2
  • 3,804
  • 4
  • 34
  • 62
  • use `testKit.signal(Terminated(ref))` instead of .run – AminMal Apr 20 '22 at 18:51
  • @AminMal Thanks. This compiles. However, it now throws a DeathPactException, which the docs say means that the actor is not handling the Terminated message, even though my production code definitely does handle it. Any idea why? – JoeMjr2 Apr 20 '22 at 19:26
  • I just read the docs again and found out that `BehaviorTestKit.signal(...)` sends the signal to the **BehaviorTestKit** and not the actor living inside the test kit. So that's where we're going wrong. Let me do a quick look. – AminMal Apr 20 '22 at 20:09

1 Answers1

2

Are you sure that your production code definitely handles the Terminated signal?

Signals are not, from the perspective of a typed Behavior, messages. They are processed by the signal handler installed by receiveSignal. That signal handler takes not just the signal but the ActorContext as well, wrapped up in a tuple. If the response to a Terminated signal doesn't require the context, you still have to match against it:

// inside a .receiveSignal...
case (_, Terminated(terminatedActor)) =>
  val actorName = terminatedActor.path.name
  if (actorName.startsWith("ChildActor")) {
    doSomething()
  }
  Behaviors.same

Note that Akka's test suite includes this test which exercises handling the Terminated signal when sent via testKit.signal:

      val other = TestInbox[String]()
      val testkit = BehaviorTestKit[Parent.Command](Parent.init)
      noException should be thrownBy {
        testkit.signal(Terminated(other.ref))
      }
Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30
  • 1
    Ah, that was it! I didn't realize that Terminated had to be received via receiveSignal instead of receiveMessage. Thanks for the reference to testkit.signal() also. Worked beautifully. – JoeMjr2 Apr 25 '22 at 19:56