Setak: A Framework for Stepwise Deterministic Testing of Akka Actors

How to Use by Example

We show the usage of Setak by a well-known example, BoundedBuffer. The code for BoundedBuffer actor is shown in the following:

class BoundedBuffer(size: Int) extends Actor { var content = new Array[Int](size) var head, tail, curSize = 0 def receive = { case Put(x) ⇒ if (curSize < size) { content(tail) = x tail = (tail + 1) % size curSize += 1 } case Get ⇒ if (curSize > 0) { val r = content(head) head = (head + 1) % size curSize -= 1 self.reply(r) } else self.reply(-2) } }

This actor accepts two messages, Put(x) and Get. Upon processing Put(x), if the current size of the buffer is less than the maximum size, it adds x to its content. We assume that x is a positive integer. Upon processing Get message, if the current size is greater than zero, it removes a value from its content and returns that value. Otherwise, it returns -2.

Unit Testing

We want to test the behavior of a bounded buffer actor with capacity of one elment upon receiving two messages Put and Get. We consider two senarios:

The test case for these two scenarios is shown in the following. For writing test cases in JUnit format, the test class should extend SetakJunit and for writing test cases in FlatSpec or WordSpec formats use SetakFlatSpec or SetakWordSpec traits respectively.

In order to make the test control the actors in the program under test, you should use the factory provided by the Setak for creating actors, i.e. akka.setak.TestActorRefFactory.actorOf. This factory returns an instance of TestActorRef as a reference to that actor. Each Setak test has a testActroRefFactory as a field. In order to create the acors you should call actorOf method of that factory. Calling actrOf inside of the test class will automatically use that factory for creating actors.

The JUnit test suite for these scenarios are shown in the following. Note that for writing JUnit test suite the test class should extend SetakJUnit. In order to check if two Put messages have been processed or not, we need to define the Put message sent to buffer as a TestMessageEnvelop. Each TestMessageEnvelop consists of three parameters, sender, receiver, and message. The message parameter can be an object (using testMessageEnvelop factory) or or a pattern in the form of partial function (using testMessagePatternEnvelop factory). The wildcards for sender and receiver is anyActorRef and for message is anyMessage. In this test, since the sender is the test class and it is not an actor, we set the sender parameter to anyActorRef. Furthermore, becuase the test message envelop put will refer to any Put(x) message regardless of the value of x, it is created using testMessagePatternEnvelop factory.

You should use org.junit.Test annotation for specifying test methods, and org.junit.Before and org.junit.After annotations for specifying the methods that should be called before and after each test.

class JUnitTestBoundedBuffer extends SetakJUnit { var buf: TestActorRef = _ var put: TestMessageEnvelop = _ @Before def setUp() { buf = actorOf(new BoundedBuffer(1)).start put = testMessagePatternEnvelop(anyActorRef, buf, { case Put(_) ⇒ }) } @Test def testPutGet() { buf ! Put(12) whenStable { assert(buf.actorObject[BoundedBuffer].curSize == 1, "The current size is not one") } val getValue = (buf ? Get).mapTo[Int].get assert(getValue == 12, "The returned value is not 12") } @Test def testTwoPuts() { buf ! Put(1) buf ! Put(2) whenStable { assert(buf.actorObject[BoundedBuffer].curSize == 1, "The current size is not one") assert(processingCount(put) == 2, "Put has not been processed twice") } } }

The API processingCount accepts a test message envelop and returns the number of times that message is processed.

To write the above test in FlatSpec form, the test class should extend SetakFlatSpec. The above test in FlatSpec form is shown in the following:

class SpecTestBoundedBuffer extends SetakFlatSpec { var buf: TestActorRef = _ var put: TestMessageEnvelop = _ override def setUp() { buf = actorOf(new BoundedBuffer(1)).start put = testMessagePatternEnvelop(anyActorRef, buf, { case Put(_) ⇒ }) } "the current size of buffer" should "be one and returns value 12" in { buf ! Put(12) whenStable { assert(buf.actorObject[BoundedBuffer].curSize == 1, " The current size is not one") } val getValue = (buf ? Get).mapTo[Int].get assert(getValue == 12, "The returned value is not 12") } "Both Put messages" should "be processed and only the first one should update the content" in { buf ! Put(1) buf ! Put(2) whenStable { assert(buf.actorObject[BoundedBuffer].curSize == 1, "The current size is not one") assert(processingCount(put) == 2, "Put has not been processed twice") } } }

