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?

Akka Testkit Patterns

  • 1.
    Reactive Amsterdam meetup12.07.2016 Mike Kotsur
 @sotomajor_ua Actor testing patterns with TestKit
  • 2.
    • Testing • TestingAkka applications • Better testing Akka applications • Testing Akka applications with less problems Agenda
  • 3.
  • 4.
    A provocative talkand 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
  • 5.
    Programmers at workmaintaining a Ruby on Rails testless application Eero Järnefelt, Oil on canvas, 1893
  • 6.
  • 7.
  • 8.
  • 9.
    • Retry theassertion many times; • The worker tells when the work is done. 2 conceptual solutions
  • 10.
    • Retry theassertion many times; • The worker tells when the work is done. 2 conceptual solutions questions How many times? How long to wait?
  • 11.
    • Not justactors and messages! • Willing to help you with the test kit. So, what about Akka?
  • 18.
    object IncrementorActorMessages {
 caseclass Inc(i: Int)
 }
 
 class IncrementorActor extends Actor {
 var sum: Int = 0
 
 override def receive: Receive = {
 case Inc(i) => sum = sum + i
 }
 } // TODO: test this
  • 19.
    class IncrementorActorTest
 extends TestKit(ActorSystem("test-system")){
 
 
 // it(“should …”) { … }
 
 
 } // We need an actor system
  • 20.
    it("should have sum= 0 by default") {
 
 val actorRef = TestActorRef[IncrementorActor]
 actorRef.underlyingActor.sum shouldEqual 0
 
 } // Uses the same thread Has a real type!
  • 21.
    it("should increment onnew 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
  • 22.
    class LazyIncrementorActor extendsActor {
 var sum: Int = 0
 
 override def receive: Receive = {
 case Inc(i) =>
 Future {
 Thread.sleep(100)
 sum = sum + i
 }
 }
 
 }
  • 27.
    object IncrementorActorMessages { caseclass 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
  • 28.
    // ... withImplicitSender it("should have sum = 0 by default") { val actorRef = system .actorOf(Props(classOf[IncrementorActor])) actorRef ! Result expectMsg(0) } TestKit trait
  • 29.
    it("should increment onnew messages") { val actorRef = system .actorOf(Props(classOf[IncrementorActor])) actorRef ! Inc(2) actorRef ! Result expectMsg(2) actorRef ! Inc(3) actorRef ! Result expectMsg(5) }
  • 30.
  • 31.
    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*
  • 32.
    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*
  • 33.
    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*
  • 34.
    def ignoreMsg(pf: PartialFunction[AnyRef,Boolean]) def ignoreNoMsg() // ignore*
  • 35.
    val probe =TestProbe() probe watch target target ! PoisonPill probe.expectTerminated(target) // death watch Somewhere in the app code
  • 36.
    • context.actorOf() • context.parent •context.child(name) Dependency injection
  • 37.
    • Use props; •Use childMaker: ActorRefFactory => ActorRef; • Use a fabricated parent. Dependency Injection
  • 38.
    class MyActor extendsActor with ActorLogging { override def receive: Receive = { case DoSideEffect => log.info("Hello World!") } } // event filter
  • 39.
    // akka.loggers =["akka.testkit.TestEventListener"] EventFilter.info( message = "Hello World!”, occurrences = 1 ).intercept { myActor ! DoSomething } // event filter
  • 40.
    class MyActor extendsActor with ActorLogging { override def supervisorStrategy: Unit = OneForOneStrategy() { case _: FatalException => SupervisorStrategy.Escalate case _: ShitHappensException => SupervisorStrategy.Restart } } // supervision
  • 41.
    // unit-test style valactorRef = TestActorRef[MyActor](MyActor.props()) val pf = actorRef.underlyingActor .supervisorStrategy.decider pf(new FatalException()) should be (Escalate) pf(new ShitHappensException()) should be (Restart) // supervision
  • 42.
  • 43.
  • 44.
    flaky /ˈfleɪki/ adjective, informal (ofa device or software) prone to break down; unreliable.
  • 45.
    class MyActorTest extends TestKit(ActorSystem("test-system")) withFunSpecLike { override protected def afterAll(): Unit = { super.afterAll() system.shutdown() system.awaitTermination() } } // shutting down the actor system
  • 46.
    trait AkkaTestBase extendsBeforeAndAfterAll with FunSpecLike { this: TestKit with Suite => override protected def afterAll() { super.afterAll() system.shutdown() system.awaitTermination() } } // shutting down the actor system
  • 47.
    akka.test.single-expect-default = 3seconds akka.test.timefactor = 10 // timeouts import scala.concurrent.duration._ import akka.testkit._ 10.milliseconds.dilated
  • 48.
    class Settings(...) extendsExtension { object Jdbc { val Driver = config.getString("app.jdbc.driver") val Url = config.getString("app.jdbc.url") } } // settings extension
  • 49.
    // 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
  • 50.
    case class Identify(messageId:Any) case class ActorIdentity( correlationId: Any, ref: Option[ActorRef] ) // dynamic actors
  • 51.
    // Beware ofunique name lifecycles and scopes…
  • 52.
    // Prefer checkingmessages over checking side-effects.
  • 53.
  • 54.
    // Run yourtests on slow VM and different OS.
  • 55.
    // Extract *all*timeouts into conf files. So that you can easily override them -Dakka.test.someImportantProperty=3000
  • 56.
    // Don’t hesitateto rewrite a test, or the code.
  • 57.
    // Don’t trustassertion errors, check logs.
  • 58.
    // Base yourdecisions on historical data.
  • 59.