Not sure how to test behaviors in akka-typed

I’m struggling to find out how to test behaviors in akka-typed. Looking at the following example, from the documentation

  def bot(greetingCounter: Int, max: Int): Behavior[HelloWorld.Greeted] =
    Behaviors.receive { (context, message) =>
      val n = greetingCounter + 1
      println(s"Greeting ${n} for ${message.whom}")
      if (n == max) {
        Behaviors.stopped
      } else {
        message.from ! HelloWorld.Greet(message.whom, context.self)
        bot(n, max)
      }
    }
}

I want to write a test where I pass in an initial value for bot e.g. bot(1,2), send it a message and then check whether the next behavior from an execution of bot(1,2) is bot(2,2). I can’t seem to find any examples on how todo this.

Ok. I’ve found the snapshot documentation https://doc.akka.io/docs/akka/snapshot/typed/testing.html According to this synchronoous testing has limitations, which I agree with, unless I’ve misread something.

Looks like you can’t check the next behavior the actor will call
bot(n,max)
due to nothing being mentioned in the documentation (I’ll be happy if somebody corrects me, if I am wrong)

I can use synchronous testing for checking for effects (actor spawning, behavior stopping etc), messages sent to child actors and use asynchronous testing to ensure input messages generate the expected output messages given a particular state the actor is in.

Just worked out that if you add a debug statement

    Behaviors.receive { (context, message) =>
      context.log.debug("bot; {}, {}", greetingCounter, max)

Then in your tests you can do

testKit.logEntries() shouldBe Seq(CapturedLogEvent(Logging.DebugLevel, "bot; 1, 2"))

testKit.logEntries() shouldBe Seq(CapturedLogEvent(Logging.DebugLevel, "bot; 2, 2"))

to check the parameters passed into the behavior in each new iteration.

Hey Abdul,

I, personally, wouldn’t use a debug message for such purposes. Instead, you could include a “get” message in your actor’s protocol. That way you can inquire about its current state whenever that is needed. Consider the following example:

sealed trait Command
final case class Increase(value: Int) extends Command
final case class Decrease(value: Int) extends Command
final case class GetCurrent(replyTo: ActorRef[Int]) extends Command

object Counter {

  def init(value: Int = 0): Behavior[Command] = Behaviors.receiveMessage {
    case Increase(increment) => init(value + increment)
    case Decrease(decrement) => init(value - decrement)
    case GetCurrent(replyTo) =>
      replyTo ! value
      Behaviors.same
  }

}

class CounterSpec extends FlatSpec with BeforeAndAfterAll {
  private val testKit = ActorTestKit()
  override def afterAll(): Unit = testKit.shutdownTestKit()

  "Counter" should "be counting correctly" in {
    val probe = testKit.createTestProbe[Int]()
    val counter = testKit.spawn(Counter.init(100))
    counter ! Increase(100)
    counter ! Decrease(199)
    counter ! GetCurrent(probe.ref)

    probe.expectMessage(1)
  }

}

Alternatively, using your example, you can modify the Greet message to include the current count in the reply to actor sending the Greeted message.

Hope this helps.

1 Like

Hi, echoing what @chmodas wrote you have a few options.

You can create a get status command to return some state of interest that is safe to do, e.g.

  1. case object GetState extends Command
  2. handle it: https://github.com/akka/akka/blob/master/akka-actor-typed-tests/src/test/scala/akka/actor/typed/SupervisionSpec.scala#L59-L62
  3. test it: https://github.com/akka/akka/blob/master/akka-actor-typed-tests/src/test/scala/akka/actor/typed/SupervisionSpec.scala#L441-L446

I hope that helps,
Helena

1 Like

Thanks Borislav, I didn’t think about using a message to enquire about the internal state. That is probably better than creating noise in the form of extra logging for code testing.

1 Like

Thank you helena, for the clarification.

1 Like

I am also struggling to find a method I like. I’m not a big fan of creating a new message just for testing. How do you make sure it’s not used in production?

For simple cases, I’ve found injecting a function the actor uses to change state can allow me to verify a behavior produces the desired next behavior AND that it produces it with the desired next state.

I’m using ScalaMock. https://scalamock.org/

import akka.actor.testkit.typed.scaladsl.BehaviorTestKit
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.must.Matchers

sealed trait Command
final case class Increase(value: Int) extends Command
final case class Decrease(value: Int) extends Command

object Counter {
  type NextBehavior = (State => Behavior[Command], State) => Behavior[Command]

  case class State(count: Int,
                   next: NextBehavior = (next, state) => next(state))

  val singleBehavior: State => Behavior[Command] = { state =>
    Behaviors.receiveMessage {
      case Increase(increment) =>
        state.next(singleBehavior, state.copy(count = state.count + increment))
      case Decrease(decrement) =>
        state.next(singleBehavior, state.copy(count = state.count - decrement))
    }
  }

  def apply(next: NextBehavior = (next, state) => next(state)): Behavior[Command] = {
    next(singleBehavior, State(0, next))
  }
}

class CounterSpec extends AnyFlatSpec with Matchers with MockFactory {
  import Counter._

  sealed trait NextFixture {
    val mockNext = mockFunction[State => Behavior[Command], State, Behavior[Command]]
  }

  "Counter.apply" must "initialize to zero with the provided next function" in new NextFixture {
    mockNext.expects(singleBehavior, State(0, mockNext)).returning(Behaviors.empty)
    BehaviorTestKit(Counter(mockNext)).returnedBehavior mustBe Behaviors.empty
  }

  "singleBehavior" must "increase the count by value when Increase(value)" in new NextFixture {
    mockNext.expects(singleBehavior, State(5, mockNext)).returning(Behaviors.empty)
    val sut = BehaviorTestKit(singleBehavior(State(0, mockNext)))
    sut.run(Increase(5))
    sut.returnedBehavior mustBe Behaviors.empty
  }

  it must "decrease the count by value when Decrease(value)" in new NextFixture {
    mockNext.expects(singleBehavior, State(5, mockNext)).returning(Behaviors.empty)
    val sut = BehaviorTestKit(singleBehavior(State(10, mockNext)))
    sut.run(Decrease(5))
    sut.returnedBehavior mustBe Behaviors.empty
  }
}