就像 Actors 对假人进行单元测试一样
Akka Actors unit testing for dummies
我是 Akka 和 Scala 的新手,我来自非并发世界。可能我做错了很多事情,即使与问题无关,我也会感谢反馈。
我正在用 Akka 和 Scala 做一个简单的聊天应用程序。我从 "typing feature" 开始(bc 业务需求)...这是 whatsapp 或 tellegram "John is typing a message" 中的典型功能。
我使用两种演员类型对其进行了建模:Talkers 和 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 编写代码之前,我已经进行了如下测试:
- 我得到了我的对话演员
- 我嘲笑说话的人
- 我向我的演员发送消息打字
- 我希望谈话者能得到通知
我真的不知道这在 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
}
}
我应该使用 TestActorRef 吗?我应该使用 TestProbe() (我读到这是用于集成测试)
如何创建 Talker 模拟?这种做法正确吗?
向我的对话Actor注入List of Talkers是否正确?
我搜索了文档,但我认为有很多太旧了,我不确定代码示例是否仍然有效。
谢谢大家抽出时间,对于这个菜鸟问题深表歉意:=)
确实,Akka 的测试情况至少可以说有点混乱。
在 Akka 中通常有两种测试,同步和异步,有些人将其称为 'unit' 和 'integration' 测试。
'Unit tests' 是同步的,你直接测试接收方法而不需要演员系统等。在你的情况下,你会想要模拟 List[Talkers]
,调用你的receive
方法并验证是否调用了发送方法。您可以直接使用 new Conversation(mockTalkers)
实例化您的 actor,在这种情况下没有必要使用 TestActorRef
。对于模拟,我推荐 ScalaMock.
'Integration tests' 是异步的,通常测试多个 actor 一起工作。这是您继承 TestKit
,实例化 TestProbe
s 作为您的谈话者,使用一个向 Conversation
演员发送消息,并验证另一个收到 InterlocutorTyping
消息.
您认为适合哪种测试取决于您。我个人的意见是,除非你的 actor 有复杂的内部行为,否则你应该跳过同步测试并直接进行异步 ('integration') 测试,因为这将涵盖你可能会错过的更棘手的并发边缘情况。这些也更 'black-box',因此在您改进设计时对变化不那么敏感。
有关 doc page 的更多详细信息和代码示例。
最后我做到了(比问题多了一些功能):
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()
}
}
}
我是 Akka 和 Scala 的新手,我来自非并发世界。可能我做错了很多事情,即使与问题无关,我也会感谢反馈。
我正在用 Akka 和 Scala 做一个简单的聊天应用程序。我从 "typing feature" 开始(bc 业务需求)...这是 whatsapp 或 tellegram "John is typing a message" 中的典型功能。
我使用两种演员类型对其进行了建模:Talkers 和 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 编写代码之前,我已经进行了如下测试:
- 我得到了我的对话演员
- 我嘲笑说话的人
- 我向我的演员发送消息打字
- 我希望谈话者能得到通知
我真的不知道这在 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
}
}
我应该使用 TestActorRef 吗?我应该使用 TestProbe() (我读到这是用于集成测试)
如何创建 Talker 模拟?这种做法正确吗?
向我的对话Actor注入List of Talkers是否正确?
我搜索了文档,但我认为有很多太旧了,我不确定代码示例是否仍然有效。
谢谢大家抽出时间,对于这个菜鸟问题深表歉意:=)
确实,Akka 的测试情况至少可以说有点混乱。
在 Akka 中通常有两种测试,同步和异步,有些人将其称为 'unit' 和 'integration' 测试。
'Unit tests' 是同步的,你直接测试接收方法而不需要演员系统等。在你的情况下,你会想要模拟
List[Talkers]
,调用你的receive
方法并验证是否调用了发送方法。您可以直接使用new Conversation(mockTalkers)
实例化您的 actor,在这种情况下没有必要使用TestActorRef
。对于模拟,我推荐 ScalaMock.'Integration tests' 是异步的,通常测试多个 actor 一起工作。这是您继承
TestKit
,实例化TestProbe
s 作为您的谈话者,使用一个向Conversation
演员发送消息,并验证另一个收到InterlocutorTyping
消息.
您认为适合哪种测试取决于您。我个人的意见是,除非你的 actor 有复杂的内部行为,否则你应该跳过同步测试并直接进行异步 ('integration') 测试,因为这将涵盖你可能会错过的更棘手的并发边缘情况。这些也更 'black-box',因此在您改进设计时对变化不那么敏感。
有关 doc page 的更多详细信息和代码示例。
最后我做到了(比问题多了一些功能):
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()
}
}
}