Be smart when testing your Akka code

Preview:

Citation preview

Blood, sweat and tears

... or be smart when testing your Akka code

• PHP, NodeJS, AngularJS, Python, Java, Scala;

• Living in the Netherlands, working at

• Developing release automation product: XL Release.

About me

github:mkotsur/restito

• TDD is great when done properly!

• Reactive complexity;

• Learned a lot during last 4 months.

Why testing?

• Tests;

• Better tests;

• Problem-less tests.

• Concurrency, parallelism, state.

• Not just messages;

• Willing to help you with the TestKit.

Not only messages

Not only messages

Not only messages

Not only messages

Not only messages

object IncrementorActorMessages {

case class Inc(i: Int)

}

class IncrementorActor extends Actor {

var sum: Int = 0

override def receive: Receive = { case Inc(i) => sum = sum + i }

}

Sync unit-testing

• Works with `CallingThreadDispatcher`;

• Supports either message-sending style, or direct invocations.

class IncrementorActorTest extends TestKit(ActorSystem(“test-system")) {

...

}

it("should have sum = 0 by default") { val actorRef = TestActorRef[IncrementorActor]

actorRef.underlyingActor.sum shouldEqual 0 }

it("should increment on new messages") { val actorRef = TestActorRef[IncrementorActor]

actorRef ! Inc(2) actorRef.underlyingActor.sum shouldEqual 2

actorRef.underlyingActor.receive(Inc(3)) actorRef.underlyingActor.sum shouldEqual 5 }

class LazyIncrementorActor extends Actor {

var sum: Int = 0

override def receive: Receive = { case Inc(i) => Future { Thread.sleep(100) sum = sum + i } }

}

Not good enough

Not only messages

object IncrementorActorMessages {

case class Inc(i: Int)

case object Result

}

class IncrementorActor extends Actor {

var sum: Int = 0

override def receive: Receive = {

case Inc(i) => sum = sum + i

case Result => sender() ! sum }

}

New message

it("should have sum = 0 by default") { val actorRef = system .actorOf(Props(classOf[IncrementorActor]))

val probe = TestProbe() actorRef.tell(Result, probe.ref)

probe.expectMsg(0) }

Using TestProbe

it("should have sum = 0 by default") { val actorRef = system .actorOf(Props(classOf[IncrementorActor]))

actorRef ! Result

expectMsg(0) }

Using TestProbe

... with ImplicitSender

it("should increment on new messages") { val actorRef = system .actorOf(Props(classOf[IncrementorActor]))

actorRef ! Inc(2) actorRef ! Result

expectMsg(2)

actorRef ! Inc(3) actorRef ! Result

expectMsg(5) }

Using TestProbe

expectMsg*

def expectMsg[T](d: Duration, msg: T): T

def expectMsgPF[T](d: Duration) (pf: PartialFunction[Any, T]): T

def expectMsgClass[T](d: Duration, c: Class[T]): T

def expectNoMsg(d: Duration) // blocks

Fishing

def receiveN(n: Int, d: Duration): Seq[AnyRef]

def receiveWhile[T](max: Duration, idle: Duration, n: Int) (pf: PartialFunction[Any, T]): Seq[T]

def fishForMessage(max: Duration, hint: String) (pf: PartialFunction[Any, Boolean]): Any

Awaits

def awaitCond(p: => Boolean, max: Duration, interval: Duration)

def awaitAssert(a: => Any, max: Duration, interval: Duration)

// from ScalaTest

def eventually[T](fun: => T) (implicit config: PatienceConfig): T

Ignores

def ignoreMsg(pf: PartialFunction[AnyRef, Boolean])

def ignoreNoMsg()

Death watching

val probe = TestProbe()

probe watch target target ! PoisonPill

probe.expectTerminated(target)

Test probes as dependencies

class HappyParentActor(childMaker: ActorRefFactory => ActorRef) extends Actor {

val child: ActorRef = childMaker(context)

override def receive: Receive = { case msg => child.forward(msg) }

}

Event filter

class MyActor extends Actor with ActorLogging {

override def receive: Receive = {

case DoSideEffect => log.info("Hello World!") } }

Event filter

EventFilter.info( message = "Hello World!", occurrences = 1 ).intercept { myActor ! DoSomething }

akka.loggers = ["akka.testkit.TestEventListener"]

Supervision

class MyActor extends Actor with ActorLogging {

override def supervisorStrategy: Unit = OneForOneStrategy() { case _: FatalException => SupervisorStrategy.Escalate case _: ShitHappensException => SupervisorStrategy.Restart }

}

Supervision

val actorRef = TestActorRef[MyActor](MyActor.props()) val pf = actorRef.underlyingActor .supervisorStrategy.decider

pf(new FatalException()) should be (Escalate) pf(new ShitHappensException()) should be (Restart)

• Tests;

• Better tests;

• Problem-less tests.

TestBase

class MyActorTest extends TestKit(ActorSystem("test-system")) with FunSpecLike {

override protected def afterAll(): Unit = { super.afterAll() system.shutdown() system.awaitTermination() } }

TestBaseclass MyActorTest extends TestKit(ActorSystem("my-system")) with AkkaTestBase { ... }

trait AkkaTestBase extends BeforeAndAfterAll with FunSpecLike { this: TestKit with Suite =>

override protected def afterAll() { super.afterAll() system.shutdown() system.awaitTermination() } }

TestBase: v2class MyActorTest extends AkkaTestBase { ... }

abstract class AkkaTestBase extends TestKit(ActorSystem("test-system")) with FunSpecLike with BeforeAndAfterAll {

override protected def afterAll() { super.afterAll() system.shutdown() } }

Timeouts

akka.test.single-expect-default = 3 seconds

akka.test.timefactor = 10

Settings extensionclass Settings(...) extends Extension {

object Jdbc { val Driver = config.getString("app.jdbc.driver") val Url = config.getString("app.jdbc.url") }

}

Settings extension

class MyActor extends Actor { val settings = Settings(context.system) val connection = client.connect( settings.Jdbc.Driver, settings.Jdbc.Url ) }

Settings extension

val config = ConfigFactory.parseString(""" app.jdbc.driver = "org.h2.Driver" app.jdbc.url = "jdbc:h2:mem:repository" """)

val system = ActorSystem("testsystem", config)

Dynamic actors

case class Identify(messageId: Any)

case class ActorIdentity( correlationId: Any, ref: Option[ActorRef] )

• Continuous delivery;

• No dedicated QA engineers;

• 500+ Jenkins jobs.

We depend on tests

• Tests;

• Better tests;

• Problem-less tests.

Be careful with mocking.

Prefer checking messages over checking side-effects.

Single responsibility principle.

Run your tests on slow VM and different OS.

Extract *all* timeouts into conf files. So that you can easily override them on Jenkins.

Don’t hesitate to rewrite test.

Don't hesitate to rewrite application code.

Don’t trust assertion errors, check logs.

Base your decisions on historical data.

Be humane and spread the word.

Questions?

github:mkotsur/akka-smart-testing