DOC: Update Testing Actor Systems (TestKit) Chapter. See #1486

This commit is contained in:
Patrik Nordwall 2011-12-15 15:03:05 +01:00
parent 73b79d6e3e
commit 30416af3c4
3 changed files with 334 additions and 189 deletions

View file

@ -0,0 +1,44 @@
package akka.docs.testkit
//#plain-spec
import akka.actor.ActorSystem
import akka.actor.Actor
import akka.actor.Props
import akka.testkit.TestKit
import org.scalatest.WordSpec
import org.scalatest.matchers.MustMatchers
import org.scalatest.BeforeAndAfterAll
import akka.testkit.ImplicitSender
object MySpec {
class EchoActor extends Actor {
def receive = {
case x sender ! x
}
}
}
//#implicit-sender
class MySpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender
with WordSpec with MustMatchers with BeforeAndAfterAll {
//#implicit-sender
def this() = this(ActorSystem("MySpec"))
import MySpec._
override def afterAll {
system.shutdown()
}
"An Echo actor" must {
"send back messages unchanged" in {
val echo = system.actorOf(Props[EchoActor])
echo ! "hello world"
expectMsg("hello world")
}
}
}
//#plain-spec

View file

@ -0,0 +1,238 @@
package akka.docs.testkit
//#imports-test-probe
import akka.testkit.TestProbe
import akka.actor.Actor
import akka.actor.ActorRef
import akka.actor.Props
import akka.util.duration._
//#imports-test-probe
import akka.testkit.AkkaSpec
import akka.actor.Actor
import akka.testkit.DefaultTimeout
import akka.testkit.ImplicitSender
import akka.actor.ActorRef
import akka.actor.Props
object TestkitDocSpec {
case object Say42
case object Unknown
class MyActor extends Actor {
def receive = {
case Say42 sender ! 42
case "some work" sender ! "some result"
}
}
//#my-double-echo
class MyDoubleEcho extends Actor {
var dest1: ActorRef = _
var dest2: ActorRef = _
def receive = {
case (d1: ActorRef, d2: ActorRef)
dest1 = d1
dest2 = d2
case x
dest1 ! x
dest2 ! x
}
}
//#my-double-echo
import akka.testkit.TestProbe
//#test-probe-forward-actors
class Source(target: ActorRef) extends Actor {
def receive = {
case "start" target ! "work"
}
}
class Destination extends Actor {
def receive = {
case x // Do something..
}
}
//#test-probe-forward-actors
class LoggingActor extends Actor {
//#logging-receive
import akka.event.LoggingReceive
implicit def system = context.system
def receive = LoggingReceive(this) {
case msg // Do something...
}
//#logging-receive
}
}
class TestkitDocSpec extends AkkaSpec with DefaultTimeout with ImplicitSender {
import TestkitDocSpec._
"demonstrate usage of TestActorRef" in {
//#test-actor-ref
import akka.testkit.TestActorRef
val actorRef = TestActorRef[MyActor]
val actor = actorRef.underlyingActor
//#test-actor-ref
}
"demonstrate usage of TestFSMRef" in {
//#test-fsm-ref
import akka.testkit.TestFSMRef
import akka.actor.FSM
import akka.util.duration._
val fsm = TestFSMRef(new Actor with FSM[Int, String] {
startWith(1, "")
when(1) {
case Ev("go") goto(2) using "go"
}
when(2) {
case Ev("back") goto(1) using "back"
}
})
assert(fsm.stateName == 1)
assert(fsm.stateData == "")
fsm ! "go" // being a TestActorRef, this runs also on the CallingThreadDispatcher
assert(fsm.stateName == 2)
assert(fsm.stateData == "go")
fsm.setState(stateName = 1)
assert(fsm.stateName == 1)
assert(fsm.timerActive_?("test") == false)
fsm.setTimer("test", 12, 10 millis, true)
assert(fsm.timerActive_?("test") == true)
fsm.cancelTimer("test")
assert(fsm.timerActive_?("test") == false)
//#test-fsm-ref
}
"demonstrate testing of behavior" in {
//#test-behavior
import akka.testkit.TestActorRef
import akka.util.duration._
import akka.dispatch.Await
val actorRef = TestActorRef(new MyActor)
// hypothetical message stimulating a '42' answer
val result = Await.result((actorRef ? Say42), 5 seconds).asInstanceOf[Int]
result must be(42)
//#test-behavior
}
"demonstrate unhandled message" in {
//#test-unhandled
import akka.testkit.TestActorRef
import akka.actor.UnhandledMessageException
val ref = TestActorRef[MyActor]
intercept[UnhandledMessageException] { ref(Unknown) }
//#test-unhandled
}
"demonstrate expecting exceptions" in {
//#test-expecting-exceptions
import akka.testkit.TestActorRef
val actorRef = TestActorRef(new Actor {
def receive = {
case boom throw new IllegalArgumentException("boom")
}
})
intercept[IllegalArgumentException] { actorRef("hello") }
//#test-expecting-exceptions
}
"demonstrate within" in {
type Worker = MyActor
//#test-within
import akka.actor.Props
import akka.util.duration._
val worker = system.actorOf(Props[Worker])
within(200 millis) {
worker ! "some work"
expectMsg("some result")
expectNoMsg // will block for the rest of the 200ms
Thread.sleep(300) // will NOT make this block fail
}
//#test-within
}
"demonstrate dilated duration" in {
//#duration-dilation
import akka.util.duration._
import akka.testkit._
10.milliseconds.dilated
//#duration-dilation
}
"demonstrate usage of probe" in {
//#test-probe
val probe1 = TestProbe()
val probe2 = TestProbe()
val actor = system.actorOf(Props[MyDoubleEcho])
actor ! (probe1.ref, probe2.ref)
actor ! "hello"
probe1.expectMsg(50 millis, "hello")
probe2.expectMsg(50 millis, "hello")
//#test-probe
//#test-special-probe
case class Update(id: Int, value: String)
val probe = new TestProbe(system) {
def expectUpdate(x: Int) = {
expectMsgPF() {
case Update(id, _) if id == x true
}
sender ! "ACK"
}
}
//#test-special-probe
}
"demonstrate probe reply" in {
import akka.testkit.TestProbe
import akka.util.duration._
//#test-probe-reply
val probe = TestProbe()
val future = probe.ref ? "hello"
probe.expectMsg(0 millis, "hello") // TestActor runs on CallingThreadDispatcher
probe.sender ! "world"
assert(future.isCompleted && future.value == Some(Right("world")))
//#test-probe-reply
}
"demonstrate probe forward" in {
import akka.testkit.TestProbe
import akka.actor.Props
//#test-probe-forward
val probe = TestProbe()
val source = system.actorOf(Props(new Source(probe.ref)))
val dest = system.actorOf(Props[Destination])
source ! "start"
probe.expectMsg("work")
probe.forward(dest)
//#test-probe-forward
}
"demonstrate " in {
//#calling-thread-dispatcher
import akka.testkit.CallingThreadDispatcher
val dispatcher = new CallingThreadDispatcher(system.dispatcherFactory.prerequisites)
val ref = system.actorOf(Props[MyActor].withDispatcher(dispatcher))
//#calling-thread-dispatcher
}
}

