0

I'm trying to obtain internal state of an actor in my unit test, but by some reason the old state persists.

My actor should be adding/removing/listing self-registering actor services:

class DirectoryServiceActor extends Actor {
  var servicesMap: Map[String, List[ActorRef]] = Map.empty[String, List[ActorRef]]

  def receive = {
    case AddService(serviceType) ⇒
      servicesMap = servicesMap + (serviceType -> (sender :: servicesMap.getOrElse(serviceType, List.empty[ActorRef])))
      sender ! Ack
    case RemoveService ⇒
      val oldMap = servicesMap
      servicesMap = servicesMap.mapValues(list ⇒ (if (list.contains(sender)) list.diff(List(sender)) else list).toList)
      println(servicesMap)
      if (servicesMap.equals(oldMap)) {
        sender ! Nack
      } else {
        sender ! Ack
      }

    case ListServices ⇒
      sender ! services
  }

  def services: Map[String, List[ActorRef]] = this.servicesMap
}

And my test is

"Remove existing service successfully" in {
  implicit val timeout = 10 millis

  val probe = new TestProbe(system)

  val directoryService = TestActorRef[DirectoryServiceActor]
  val actor = directoryService.underlyingActor

  directoryService.tell(AddService("test"), probe.ref)

  probe.expectMsg(timeout, Ack)

  directoryService.tell(RemoveService, probe.ref)

  probe.expectMsg(timeout, Ack)

  println("TEST: " + actor.services)

  actor.services("test") should not contain (probe.ref)
}

Judging by failed test and console output it seems that actor.underlyingActor.services returns the old value:

Map(test -> List())
TEST: Map(test -> List(Actor[akka://myApp/system/testActor3#-2080677614]))

Even though inside of the actor, the variable has already been set to a new value. What have I missed?

Update: Seems not to be related to Akka, actually, but can be worked around using futures in the test:

"Remove existing service successfully" in {
  implicit val timeout = Timeout(100 millis)

  val directoryService = TestActorRef[DirectoryServiceActor]

  val addResponseFuture = directoryService ? AddService(self, "test")

  addResponseFuture.value.get should be(Success(Ack(self)))

  val removeResponseFuture = directoryService ? RemoveService(self)

  removeResponseFuture.value.get should be(Success(Ack(self)))

  val listResponseFuture = directoryService ? ListServices

  listResponseFuture.value.get should be(Success(Map("test" -> List())))

  val actor = directoryService.underlyingActor
  actor.services("test") should not contain (self)

}

I suppose that it is happening due to mapValue not actually creating a new map: Scala: Why mapValues produces a view and is there any stable alternatives?

Community
  • 1
  • 1
abatyuk
  • 1,342
  • 9
  • 16
  • That seems very odd to me too. Do you see the same behavior if you move the `val actor = directoryService.underlyingActor` line down to just before your `println`? – joescii Jan 06 '14 at 19:24
  • What does the actor respond with if you send it a `ListServices` message? – Todd Jan 06 '14 at 19:57
  • it returns Map("test" -> List(probe.ref)), even though println(services) inside of case ListServices ⇒ prints Map(test -> List()) – abatyuk Jan 06 '14 at 20:07
  • What if you change `println(servicesMap)` in `receive` to `println(services)`? – joescii Jan 06 '14 at 20:16
  • added println(services) println(servicesMap) both to RemoveServices and ListServices event handlers - they all print Map(test -> List()) - and at the same probe.expectMsg(timeout, Map("test" -> List())) fails with java.lang.AssertionError: assertion failed: expected Map(test -> List()), found Map(test -> List(Actor[akka://myApp/system/testActor2#-413977955])) – abatyuk Jan 06 '14 at 20:28

1 Answers1

2

For some reason, I think that mapValues is what's causing issues for you. Try changing the RemoveService handling as follows:

case RemoveService =>
  val oldMap = servicesMap      
  servicesMap = servicesMap.map{
    case (key, list) => (key, list.filterNot(_ == sender))
  }

  if (servicesMap.equals(oldMap)) {
    sender ! Nack
  } else {
    sender ! Ack
  }
cmbaxter
  • 35,283
  • 4
  • 86
  • 95
  • And actually found the reason why: http://stackoverflow.com/questions/14882642/scala-why-mapvalues-produces-a-view-and-is-there-any-stable-alternatives – abatyuk Jan 07 '14 at 02:44