Actors testing is different from what you are used to. First, you have messages instead of calls, second, you have to deal with concurrency and all the consequences that it brings with it:
* Thread.sleeps in tests;
* Flakiness;
* Green on laptop / red on jenkins;
* Missed test cases.
Fortunately Akka provides a TestKit which helps to avoid all these things when used properly. Let's take out and inspect tools from this kit and learn couple of useful patterns.
4. 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
5. Programmers at work maintaining a Ruby on Rails testless application
Eero Järnefelt, Oil on canvas, 1893
9. • Retry the assertion many times;
• The worker tells when the work is done.
2 conceptual solutions
10. • Retry the assertion many times;
• The worker tells when the work is done.
2 conceptual solutions
questions
How many times?
How long to wait?
11. • Not just actors and messages!
• Willing to help you with the test kit.
So, what about Akka?
12.
13.
14.
15.
16.
17.
18. 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
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 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
22. class LazyIncrementorActor extends Actor {
var sum: Int = 0
override def receive: Receive = {
case Inc(i) =>
Future {
Thread.sleep(100)
sum = sum + i
}
}
}
23.
24.
25.
26.
27. 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
28. // ... with ImplicitSender
it("should have sum = 0 by default") {
val actorRef = system
.actorOf(Props(classOf[IncrementorActor]))
actorRef ! Result
expectMsg(0)
}
TestKit trait
29. 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)
}
40. class MyActor extends Actor with ActorLogging {
override def supervisorStrategy: Unit =
OneForOneStrategy() {
case _: FatalException =>
SupervisorStrategy.Escalate
case _: ShitHappensException =>
SupervisorStrategy.Restart
}
}
// supervision
41. // 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
46. 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