Synchronous testing

The BehaviorTestKit provides the possibility to verify the reaction of an actor behavior to specific messages. The reaction can be in the form of an Effect (different ways of spawning and stopping children actors), sending and receiving messages, and changes in behavior. Let's illustrate this testing process with an example:

"The boy should" should {
"forward given ShoppingList to the seller" in {
val testKit = BehaviorTestKit(Boy.goShopping)
val seller = TestInbox[Shop.SellByList]()
val manager = TestInbox[Manager.Command]()
val list = ShoppingList(1, 1, 1, 1)
testKit.run(GoShopping(list, seller.ref, manager.ref))
seller.expectMessage(SellByList(list, manager.ref))
assert(!testKit.isAlive)
testKit.expectEffect(NoEffects)
}
}

Here, we have wrapped a goShopping behavior into the BehaviorTestKit so that we can test it synchronously. The two TestInbox references represent actors that the Boy is supposed to communicate with. They are basically ActorRefs, but they allow us to express expectations regarding incoming messages. To trigger the test, we can create a message and run the testKit using this message as an input.

In the next line, we expect the seller actor to receive the same message, with the manager reference being propagated as a sender. This is how our boy's logic is supposed to work. Then, we verify that the Boy stopped itself by checking that it is not alive. Finally, we don't expect any effects on children as the Boy actor is not supposed to have or create any children.

In the same way that we tested that the Boy has no effects on children, we can test that the Chef has such effects:

"The chef should" should {
"create and destroy mixers as required" in {
val mixerFactory = Mixer.mix(0 seconds)
val chef = BehaviorTestKit(Chef.idle(mixerFactory))
val manager = TestInbox[Manager.Command]()
val message = Mix(Groceries(1, 1, 1, 1), manager.ref)
val dispatcher = DispatcherSelector.fromConfig("mixers-dispatcher")
chef.run(message)
chef.expectEffect(Spawned(mixerFactory, "Mixer_1", dispatcher))
val expectedByMixer = Mixer.Mix(Groceries(1, 1, 1, 1), chef.ref)
chef.childInbox("Mixer_1").expectMessage(expectedByMixer)
}
}

In this test, we create a behavior under test in the same way we just did with the Boy actor. We create a message and run it with the testing behavior wrapper. As a result, we expect a chef to have the effect of spawning a single Mixer actor with an appropriate name and dispatcher. Finally, we're looking up the mailbox of the spawned child actor by using the childInbox method and expect it to have a message that's been sent by the chef to be present in it.

Unfortunately, at the time of writing this book, the Akka TestKist still has some rough edges that require us, in this specific case, to refactor our Chef behavior to accept the mixer factory as a parameter. The reason for this is that behaviors are compared by reference, which requires us to have the same instance of the behavior for the test to pass.

Another limitation of the BehaviorTestKit is its lack of support for extensions like cluster, cluster singleton, distributed data, and receptionist. This makes it impossible to test the Seller actor in a synchronous setup because this actor registers itself with the receptionist:

context.system.receptionist ! Register(SellerKey, context.self)

We could use the synchronous approach or we could refactor the seller to take a constructor function for the receptionist and provide a mock receptionist in the test. This is an example of how this can be done in the code of the Seller:

type ReceptionistFactory = ActorContext[SellByList] => ActorRef[Receptionist.Command]
val systemReceptionist: ReceptionistFactory = _.system.receptionist
def seller(receptionist: ReceptionistFactory) = setup { ctx ⇒
receptionist(ctx) ! Register(SellerKey, ctx.self)
...

The factory is just a function from ActorContext to the ActorRef with the appropriate types.

With this change, we can implement our test, as follows:

"A seller in the shop" should {
"return groceries if given a shopping list" in {
val receptionist = TestInbox[Receptionist.Command]()
val mockReceptionist: Shop.ReceptionistFactory = _ => receptionist.ref
val seller = BehaviorTestKit(Shop.seller(mockReceptionist))
val inbox = TestInbox[Manager.Command]()
val message = ShoppingList(1,1,1,1)
seller.run(SellByList(message, inbox.ref))
inbox.expectMessage(ReceiveGroceries(Groceries(1, 1, 1, 1)))
receptionist.expectMessage(Register(Shop.SellerKey, seller.ref))
seller.expectEffect(NoEffects)
}
}

We provide a mock receptionist which is just a TestInbox[Receptionist.Command] and use it as the result of the factory, ignoring the actual actor context. Then, we execute the test as we did previously and expect the messages to be sent to the manager and receptionist appropriately. 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.44.143