59
Reactive Amsterdam meetup 12.07.2016 Mike Kotsur @sotomajor_ua Actor testing patterns with TestKit

Akka Testkit Patterns

Embed Size (px)

Citation preview

Reactive Amsterdam meetup 12.07.2016

Mike Kotsur@sotomajor_ua

Actor testing patterns with TestKit

• Testing

• Testing Akka applications

• Better testing Akka applications

• Testing Akka applications with less problems

Agenda

TDD

A provocative talk and blog posts has led to a conversation where we aim to understand each others' views and experiences.

http://martinfowler.com/articles/is-tdd-dead/

Is TDD dead?M. Fowler

K. Beck

David Heinemeier Hansson

Programmers at work maintaining a Ruby on Rails testless application

Eero Järnefelt, Oil on canvas, 1893

A test

An async test

An async test

• Retry the assertion many times;

• The worker tells when the work is done.

2 conceptual solutions

• Retry the assertion many times;

• The worker tells when the work is done.

2 conceptual solutions questions

How many times?

How long to wait?

• Not just actors and messages!

• Willing to help you with the test kit.

So, what about Akka?

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 }}

// TODO: test this

class IncrementorActorTest extends TestKit(ActorSystem("test-system")) { // it(“should …”) { … }}

// We need an actor system

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

// Uses the same thread

Has a real type!

it("should increment on new messages1") { val actorRef = TestActorRef[IncrementorActor] actorRef ! Inc(2) actorRef.underlyingActor.sum shouldEqual 2 actorRef.underlyingActor.receive(Inc(3)) actorRef.underlyingActor.sum shouldEqual 5 } it("should increment on new messages2") { val actorRef = TestActorRef[IncrementorActor] actorRef ! Inc(2) actorRef.underlyingActor.sum shouldEqual 2 actorRef.underlyingActor.receive(Inc(3)) actorRef.underlyingActor.sum shouldEqual 5 }

// “Sending” messages

Style 1

Style 2

class LazyIncrementorActor extends Actor { var sum: Int = 0 override def receive: Receive = { case Inc(i) => Future { Thread.sleep(100) sum = sum + i } }}

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

// ... with ImplicitSender

it("should have sum = 0 by default") {

val actorRef = system .actorOf(Props(classOf[IncrementorActor]))

actorRef ! Result

expectMsg(0) }

TestKit trait

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) }

TestKit

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

// expectMsg*

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

// fishing*

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

// await*

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

def ignoreNoMsg()

// ignore*

val probe = TestProbe()

probe watch target

target ! PoisonPill

probe.expectTerminated(target)

// death watch

Somewhere in the app code

• context.actorOf()

• context.parent

• context.child(name)

Dependency injection

• Use props;

• Use childMaker: ActorRefFactory => ActorRef;

• Use a fabricated parent.

Dependency Injection

class MyActor extends Actor with ActorLogging {

override def receive: Receive = { case DoSideEffect =>

log.info("Hello World!") }

}

// event filter

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

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

// event filter

class MyActor extends Actor with ActorLogging {

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

}

// supervision

// unit-test style

val actorRef = TestActorRef[MyActor](MyActor.props())

val pf = actorRef.underlyingActor .supervisorStrategy.decider

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

// supervision

// coding time

// better tests

flaky /ˈfleɪki/ adjective, informal

(of a device or software) prone to break down; unreliable.

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

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

}

// shutting down the actor system

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

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

}

// shutting down the actor system

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

// timeouts

import scala.concurrent.duration._ import akka.testkit._ 10.milliseconds.dilated

class Settings(...) extends Extension {

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

}

// settings extension

// test val config = ConfigFactory.parseString(""" app.jdbc.driver = "org.h2.Driver" app.jdbc.url = "jdbc:h2:mem:repository" """) val system = ActorSystem("testsystem", config)

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

// settings extension

case class Identify(messageId: Any)

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

// dynamic actors

// Beware of unique name lifecycles and scopes…

// 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

-Dakka.test.someImportantProperty=3000

// Don’t hesitate to rewrite a test, or the code.

// Don’t trust assertion errors, check logs.

// Base your decisions on historical data.

// QA?