lucataglia / akka-awp   1.0.0

GitHub

Akka-awp is a test library that through ActorWithProbe instances helps in writing integration tests for Akka actors applications

Scala versions: 2.12 2.13

Akka-awp

A lightweight testing library which goal is to make integration tests on complex actors application easier.

Installation

The artifacts are published to Maven Central.

libraryDependencies +=
  "net.rollercoders.akka" %% "akka-awp-testkit" % "1.0.0"

Table of contents

Abstract

Akka-awp helps in writing tests that check complex actor applications for correctness and regressions. The core idea was to write a library that could answer the following question: How can I easily test my system just by checking the final result (aka message) of the computation?

In that scenario, I would like to do this:

awp ! DoSomething thenWaitFor awp receiving Output

In other words:

Send to awp a DoSomething message, let all the actors execute their logic and then check that awp got an Output message

Here the tested awp is also the one that got the first message. This is just the simplest use case, not the only scenario covered. Let's dive into the library.

Examples

The aim of this section is to give a quick overview of how it looks like an akka-awp test. The first example has been written using the low-level API while the second one has been written using the high-level API. Both examples could be re-written using the other approach.

Check here the source code of all the examples.

Distributed reverse string

(low-level API)

This example will show you how akka-awp allow testing a complex system. Check here the source code of the DistributeRevereString actor.

Given a long string, the algorithm returns the reversed string:

  • A master actor spawns N slaves. The number of slaves is set at creation time.
  • The master receives the string to reverse. The parallelism to use is set through the Exec message along with the string to reverse (a small string can be split into 5 slices while a longer one can be split into 20 slices).
  • Using a Round Robin algorithm the master sends to each slave a slice of the whole string.

  • Each slave reverses the received string and then answer the master.
  • The master merge together all the slaves' responses sending to itself the final result through a Result message.

Given that behavior, testing with the low-level API can be done as follows:

"Distribute Reverse String using routing pool" must {
    "answer with the reversed string" in {

      val distributedSorter =
        ActorWithProbe
          .actorOf(ref =>
            Props(new DistributeRevereStringWithRoundRobinActor(Pool(10)) {
              override implicit val awpSelf: ActorRef = ref
            }))
          .withName("sorter-1")
          .build()
  
      distributedSorter ! Exec(longString, 20)
      distributedSorter eventuallyReceiveMsg Exec(longString, 20)
      distributedSorter eventuallyReceiveMsg Result(expected)
    }
}

Check here the source code of this example, could be useful to get all the side syntax you need e.g. the import statements.

Silly Actor

(high-level API)

This example will show you how the high-level API of akka-awp allow testing also the mailbox of the original sender. The high-level API are built on top of the low-level API. Check here the source code of the SillyActor.

The behavior of the SillyActor is the following:

  1. The testActor, from the ImplicitSender trait, send a String message to the SillyActor.
  2. The SillyActor send to itself the received String message wrapped into an Envelop case class.
  3. The SillyActor, once received the Envelop, answer the original sender (aka the testActor) with a pre-defined answer.
"An actor that send to itself a message" must {
    "be able to test if he received it" in {
     
      val sillyRef =
        ActorWithProbe
          .actorOf(
            ref =>
              Props(new SillyActor("Got the message") {
                override implicit val awpSelf: ActorRef = ref
              })
          )
          .withName("silly")
          .build()

      sillyRef ! "Hello akka-awp" thenWaitFor sillyRef receiving Envelop("Hello akka-awp") andThenWaitMeReceiving "Got the message"
    }
  }

Check here the source code of this example, could be useful to get all the side syntax you need e.g. the import statements.

Getting Started

Akka-awp exposes some factory methods that make available create an ActorWithProbe instance i.e. an Akka actor on which can be invoked every method of akka-testkit (like is it a TestProbe) other than new test methods implemented by the library (the low-level and high-level API we have seen in the above examples).

In order to have a concrete example to reference, here below will be used the SillyActor example.

Create an ActorWithProbe instance

Check here the source code of the testkit.

def actorOf(f: ActorRef => Props)(implicit system: ActorSystem): ActorWithProbeBuilder
def actorOf(props: Props)(implicit system: ActorSystem): ActorWithProbeBuilder
def actorOf(actorRef: ActorRef)(implicit system: ActorSystem): ActorWithProbeBuilder

These methods from the ActorWithProbe object are the entry point of the library and can be used to create an ActorWithProbe instance. When you create an awp instance you are wrapping a real-actor with an awp-actor. Doing that makes available for the awp-actor to save into its mailbox all the messages the real-actor received. Having these messages into the awp-actor mailbox is mandatory to run all the test methods the library offers.

