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.
2. • PHP, NodeJS, AngularJS, Python, Java, Scala;
• Living in the Netherlands, working at
• Developing release automation product: XL
Release.
About me
14. 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
}
}
15. Sync unit-testing
• Works with `CallingThreadDispatcher`;
• Supports either message-sending style, or direct
invocations.
17. it("should have sum = 0 by default") {
val actorRef = TestActorRef[IncrementorActor]
actorRef.underlyingActor.sum shouldEqual 0
}
18. 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
}
19. 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
22. 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
23. 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
24. it("should have sum = 0 by default") {
val actorRef = system
.actorOf(Props(classOf[IncrementorActor]))
actorRef ! Result
expectMsg(0)
}
Using TestProbe
... with ImplicitSender
25. 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
26. 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
30. Death watching
val probe = TestProbe()
probe watch target
target ! PoisonPill
probe.expectTerminated(target)
31. 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)
}
}
32. Event filter
class MyActor extends Actor with ActorLogging {
override def receive: Receive = {
case DoSideEffect =>
log.info("Hello World!")
}
}
34. Supervision
class MyActor extends Actor with ActorLogging {
override def supervisorStrategy: Unit =
OneForOneStrategy() {
case _: FatalException =>
SupervisorStrategy.Escalate
case _: ShitHappensException =>
SupervisorStrategy.Restart
}
}
35. 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)
41. Settings extension
class Settings(...) extends Extension {
object Jdbc {
val Driver = config.getString("app.jdbc.driver")
val Url = config.getString("app.jdbc.url")
}
}
42. Settings extension
class MyActor extends Actor {
val settings = Settings(context.system)
val connection = client.connect(
settings.Jdbc.Driver,
settings.Jdbc.Url
)
}
43. Settings extension
val config = ConfigFactory.parseString("""
app.jdbc.driver = "org.h2.Driver"
app.jdbc.url = "jdbc:h2:mem:repository"
""")
val system = ActorSystem("testsystem", config)
44. Dynamic actors
case class Identify(messageId: Any)
case class ActorIdentity(
correlationId: Any,
ref: Option[ActorRef]
)