4
votes

I expanded the example from http://doc.akka.io/docs/akka/snapshot/scala/testing.html#Using_Multiple_Probe_Actors.

import akka.actor._
import akka.testkit.{TestProbe, TestKit}
import org.scalatest.{Suites, BeforeAndAfter, BeforeAndAfterAll, FlatSpecLike}
import scala.concurrent.duration._

class TestProbesTestSuites extends Suites(new TestProbesTest)

class TestProbesTest extends TestKit(ActorSystem("TestProbesTestSystem")) with FlatSpecLike with BeforeAndAfterAll with BeforeAndAfter {
  override def afterAll: Unit = {
    TestKit.shutdownActorSystem(system)
  }

  "A TestProbeTest" should "test TestProbes" in {
    val actorRef = system.actorOf(Props[TestActor], "TestActor")
    val tester1 = TestProbe()
    val tester2 = TestProbe()

    Thread.sleep(500.milliseconds.toMillis)

    actorRef ! (tester1.ref, tester2.ref)
    // When you comment the next line the test fails
    tester1.expectMsg(500.milliseconds, "Hello")
    tester2.expectMsg(500.milliseconds, "Hello")

    // Alternative test
//    Thread.sleep(500.milliseconds.toMillis)
//    tester1.expectMsg(0.milliseconds, "Hello")
//    tester2.expectMsg(0.milliseconds, "Hello")
    ()
  }
}

class TestActor extends Actor with ActorLogging {
  override def receive: Receive = {
    case (actorRef1: ActorRef, actorRef2: ActorRef) => {
      // When you change the schedule time in the next line to 100.milliseconds the test fails
      context.system.scheduler.scheduleOnce(400.milliseconds, actorRef1,  "Hello")(context.system.dispatcher)
      context.system.scheduler.scheduleOnce(800.milliseconds, actorRef2,  "Hello")(context.system.dispatcher)
    }
    case x => log.warning(x.toString)
  }
}

I do not think that this a correct or good usage for a test. When I remove the tester1.expectMsg(500.milliseconds, "Hello") the test fails, so testing of tester2 dependes on testing tester1. In my opinion this bad.

Also changing line context.system.scheduler.scheduleOnce(400.milliseconds, actorRef1, "Hello")(context.system.dispatcher) to a delay of 100 milliseconds let the test fail. So testing message 2 depends on sending message 1. In my opinion this is bad, too.

To solve this I would add a Thread.sleep after sending the message and change the wait time for both #expectMsg to 0 milliseconds. Thread.sleep does also not look good for me in tests, but I think it is a must in actor testing. Is this the right way?

I thought TestProbe is made for testing multiple actors. But for me the wait time parameter of #expectMsg is quite useless, when testing multiple actors.

Any remarks are welcome.

2
If you are scheduling the message to tester2 to be received in 800 milliseconds then it's always going to fail if you expect it to be received in 500 milliseconds. It just happens to work because the first assertion causes a wait of 400ms beforehand.cmbaxter

2 Answers

3
votes

You could try a within block instead like so:

within(800.milliseconds, 900.milliseconds){
  tester1.expectMsg("Hello")
  tester2.expectMsg("Hello")
}

That way if you comment out the assertion to tester1 the test still passes.

2
votes

The test probe works synchronously. The expectMsg method is blocking. And it's good, because you want to run tests in one thread and have control over it. There are no race conditions this way.

The timeout while expecting messages is measured from the time the line is read, which means that the 500 millis in tester2.expectMsg(500.milliseconds, "Hello") will start counting down only after tester1 receives his message.

If tester1 doesn't get the message, you want to fail the test immediately, you don't need to continue to tester2.

On the other hand, scheduler is done asynchronously, in multiple threads, and the countdown starts right away because there is no blocking. So the scheduled 400 millis and 800 millis will start counting down (nearly) at the same time.

So changing the 400 millis to 100 millis means that the second message will still be sent approximately 800 millis from start, but tester1 will be expecting it 100 + 500 = 600 millis from start. That's why it fails.