5

私は Akka と Scala の初心者であり、非並行世界の出身です。おそらく私は多くのことを間違っています。質問に関係なくても、フィードバックをいただければ幸いです。

Akka と Scala で簡単なチャット アプリケーションを作成しています。私は(bcビジネス要件)「入力機能」から始めました...これは、「ジョンがメッセージを入力している」というwhatsappまたは電報の典型的な機能です。

Talkers と Conversation の 2 つのアクター タイプを使用してモデル化し、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 でコーディングを開始する前に、次のようにテストしました。

  • 会話アクターを取得します
  • 私は話者をからかう
  • アクターにメッセージを送信します
  • 話者に通知する必要があると思います

それが 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. トーカーのリストを会話アクターに挿入するのは正しいですか?

ドキュメントを検索しましたが、古すぎると思います。コード例がまだ機能しているかどうかはわかりません。

お時間をいただきありがとうございます。初心者の質問で申し訳ありません:=)

4

2 に答える 2

3

最後に私はこれを行いました(質問よりもいくつかの機能があります):

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()
    }
  }
}
于 2015-11-20T08:32:48.300 に答える
3

確かに、Akka でのテスト状況は控えめに言っても少し混乱しています。

Akka では一般に、同期と非同期の 2 種類のテストがあり、これらを「ユニット」テストと「統合」テストと呼ぶ人もいます。

  • 「単体テスト」は同期的であり、アクター システムなどを必要とせずに receive メソッドを直接テストします。この場合、 をモックしList[Talkers]、メソッドを呼び出しreceiveて、send メソッドが呼び出されることを確認します。アクターを で直接インスタンス化できます。new Conversation(mockTalkers)この場合、 を使用する必要はありませんTestActorRefモックにはScalaMockをお勧めします。

  • 「統合テスト」は非同期であり、通常、複数のアクターが連携して動作することをテストします。ここで を継承TestKitし、 をインスタンス化TestProbeしてトーカーとして機能させ、一方を使用してConversationアクターにメッセージを送信し、もう一方がInterlocutorTypingメッセージを受信することを確認します。

どの種類のテストが適切であると考えるかは、あなた次第です。私の個人的な意見では、アクターの内部動作が複雑でない限り、同期テストをスキップして、非同期 (「統合」) テストに直行する必要があります。これにより、他の方法では見逃す可能性のあるトリッキーな並行性のエッジ ケースがカバーされるからです。これらはまた、より「ブラックボックス」であるため、デザインを進化させても変化の影響を受けにくくなります。

詳細とコード サンプルについては、ドキュメント ページを参照してください。

于 2015-11-18T17:01:33.643 に答える