1

I have trait for override actorOf in tests:

trait ActorRefFactory {
  this: Actor =>

  def actorOf(props: Props) = context.actorOf(props)
}

And I have worker actor, which stop self when receive any message:

class WorkerActor extends Actor {
  override def receive: Actor.Receive = {
    case _ => { context.stop(self) }
  }
}

Also I have master actor, who creates actors and hold them in queue:

class MasterActor extends Actor with ActorRefFactory {
  var workers = Set.empty[ActorRef]

  override val supervisorStrategy = SupervisorStrategy.stoppingStrategy

  def createWorker() = {
    val worker = context watch actorOf(Props(classOf[WorkerActor]))
    workers += worker
    worker
  }

  override def receive: Receive = {
    case m: String =>
      createWorker()
    case Terminated(ref) =>
      workers -= ref
      createWorker()
  }
}

And this test, which is failed:

class ActorTest(val _system: ActorSystem) extends akka.testkit.TestKit(_system)
  with ImplicitSender
  with Matchers
  with FlatSpecLike {

  def this() = this(ActorSystem("test"))

  def fixture = new {
    val master = TestActorRef(new MasterActor() {
      override def actorOf(props: Props) = TestProbe().ref
    })
  }

  it should "NOT FAILED" in {
    val f = fixture

    f.master ! "create"
    f.master ! "create"

    f.master.underlyingActor.workers.size shouldBe 2

    val worker = f.master.underlyingActor.workers.head
    system.stop(worker)
    Thread.sleep(100)

    f.master.underlyingActor.workers.size shouldBe 2
  }

}

After Thread.sleep in the test, I give error by "1 was not equal to 2". I have not idea what happening. But, if guess I can assume that TestProbe() can't create in the time. What can I do?

Leonard
  • 551
  • 1
  • 10
  • 21

1 Answers1

2

This basically boils down to an asynchronicity issue that you want to try and avoid in unit tests for Akka. You are correctly using a TestActorRef to get hooked into the CallingThreadDispatcher for the master actor. But when you call system.stop(worker), the system still us using the default async dispatcher which will introduce this race condition on the stopping and then re-creating of a worker. The simplest way I can see to fix this issue consistently is to stop the worker like so:

master.underlyingActor.context.stop(worker)

Because you are using the context of master and that actor is using the CallingThreadDispatcher I believe this will remove the asnyc issue that you are seeing. It worked for me when I tried it.

cmbaxter
  • 35,283
  • 4
  • 86
  • 95
  • Unfortunately, it doesn't work. For example, if I add println statement in case Terminated, I got it. It means that Master actor received terminated message. – Leonard Oct 23 '14 at 02:18
  • @Leonard, why are you re-asserting after the stop that the size should still be 2? If the terminated is received properly then the size of the list should go from 2 to 1. Your failure is telling you that your size is 1 and that does not match your expectation of 2 but your expectation should actually be 1. – cmbaxter Oct 23 '14 at 12:38
  • it's not that simple. Look at createWorker() method. When worker has being terminated, master create new worker and put it in workers set. – Leonard Oct 23 '14 at 14:53
  • @Leonard, I added another answer. This one worked for me when I tried it. – cmbaxter Oct 23 '14 at 15:12