Модульное тестирование Akka Actors для чайников

Я новичок в Akka и Scala, и я из неконкурентного мира. Возможно, я делаю много вещей неправильно, я буду признателен за обратную связь, даже если она не связана с вопросом.

Я делаю простое приложение для чата с Akka и Scala. Я начал (bc бизнес-требования) с «функции ввода» ... это типичная функция в WhatsApp или телеграмме «Джон печатает сообщение».

Я смоделировал его, используя два типа актеров: Talkers и Conversation, и я хочу провести модульное тестирование своего актера Conversation. Мой актер Conversation выглядит так:

object Conversation {
  def props(conversationId: UUID, talkers: List[ActorRef])(out: ActorRef) = Props(new Conversation(conversationId, talkers))

  case class Typing(talkerId: TalkerId)
}

class Conversation(conversationId: UUID, talkers: List[ActorRef]) extends Actor with ActorLogging {
  def receive = LoggingReceive {
    case Typing(talkerId) =>
      // notify all talkers that a talker is typing
      // @TODO don't notify user which is typing
      talkers foreach {talker: ActorRef => talker ! InterlocutorTyping(talkerId)}
 }
}

Я думаю, сейчас это очень просто. Итак, прежде чем начать программировать на Scala и Akka, я протестировал это так:

  • Я получаю своего актера разговора
  • я издеваюсь над болтунами
  • Я отправляю сообщение Typing моему актеру
  • Я ожидаю, что говорящие должны быть уведомлены

Я действительно не знаю, правильный ли это подход в Scala и Akka. Мой тест (с использованием scalatest) выглядит так:

"Conversation" should {
"Notify interlocutors when a talker is typing" in {
  val talkerRef1 = system.actorOf(Props())
  val talkerRef2 = system.actorOf(Props())

  val talkerRef1Id = TalkerIdStub.random

  val conversationId = UUID.randomUUID()

  val conversationRef = system.actorOf(Props(classOf[Conversation], conversationId, List(talkerRef1, talkerRef2)))

  // should I use TestActorRef ?

  conversationRef ! InterlocutorTyping(talkerRef1Id)

  // assert that talker2 is notified when talker1 is typing
}
}
  1. Должен ли я использовать TestActorRef? Должен ли я использовать TestProbe() (я читал, что это для интеграционных тестов)

  2. Как я могу создавать макеты Talker? Правилен ли этот подход?

  3. Правильно ли вводить список говорящих в мой разговор Актер?

Я искал документацию, но я думаю, что она слишком старая, и я не уверен, что примеры кода все еще работают.

Спасибо за ваше время, ребята, и извините за этот нубский вопрос :=)


person SergiGP    schedule 18.11.2015    source источник


Ответы (2)


Это правда, что ситуация с тестированием в Акке, мягко говоря, немного запутанная.

В Akka обычно у вас есть два типа тестов, синхронные и асинхронные, которые некоторые люди называют «модульными» и «интеграционными» тестами.

  • «Модульные тесты» являются синхронными, вы напрямую тестируете метод получения, не требуя системы акторов и т. д. В вашем случае вы хотели бы смоделировать List[Talkers], вызвать свой метод receive и убедиться, что вызывается метод отправки. Вы можете напрямую создать экземпляр своего актера с помощью new Conversation(mockTalkers), в этом случае нет необходимости использовать TestActorRef. Для насмешек я рекомендую ScalaMock.

  • «Интеграционные тесты» являются асинхронными и обычно тестируют более одного участника, работающего вместе. Здесь вы наследуете TestKit, создаете экземпляры TestProbe, чтобы они действовали как говорящие, используете одного для отправки сообщения актеру Conversation и проверяете, что другой получает сообщение InterlocutorTyping.

Вам решать, какой вид теста вы считаете подходящим. Мое личное мнение заключается в том, что если у вас нет сложного внутреннего поведения в вашем акторе, вам следует пропустить синхронные тесты и сразу перейти к асинхронным («интеграционным») тестам, так как они охватывают более сложные пограничные случаи параллелизма, которые вы могли бы пропустить. Они также более «черные ящики» и поэтому менее чувствительны к изменениям по мере развития вашего дизайна.

Дополнительные сведения и примеры кода см. на странице документации.