The first actorOf method of the ones listed above is probably the one you will use more often since is the only one that makes available testing the responses the wrapped actor receive (e.g. sender() ! Response). Every user-defined actor we want to test for the "responses" must extend the AWP trait from net.rollercoders.akka.awp.trait.AWP:

trait AWP {
  this: Actor =>
  implicit val awpSelf: ActorRef
}

AWP trait force you to explicitly define an implicit val actorRef: ActorRef into your user-defined actor. Most of the times the concrete definition will be implicit val awpSelf: ActorRef = self, since there is no need to change the usual behavior of Akka into the non-test code. Done that, the implicit val awpSelf can be overwritten into test code as follows:

val sillyRef =
  ActorWithProbe
    .actorOf(
      ref =>
        Props(new SillyActor("Got the message") {
          override implicit val awpSelf: ActorRef = ref     // < - - - HERE
        })
    )
    .withName("silly")
    .build()

The anonymous class syntax allows us to overwrite the implicit ActorRef inherited by the AWP trait and so manage which is the implicit actorRef used by the ! method as implicit sender into the test code: the value used must be ref i.e. the awp reference exposed by the library. Doing that, every message send to the sillyActor (the real-actor) will be managed first by its awp (we are populating the awp-actor mailbox).

Testing an ActorWithProbe mailbox

Akka-awp makes available all the methods from the akka-testkit library but also it exposes some new methods to test actors (the low-level and high-level API we have seen in the above examples). Those methods are built on top of akka-testkit and allow to easily test if an actor will receive a message before a timeout expires. One of the goals of the high-level API is to expose a declarative programming approach.

// To test if the original sender receive some message
def thenWaitMeReceiving
def thenWaitMeReceivingType[T]

// To test if a specific awp receive some message
def thenWaitFor 
def thenWaitForAll
def receiving
def receivingType[T]

Let's use the SillyActor example to show how the syntax works. Go back to take a look at the diagram or check here the SillyActor source code to get clear about the actor behavior.

[1]

Query  | SillyActor got the Envelop:
       |
Syntax | sillyRef ! "Hello akka-awp" thenWaitFor sillyRef receiving Envelop("Hello akka-awp")
[2]

Query  | SillyActor got a message of type Envelop:
       |
Syntax | sillyRef.!("Hello akka-awp").thenWaitFor(sillyRef).receivingType[Envelop]
[3]

Query  | testActor got the answer
       |
Syntax | sillyRef ! "Hello akka-awp" thenWaitMeReceiving "Got the message"
[4]

Query  | testActor got a message of type String:
       |
Syntax | sillyRef.!("Hello akka-awp").andThenWaitMeReceivingType[String]
[5]

Query  | SillyActor got the Envelop and testActor got the answer:
       |
Syntax | sillyRef ! "Hello akka-awp" thenWaitFor sillyRef receiving Envelop("Hello akka-awp") andThenWaitMeReceiving "Got the message"
[6] 
 
Query  | SillyActor got a message of type Envelop and testActor got a message of type String:
       |
Syntax | sillyRef.!("Hello akka-awp").thenWaitFor(sillyRef).receivingType[Envelop].andThenWaitMeReceivingType[String]

SillyActor got the Envelop (akka-awp low-level API)

// [1]

sillyRef ! "Hello akka-awp"
sillyRef eventuallyReceiveMsg Envelop("Hello akka-awp")

SillyActor got the Envelop (akka-testkit)

// [1]

sillyRef ! "Hello akka-awp"
sillyRef expectMsg "Hello akka-awp"
sillyRef expectMsg Envelop("Hello akka-awp")

FAQ

How is possible invoke test method on real-actor ?

Akka-awp is able to invoke test methods on real-actors because it handles under the hood a hidden TestProbe that receives all the messages the actor gets.

It is a good practice to test auto-messages (e.g. self ! Msg) ?

No, it isn't. I wrote some examples that done that to show all the library features, but most of the time you don't want to test auto-messages. Moreover, to test auto-messages you need to change the real-actor source code using awpSelf instead of self (awpSelf ! Msg) otherwise the awp-actor won't get those messages into its mailbox.

Hint The auto-messages, like private methods in Object-oriented programming, should not be tested.

Corner cases

In this section will be listed all the founded corner case.

  • Even if you use awpSelf to intercept auto-messages, that won't make available to test messages that come from timers (e.g. timers.startTimerAtFixedRate(Key, Msg, 1 second). These messages are sent using the self ActorRef that comes from the Actor trait.

License

Akka-awp is Open Source and available under the Apache 2 License.

This code uses Akka which is licensed under the Apache 2 License, and can be obtained here