I'm newbie in Akka and Scala and i come from a non-concurrent world. Probably i'm doing a lot of things wrong, i will appreciate feedback even it's not related to the question.
I'm doing a simple chat application with Akka and Scala. I started (bc business requirements) by "typing feature"... it's the typical feature in whatsapp or tellegram "John is typing a message".
I have modelled it using two actors types: Talkers and Conversation, and I want to unit test my Conversation actor. My Conversation actor looks like this:
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)}
}
}
I think, by now is very simple. So, before start coding in Scala and Akka I had tested this like:
I don't really know if it's the correct approach in Scala and Akka. My test (using scalatest) looks like this:
"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
}
}
Should I use TestActorRef? Should I use TestProbe() (I read that this is for integration tests)
How can I create Talker mocks? Is this approach correct?
It's correct to inject a List of Talkers to my conversation Actor?
I searched for documentation, but I think there are a lot too old and I'm not sure if the code examples are still functional.
Thank you for your time guys, and sorry about this noob question :=)
It's true that the testing situation in Akka is a little confusing to say the least.
In Akka generally you have two kinds of test, synchronous and asynchronous which some people term as 'unit' and 'integration' tests.
'Unit tests' are synchronous, you directly test the receive method without requiring an actor system, etc. In your case, you would want to mock the List[Talkers]
, call your receive
method and verify that the send method is called. You can directly instantiate your actor with new Conversation(mockTalkers)
, it's not necessary in this case to use TestActorRef
. For mocking, I recommend ScalaMock.
'Integration tests' are asynchronous, and generally test more than one actor working together. This is where you inherit TestKit
, instantiate TestProbe
s to act as your talkers, use one to send a message to the Conversation
actor, and verify that the other receives the InterlocutorTyping
message.
It's up to you which kind of test you think is appropriate. My personal opinion is that unless you have complicated internal bevaviour in your actor, you should skip the synchronous tests and go straight for the asynchronous ('integration') tests as this will cover more tricky concurrency edge cases that you might otherwise miss. These are also more 'black-box' and so less sensitive to change as you evolve your design.
More details and code samples on the doc page.
Finally I did this (there are some more functionality than in the question):
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) }
}
}
And my test:
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()
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With