Как насмехаться с ребенком Актеры для тестирования системы Akka?

Когда у меня есть родительский актер в Акке, он непосредственно создает дочернего актера при инициализации, когда я хочу написать модульные тесты для родительского актера, как я могу заменить дочернего актера тестовым или макетным?

Например, со следующим примером надуманного кода:

class TopActor extends Actor {
  val anotherActor = context.actorOf(AnotherActor.props, "anotherActor")

  override def receive: Receive = {
    case "call another actor" => anotherActor ! "hello"
  }
}

class AnotherActor extends Actor {

  override def recieve: Receive = {
    case "hello" => // do some stuff
  }

}

Если я хочу написать тест для TopActor, чтобы проверить сообщение, отправленное в AnotherActor, это "привет", как заменить реализацию AnotherActor? Кажется, что TopActor создает этот ребенок напрямую, поэтому доступ к нему непросто.

Ответ 1

Кажется, что работает следующий подход, но переопределение значения val другого пользователя напрямую кажется немного грубым. Мне было интересно, есть ли какие-либо другие чистые/рекомендуемые решения, поэтому я все еще задавал вопрос, хотя у меня есть этот рабочий ответ:

class TopActorSpec extends MyActorTestSuiteTrait {
  it should "say hello to AnotherActor when receive 'call another actor'" {
    val testProbe = TestProbe()

    val testTopActor = TestActorRef(Props(new TopActor {
      override val anotherActor = testProbe.ref
    }))

    testTopActor ! "call another actor"
    testProbe.expectMsg(500 millis, "hello")
  }
}

Ответ 2

Возможно, это решение поможет любому решить эту проблему.

У меня есть класс родитель-актер, который создает некоторых дочерних актеров. Родитель-актер действует как форвардер, он проверяет, существует ли ребенок по предоставленному идентификатору и отправляет ему сообщение, если это так. В parent-actor я использую context.child(actorId), чтобы проверить, существует ли уже ребенок. Если я хочу проверить, как будет действовать родитель-актер, и что он ему отправит, я использую ниже код:

"ParentActor " should " send XXX message to child actor if he receives YYY message" in {
   val parentActor = createParentActor(testActor, "child_id")
   parentActor ! YYY("test_id")
   expectMsg( XXX )
}

def createParentActor(mockedChild: ActorRef, mockedChildId: String): ParentActor = {
    TestActorRef( new ParentActor(){
      override def preStart(): Unit = {
        context.actorOf( Props(new Forwarder(mockedChild)), mockedChildId)
      }
    } )
  }

  class Forwarder(target: ActorRef) extends Actor {
    def receive = {
      case msg => target forward msg
    }
  }

Ответ 3

Вы можете проверить это решение, которое я нашел онлайн (кредиты идут на Stig Brautaset): http://www.superloopy.io/articles/2013/injecting-akka-testprobe.html

Это изящное решение, но немного сложное. Он начинается с создания anotherActor через признак (ChildrenProvider), чем у вас может быть productionChildrenProvider, который возвращает экземпляр AnotherActor. Во время теста testChildrenProvider вернет TestProbe вместо этого. Глядя на тестовый код, он довольно чистый. Но реализация Actor - это то, о чем я должен думать.

Ответ 4

Я новичок в Scala сам. Тем не менее я столкнулся с той же проблемой и подошел к ней следующим образом. Идея моего подхода заключается в том, чтобы ввести информацию о том, как создать дочернего актера в соответствующем родителе. Чтобы обеспечить чистую инициализацию, я создаю метод factory, который я использую для создания самого актера:

object Parent { 
  def props() :Props {
    val childSpawner = {
      (context :ActorContext) => context.actorOf(Child.props())
    }
    Props(classOf[Parent], spawnChild)
  }
}

class Parent(childSpawner: (ActorContext) => ActorRef) extends Actor {
  val childActor = childSpawner(context)
  context.watch(childActor)

  def receive = {
    // Whatever
  }
}

object Child {
  def props() = { Props(classOf[Child]) }
}

class Child extends Actor {
  // Definition of Child
}

Затем вы можете проверить это следующим образом:

// This returns a new actor spawning function regarding the FakeChild
object FakeChildSpawner{
  def spawn(probe :ActorRef) = {
    (context: ActorContext) => {
      context.actorOf(Props(new FakeChild(probe)))
    }
  }
}

// Fake Child forewarding messages to TestProbe
class FakeChild(probeRef :ActorRef) extends Actor {
  def receive = {
    case msg => probeRef ! (msg)
  }
}

"trigger actions of it children" in {
  val probe = TestProbe()

  // Replace logic to spawn Child by logic to spawn FakeChild
  val actorRef = TestActorRef(
    new Parent(FakeChildSpawner.spawn(probe.ref))
  )

  val expectedForewardedMessage = "expected message to child"
  actorRef ! "message to parent"

  probe.expectMsg("expected message to child")
}

Выполняя это, вы извлекаете действие нереста из родителя в анонимную функцию, которая может быть заменена в тесте актером FakeChild, который полностью находится в ваших руках. Представление сообщений от FakeChild до TestProbe решает проблему тестирования.

Я надеюсь, что это поможет.