View file

@ -15,11 +15,6 @@ Testing Actor Systems
.. module:: akka-testkit
:synopsis: Tools for Testing Actor Systems
.. moduleauthor:: Roland Kuhn
.. versionadded:: 1.0
.. versionchanged:: 1.1
added :class:`TestActorRef`
.. versionchanged:: 1.2
added :class:`TestFSMRef`
As with any piece of software, automated tests are a very important part of the
development cycle. The actor model presents a different view on how units of
@ -74,12 +69,7 @@ Having access to the actual :class:`Actor` object allows application of all
traditional unit testing techniques on the contained methods. Obtaining a
reference is done like this:
.. code-block:: scala
import akka.testkit.TestActorRef
val actorRef = TestActorRef[MyActor]
val actor = actorRef.underlyingActor
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#test-actor-ref
Since :class:`TestActorRef` is generic in the actor type it returns the
underlying actor with its proper static type. From this point on you may bring
@ -92,35 +82,9 @@ Testing Finite State Machines
If your actor under test is a :class:`FSM`, you may use the special
:class:`TestFSMRef` which offers all features of a normal :class:`TestActorRef`
and in addition allows access to the internal state::
and in addition allows access to the internal state:
import akka.testkit.TestFSMRef
import akka.util.duration._
val fsm = TestFSMRef(new Actor with FSM[Int, String] {
startWith(1, "")
when (1) {
case Ev("go") => goto(2) using "go"
}
when (2) {
case Ev("back") => goto(1) using "back"
}
})
assert (fsm.stateName == 1)
assert (fsm.stateData == "")
fsm ! "go" // being a TestActorRef, this runs also on the CallingThreadDispatcher
assert (fsm.stateName == 2)
assert (fsm.stateData == "go")
fsm.setState(stateName = 1)
assert (fsm.stateName == 1)
assert (fsm.timerActive_?("test") == false)
fsm.setTimer("test", 12, 10 millis, true)
assert (fsm.timerActive_?("test") == true)
fsm.cancelTimer("test")
assert (fsm.timerActive_?("test") == false)
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#test-fsm-ref
Due to a limitation in Scalas type inference, there is only the factory method
shown above, so you will probably write code like ``TestFSMRef(new MyFSM)``
@ -150,11 +114,7 @@ usual. This trick is made possible by the :class:`CallingThreadDispatcher`
described below; this dispatcher is set implicitly for any actor instantiated
into a :class:`TestActorRef`.
.. code-block:: scala
val actorRef = TestActorRef(new MyActor).start()
val result = (actorRef ? Say42).as[Int] // hypothetical message stimulating a '42' answer
result must be (Some(42))
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#test-behavior
As the :class:`TestActorRef` is a subclass of :class:`LocalActorRef` with a few
special extras, also aspects like linking to a supervisor and restarting work
@ -172,7 +132,7 @@ One more special aspect which is overridden for single-threaded tests is the
To summarize: :class:`TestActorRef` overwrites two fields: it sets the
dispatcher to :obj:`CallingThreadDispatcher.global` and it sets the
:obj:`receiveTimeout` to zero.
:obj:`receiveTimeout` to None.
The Way In-Between
------------------
@ -180,15 +140,13 @@ The Way In-Between
If you want to test the actor behavior, including hotswapping, but without
involving a dispatcher and without having the :class:`TestActorRef` swallow
any thrown exceptions, then there is another mode available for you: just use
the :class:`TestActorRef` as a partial function, the calls to
:meth:`isDefinedAt` and :meth:`apply` will be forwarded to the underlying
actor:
the :meth:`apply` method :class:`TestActorRef`, which will be forwarded to the
underlying actor:
.. code-block:: scala
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#test-unhandled
val ref = TestActorRef[MyActor]
ref.isDefinedAt('unknown) must be (false)
intercept[IllegalActorStateException] { ref(RequestReply) }
The above sample assumes the default behavior for unhandled messages, i.e.
that the actor doesn't swallow all messages and doesn't override :meth:`unhandled`.
Use Cases
---------
@ -221,33 +179,13 @@ stimuli at varying injection points and arrange results to be sent from
different emission points, but the basic principle stays the same in that a
single procedure drives the test.
The :class:`TestKit` trait contains a collection of tools which makes this
common task easy:
The :class:`TestKit` class contains a collection of tools which makes this
common task easy.
.. code-block:: scala
.. includecode:: code/akka/docs/testkit/PlainWordSpec.scala#plain-spec
import akka.testkit.TestKit
import org.scalatest.WordSpec
import org.scalatest.matchers.MustMatchers
class MySpec extends WordSpec with MustMatchers with TestKit {
"An Echo actor" must {
"send back messages unchanged" in {
val echo = Actor.actorOf(Props[EchoActor])
echo ! "hello world"
expectMsg("hello world")
}
}
}
The :class:`TestKit` contains an actor named :obj:`testActor` which is
implicitly used as sender reference when dispatching messages from the test
When using ``with ImplicitSender`` the :class:`TestKit` contains an actor named :obj:`testActor`
which is implicitly used as sender reference when dispatching messages from the test
procedure. This enables replies to be received by this internal actor, whose
only function is to queue them so that interrogation methods like
:meth:`expectMsg` can examine them. The :obj:`testActor` may also be passed to
@ -256,6 +194,9 @@ is a whole set of examination methods, e.g. receiving all consecutive messages
matching certain criteria, receiving a whole sequence of fixed messages or
classes, receiving nothing for some time, etc.
To avoid memory leaks it is important that you shutdown the :class:`ActorSystem`
when the test is finished, as illustrated in the :meth:`afterAll` method above.
.. note::
The test actor shuts itself down by default after 5 seconds (configurable)
@ -385,15 +326,11 @@ with message flows:
Expecting Exceptions
--------------------
One case which is not handled by the :obj:`testActor` is if an exception is
thrown while processing the message sent to the actor under test. This can be
tested by using a :class:`Future` based invocation::
Testing that an expected exception is thrown while processing a message sent to
the actor under test can be done by using a :class:`TestActorRef` :meth:`apply` based
invocation:
// assuming ScalaTest ShouldMatchers
evaluating {
(someActor ? badOperation).await.get
} should produce [UnhandledMessageException]
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#test-expecting-exceptions
.. _TestKit.within:
@ -426,21 +363,7 @@ It should be noted that if the last message-receiving assertion of the block is
latencies. This means that while individual contained assertions still use the
maximum time bound, the overall block may take arbitrarily longer in this case.
.. code-block:: scala
class SomeSpec extends WordSpec with MustMatchers with TestKit {
"A Worker" must {
"send timely replies" in {
val worker = ActorSystem().actorOf(...)
within (500 millis) {
worker ! "some work"
expectMsg("some result")
expectNoMsg // will block for the rest of the 500ms
Thread.sleep(1000) // will NOT make this block fail
}
}
}
}
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#test-within
.. note::
@ -460,25 +383,18 @@ invariably lead to spurious test failures on the heavily loaded Jenkins server
internally scaled by a factor taken from the :ref:`configuration`,
``akka.test.timefactor``, which defaults to 1.
You can scale other durations with the same factor by using the implicit conversion
in ``akka.testkit`` package object to add dilated function to :class:`Duration`.
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#duration-dilation
Resolving Conflicts with Implicit ActorRef
------------------------------------------
If you want the sender of messages inside your TestKit-based tests to be the `testActor```
simply mix in `ÌmplicitSender`` into your test.
If you want the sender of messages inside your TestKit-based tests to be the ``testActor``
simply mix in ``ÌmplicitSender`` into your test.
.. code-block:: scala
class SomeSpec extends WordSpec with MustMatchers with TestKit with ImplicitSender {
"A Worker" must {
"send timely replies" in {
val worker = ActorSystem().actorOf(...)
within (500 millis) {
worker ! "some work" // testActor is the "sender" for this message
expectMsg("some result")
}
}
}
}
.. includecode:: code/akka/docs/testkit/PlainWordSpec.scala#implicit-sender
Using Multiple Probe Actors
---------------------------
@ -489,42 +405,16 @@ at the :obj:`testActor` when using the :class:`TestKit` as a mixin. Another
approach is to use it for creation of simple probe actors to be inserted in the
message flows. To make this more powerful and convenient, there is a concrete
implementation called :class:`TestProbe`. The functionality is best explained
using a small example::
using a small example:
class MyDoubleEcho extends Actor {
var dest1 : ActorRef = _
var dest2 : ActorRef = _
def receive = {
case (d1: ActorRef, d2: ActorRef) =>
dest1 = d1
dest2 = d2
case x =>
dest1 ! x
dest2 ! x
}
}
val probe1 = TestProbe()
val probe2 = TestProbe()
val actor = ActorSystem().actorOf(Props[MyDoubleEcho])
actor ! (probe1.ref, probe2.ref)
actor ! "hello"
probe1.expectMsg(50 millis, "hello")
probe2.expectMsg(50 millis, "hello")
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala
:include: imports-test-probe,my-double-echo,test-probe
Probes may also be equipped with custom assertions to make your test code even
more concise and clear::
more concise and clear:
case class Update(id : Int, value : String)
val probe = new TestProbe {
def expectUpdate(x : Int) = {
expectMsg {
case Update(id, _) if id == x => true
}
reply("ACK")
}
}
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala
:include: test-special-probe
You have complete flexibility here in mixing and matching the :class:`TestKit`
facilities with your own checks and choosing an intuitive name for it. In real
@ -535,13 +425,9 @@ Replying to Messages Received by Probes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The probes keep track of the communications channel for replies, if possible,
so they can also reply::
so they can also reply:
val probe = TestProbe()
val future = probe.ref ? "hello"
probe.expectMsg(0 millis, "hello") // TestActor runs on CallingThreadDispatcher
probe.reply("world")
assert (future.isCompleted && future.as[String] == "world")
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#test-probe-reply
Forwarding Messages Received by Probes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -550,15 +436,10 @@ Given a destination actor ``dest`` which in the nominal actor network would
receive a message from actor ``source``. If you arrange for the message to be
sent to a :class:`TestProbe` ``probe`` instead, you can make assertions
concerning volume and timing of the message flow while still keeping the
network functioning::
network functioning:
val probe = TestProbe()
val system = ActorSystem()
val source = system.actorOf(Props(new Source(probe)))
val dest = system.actorOf(Props[Destination])
source ! "start"
probe.expectMsg("work")
probe.forward(dest)
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala
:include: test-probe-forward-actors,test-probe-forward
The ``dest`` actor will receive the same message invocation as if no test probe
had intervened.
@ -572,7 +453,7 @@ described :ref:`above <TestKit.within>` is local to each probe. Hence, probes
do not react to each other's deadlines or to the deadline set in an enclosing
:class:`TestKit` instance::
class SomeTest extends TestKit {
class SomeTest extends TestKit(_system: ActorSystem) with ImplicitSender {
val probe = TestProbe()
@ -599,25 +480,9 @@ so long as all intervening actors run on this dispatcher.
How to use it
-------------
Just set the dispatcher as you normally would, either from within the actor
Just set the dispatcher as you normally would:
.. code-block:: scala
import akka.testkit.CallingThreadDispatcher
class MyActor extends Actor {
self.dispatcher = CallingThreadDispatcher.global
...
}
or from the client code
.. code-block:: scala
val ref = system.actorOf(Props[MyActor].withDispatcher(CallingThreadDispatcher.global))
As the :class:`CallingThreadDispatcher` does not have any configurable state,
you may always use the (lazily) preallocated one as shown in the examples.
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#calling-thread-dispatcher
How it works
------------
@ -719,13 +584,11 @@ options:
This is enabled by a setting in the :ref:`configuration` — namely
``akka.actor.debug.receive`` — which enables the :meth:`loggable`
statement to be applied to an actors :meth:`receive` function::
statement to be applied to an actors :meth:`receive` function:
import akka.event.LoggingReceive
def receive = LoggingReceive(this) {
case msg => ...
}
.. includecode:: code/akka/docs/testkit/TestkitDocSpec.scala#logging-receive
.
The first argument to :meth:`LoggingReceive` defines the source to be used in the
logging events, which should be the current actor.
@ -755,12 +618,12 @@ All these messages are logged at ``DEBUG`` level. To summarize, you can enable
full logging of actor activities using this configuration fragment::
akka {
loglevel = "DEBUG"
loglevel = DEBUG
actor {
debug {
receive = "true"
autoreceive = "true"
lifecycle = "true"
receive = on
autoreceive = on
lifecycle = on
}
}
}