person jazmit    schedule 18.11.2015
comment
Привет Джазмит, спасибо за ваш ответ. Конечно, я знаю, что мой тест глупый. И, конечно, я тоже проведу некоторое интеграционное тестирование, но я начну с простого :P Итак, по вашему мнению, для этого фиктивного теста я должен издеваться над своими говорящими, должен ли я использовать имитирующий фреймворк? Вы порекомендуете мне один? Есть ли способ сделать это напрямую с Teskit? Еще раз спасибо за ваше время :) - person SergiGP; 18.11.2015
comment
Я бы рекомендовал использовать ScalaMock, добавил ссылку на ответ. TestKit предназначен для асинхронных тестов, он вам не понадобится для простых тестов синхронизации. - person jazmit; 18.11.2015
comment
Возможно, вам повезет с имитацией ссылки на актора с помощью TestProbe, но рано или поздно вам понадобится имитирующая среда для других зависимостей. - person jazmit; 18.11.2015

Наконец я сделал это (есть еще несколько функций, чем в вопросе):

object Conversation {
  def props(conversationId: UUID)(out: ActorRef) = Props(new Conversation(conversationId))

  case class TalkerTyping(talkerId: TalkerId)
  case class TalkerStopTyping(talkerId: TalkerId)

  case class Join(talker: ActorRef)
  case class Leave(talker: ActorRef)
}

class Conversation(conversationId: UUID) extends Actor with ActorLogging {

  var talkers : ListBuffer[ActorRef] = ListBuffer.empty

  val senderFilter = { talker: ActorRef => talker != sender() }

  def receive = LoggingReceive {
    case Join =>
      talkers += sender()

    case Leave =>
      talkers -= sender()

    case TalkerTyping(talkerId) => // notify all talkers except sender that a talker is typing
      talkers filter senderFilter foreach { talker: ActorRef => talker ! InterlocutorTyping(talkerId) }

    case TalkerStopTyping(talkerId) => // notify all talkers except sender that a talker has stopped typing
      talkers filter senderFilter foreach { talker: ActorRef => talker ! InterlocutorStopTyping(talkerId) }
  }
}

И мой тест:

class ConversationSpec extends ChatUnitTestCase("ConversationSpec") {

  trait ConversationTestHelper {
    val talker = TestProbe()
    val anotherTalker = TestProbe()
    val conversationRef = TestActorRef[Conversation](Props(new Conversation(UUID.randomUUID())))
    val conversationActor = conversationRef.underlyingActor
  }

  "Conversation" should {
    "let user join it" in new ConversationTestHelper {
      conversationActor.talkers should have size 0

      conversationRef ! Join

      conversationActor.talkers should have size 1
      conversationActor.talkers should contain(testActor)
    }
    "let joining user leave it" in new ConversationTestHelper {

      conversationActor.talkers should have size 0
      conversationRef ! Join
      conversationActor.talkers should have size 1
      conversationActor.talkers should contain(testActor)

      conversationRef ! Leave
      conversationActor.talkers should have size 0
      conversationActor.talkers should not contain testActor
    }
    "notify interlocutors when a talker is typing" in new ConversationTestHelper {

      val talker1 = TestProbe()
      val talker2 = TestProbe()

      talker1.send(conversationRef, Join)
      talker2.send(conversationRef, Join)

      val talker2Id = TalkerIdStub.random

      talker2.send(conversationRef, TalkerTyping(talker2Id))

      talker1.expectMsgPF() {
        case InterlocutorTyping(talkerIdWhoTyped) if talkerIdWhoTyped == talker2Id => true
      }
      talker2.expectNoMsg()
}
"notify interlocutors when a talker stop typing" in new ConversationTestHelper {

  val talker1 = TestProbe()
  val talker2 = TestProbe()

  talker1.send(conversationRef, Join)
  talker2.send(conversationRef, Join)

  val talker2Id = TalkerIdStub.random

  talker2.send(conversationRef, TalkerStopTyping(talker2Id))

  talker1.expectMsgPF() {
    case InterlocutorStopTyping(talkerIdWhoStopTyping) if talkerIdWhoStopTyping == talker2Id => true
  }
  talker2.expectNoMsg()
    }
  }
}
person SergiGP    schedule 20.11.2015