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.
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.
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.
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.
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.
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
}
}
In my toy projects I’ve used messages with package private scope for test-only messages. So tests in the same package can use the test messages, but trying to use them from outside the package should give compiler errors:
sealed trait PrivateCommand
private[mypackage] case object GetState extends PrivateCommand
That’s the basic idea, but you probably want to have public messages too in time, so I have done something like:
trait BaseMsg
// private test messages
sealed trait PrivateCommand extends BaseMsg
private[mypackage] case object GetState extends PrivateCommand
...
// public protocol messages
sealed trait PublicCommand extends BaseMsg
case object PublicMessage extends PublicCommand