For specifying the methods that should be called before and after each test, you should override setUp and tearDown methods respectively.

Integration Testing

Consider an actor system with three actors: BoundedBuffer, Producer, and Consumer. The code for Producer and consumer are shown in the follwoing:

class Consumer(buf: ActorRef) extends Actor { var token: Int = -1 def receive = { case Consume(count) ⇒ { for (i ← 1 to count) { token = (buf ? Get).get.asInstanceOf[Int] } } case GetToken ⇒ self.reply(token) } } class Producer(buf: ActorRef) extends Actor { def receive = { case Produce(values) ⇒ { values.foreach(v ⇒ buf ! Put(v)) } } }

The consumer accepts a Consume(count) message with a counter argumet and sends Get messages to the buffer with the specified counter. Also consumer accepts a GetToken message and replies with the last received value from the buffer. The default value for the token field is -1. The producer accepts a Produce(list) message with a list of numbers as argument and sends Put message to the buffer for each value in the list.

We want to test the following scenario which consists of two steps:

This test in the form of JUnit is shown in the following. In order to specify the order of messages in the test execution, the messages sent to buffer should be defined as TestMessageEnvelops. Note that becuase consumer sends the Get message via a future, the sender of get message envelop is set to anyActorRef. The API setSchedule(po1,po2,...,pon) accepts a set of partial orders, po1, po2, ..., pon, as argumets. Each partial order is specified by using -> between test message envelops, e.g. setSchedule(tme1->tme2, tme3->tme4->tme5). The rule for the partial orders is that the receiver of the test message envelops in each partial order should be the same (since actors do not share their local state, the order of messages sent to the same actor can only affect the results)

class TestBoundedBufferWithProducerConsumer extends SetakJUnit { @Test def testBufferWithProducerConsumer() { val buf = actorOf(new BoundedBuffer(1)).start val consumer = actorOf(new Consumer(buf)).start val producer = actorOf(new Producer(buf)).start val put1 = testMessageEnvelop(producer, buf, Put(1)) val get = testMessageEnvelop(anyActorRef, buf, Get) //step 1 consumer ! Consume(1) whenStable { assert((consumer ? GetToken).mapTo[Int].get == -2) } //step 2 setSchedule(put1 -> get) producer ! Produce(List(1)) consumer ! Consume(1) whenStable { assert((consumer ? GetToken).mapTo[Int].get == 1) } } }

New Feature: Support for Non-Stable Systems

There might be the case where the system cannot get stable, i.e. there are always some messages to be processed. We have added a feature to Setak to support those cases. Two APIs, afterMessage(message) { body } and afterAllMessages { body } can be used for this puprose. afterMessage(message) accepts a test message envelop as input and executes the body after message is processed. afterAllMessages executes the body after all the test message envelops are processed in the system. Using this feature is not limited to testing non-stable systems; if all the messages are known and defined as test message envelops, afterAllMessages instead of whenStable is an alternate and more reliable way of checking assertions in the system. The follwoing test case shows the usage of these two APIs:

class TestBoundedBufferWithProducerConsumer extends SetakJUnit { @Test def testBufferWithProducerConsumer() { val buf = actorOf(new BoundedBuffer(1)).start val consumer = actorOf(new Consumer(buf)).start val producer = actorOf(new Producer(buf)).start val put1 = testMessageEnvelop(producer, buf, Put(1)) val get1 = testMessageEnvelop(anyActorRef, buf, Get) val get2 = testMessageEnvelop(anyActorRef, buf, Get) //step 1 consumer ! Consume(1) afterMessage(get1) { assert((consumer ? GetToken).mapTo[Int].get == -2) } //step 2 setSchedule(put1 -> get2) producer ! Produce(List(1)) consumer ! Consume(1) afterAllMessages { assert((consumer ? GetToken).mapTo[Int].get == 1) } } }

In the above test in step 1, it waits for get1 to be processed and then executes the body (check the assertion). In step 2, it waits for all test messages, i.e. put and get2, to be processed and then check the assertion.

Summary of Built-in Features

In summary, Setak provides teh following features:

In the "test" folder of the source code, you can find the examples and also test cases written for Setak. Both of them show the usage of Setak.