Testing actors

Actor-based systems are different from systems that are built using a traditional approach. Naturally, testing actors is different from regular testing. Actors send and receive messages in an asynchronous manner and are usually examined via message flow analysis. A typical setup will include three parts:

  1. The source of the messages
  2. The actor under test
  3. The receiver of the actor's responses

Luckily, Akka includes a testing module that abstracts a lot of setup logic and provides useful helpers for common testing activities. The name of the module is Akka TestKit and it is contained in a separate module that needs to be added to the project's test scope:

libraryDependencies += "com.typesafe.akka" %% "akka-testkit" % akkaVersion % Test

Having this dependency allows us to extend a TestKit class. The TestKit implements a special testing environment that mimics the internals of a normal actor system but provides access to some of the details that are hidden in a production implementation.

Here is an example of the ScalaTest specification that extends TestKit:

class BakerySpec(_system: ActorSystem) extends TestKit(_system)
with Matchers with WordSpecLike with BeforeAndAfterAll
with ImplicitSender {

def this() = this(ActorSystem("BakerySpec"))

override def afterAll: Unit = shutdown(system)

Here, we extend a TestKit with usual the ScalaTest matchers and WordSpec, but also mix a BeforeAndAfterAll and an ImplicitSender in. Then, we implement the default constructor by instantiating a BakerySpec actor system. Lastly, we override an afterAll method to make sure that our test actor system is properly terminated after the test.

In SBT, tests are usually run in parallel. In this case, it is important to name an actor system properly and in this case, remoting is also used to override the default port to avoid conflicts between simultaneously executing tests. Also, we should not forget to shut down the actor system gracefully to ensure that our resources are cleaned up properly.

The TestKit implements and brings into scope a testActor field, which we can use to send messages from the test code. Usually, we'd like these messages to be sent from a well-known actor. The ImplicitSender trait implements a reference to the testActor that is attached to the message at the moment it is sent.

The TestKit also maintains an internal queue of the messages sent to the testActor and defines a host of useful methods to inspect this queue.

This is how some of these predefined methods can be used to test our Boy actor:

"The boy should" should {
val boyProps = Boy.props(system.actorSelection(testActor.path))
val boy = system.actorOf(boyProps)

"forward given ShoppingList to the seller" in {
val list = ShoppingList(0, 0, 0, 0)
boy ! list
within(3 millis, 20 millis) {
expectMsg(list)
lastSender shouldBe testActor
}
}
"ignore other message types" in {
boy ! 'GoHome
expectNoMessage(500 millis)
}
}

Recapping the logic of the Boy actor, all it is doing is forwarding ShoppingList to another actor provided as a constructor parameter. In order to test this behaviour, we first create an ActorSelection as required by the boy's constructor, use our default testActor as a target, and create a boy actor as a child of the test actor system that the TestKit provides us with.

In the first test, we send a ShoppingList to the Boy and expect it to forward the list to the testActor within a predefined time interval between 3 and 30 milliseconds. We verify that the message is indeed a ShoppingList and that the sender is a testActor.

In the second test, we verify that the Boy ignores other messages. To check this, we send it a message with a Symbol type and expect our testActor to receive nothing within 500 milliseconds. As normal forwarding is expected to take no more than 20 milliseconds by our first test, we can be sure that the message has been ignored by the Boy.

testActorlastSenderwithin, expectMsg, and expectNoMsg are implemented by the TestKit and save us from writing boilerplate code.

There are lots of other helpful methods in the TestKit that we will take a look at shortly. Most of them exist in two forms: one takes a timeout as a parameter and another uses a default timeout. The timeout defines how long TestKit will wait for the condition to happen. The default timeout can be overridden by using the within wrapper, as shown previously, by changing the configuration or by using a timescale parameter that will affect all durations within the scope.

We are already familiar with the expectMsg and expectNoMessage assertions. Let's take a look at some of the other available helpers:

  • def expectMsgClass[C](c: Class[C]): C expects and returns a single message of type C.
  • def expectMsgType[T](implicit t: ClassTag[T]): T does the same as the previous helper, but uses implicit to construct the type parameter.
  • def expectMsgAnyOf[T](obj: T*): T This expects one message and verifies that it is equal to one of the constructor parameters.
  • def expectMsgAnyClassOf[C](obj: Class[_ <: C]*): C does the same as before, but verifies the type of the message instead of the actual message.
  • def expectMsgAllOf[T](obj: T*): Seq[T] expects the number of messages and verifies that all of them are equal to the constructor parameters.
  • def expectMsgAllClassOf[T](obj: Class[_ <: T]*): Seq[T] does the same as before, but verifies types of messages.
  • def expectMsgAllConformingOf[T](obj: Class[_ <: T]*): Seq[T] does the same as expectMsgAllClassOf, but checks conformity (instanceOf) instead of class equality.
  • def expectNoMessage(): Unit verifies that no message is received during the specified or default timeout.
  • def receiveN(n: Int): Seq[AnyRef] receives N messages and returns them to the caller for further verification.
  • def expectMsgPF[T](max: Duration = Duration.Undefined, hint: String = "")(f: PartialFunction[Any, T])T expects a single message and verifies that a given partial function is defined.
  • def expectTerminated(target: ActorRef, max: Duration = Duration.Undefined): Terminated expects a single Terminated message from a specified target.
  • def fishForMessage(max: Duration = Duration.Undefined, hint: String = "")(f: PartialFunction[Any, Boolean]): Any expects multiple messages for which given partial function is defined. It returns the first message for which f returns true.

Our Baker actor is designed in such a way that it sends messages to its parent, which means that we will be unable to receive responses from the Baker if we create it using the test actor system. Let's take a look at how TestKit can help us in this situation:

"The baker should" should {
val parent = TestProbe()
val baker = parent.childActorOf(Props(classOf[Baker], 0 millis))
"bake cookies in batches" in {
val count = Random.nextInt(100)
baker ! RawCookies(Oven.size * count)
parent.expectMsgAllOf(List.fill(count)(Cookies(Oven.size)):_*)
}
}

Here, we're constructing a test actor using TestProbe(). The TestProbe is another nice feature provided by the TestKit that allows you to send, receive, and reply to messages, and is useful in testing scenarios when multiple test actors are required. In our case, we're using its ability to create child actors to create a Baker actor as a child.

Then, we need to generate a number of RawCookies so that it requires a number of turns to bake them. We expect this number of messages to be sent to the parent in the next line.

Up until now, we have tested actors in isolation. Our grocery Store is built in a way that it instantiates an anonymous actor. This makes the approach of testing an actor in isolation impossible. Let's demonstrate how we can verify that the Seller actor returns the expected Groceries if given a ShoppingList:

class StoreSpec(store: Store) extends TestKit(store.store)
with Matchers with WordSpecLike with BeforeAndAfterAll {

def this() = this(new Store {})

override def afterAll: Unit = shutdown(system)

"A seller in store" should {
"do nothing for all unexpected message types" in {
store.seller ! 'UnexpectedMessage
expectNoMessage()
}
"return groceries if given a shopping list" in {
store.seller.tell(ShoppingList(1, 1, 1, 1), testActor)
expectMsg(Groceries(1,1,1,1))
}
}
}

We will construct our test class like we did previously, but with one subtle difference. The Seller actor is defined anonymously and therefore is only constructed as a part of the whole actor system. Because of this, we instantiate the Store in the default constructor and use the underlying actor system that's accessible via the store field as a constructor parameter for the TestKit instance.

In the test itself, we're sending test inputs directly to the seller ActorRef using the store that we constructed previously. We have not extended ImplicitSender and need to provide a testActor as a sender reference explicitly.

Now that we have our application implemented and tested, let's run it!

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

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