I've been trying to learn how to test akka-typed Actors. I've been referencing various examples online. I am successful at running the sample code but my efforts to write simple unit tests fail.
Can someone point out what I'm doing wrong? My goal is to be able to write unit test that verify each behavior.
build.sbt
import Dependencies._
ThisBuild / scalaVersion := "2.13.7"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "com.example"
ThisBuild / organizationName := "example"
val akkaVersion = "2.6.18"
lazy val root = (project in file("."))
.settings(
name := "akkat",
libraryDependencies ++= Seq(
scalaTest % Test,
"com.typesafe.akka" %% "akka-actor-typed" % akkaVersion,
"com.typesafe.akka" %% "akka-actor-testkit-typed" % akkaVersion % Test,
"ch.qos.logback" % "logback-classic" % "1.2.3"
)
)
EmotionalFunctionalActor.scala
package example
import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.Behaviors
object EmotionalFunctionalActor {
trait SimpleThing
object EatChocolate extends SimpleThing
object WashDishes extends SimpleThing
object LearnAkka extends SimpleThing
final case class Value(happiness: Int) extends SimpleThing
final case class HowHappy(replyTo: ActorRef[SimpleThing]) extends SimpleThing
def apply(happiness: Int = 0): Behavior[SimpleThing] = Behaviors.receive { (context, message) =>
message match {
case EatChocolate =>
context.log.info(s"($happiness) eating chocolate")
EmotionalFunctionalActor(happiness + 1)
case WashDishes =>
context.log.info(s"($happiness) washing dishes, womp womp")
EmotionalFunctionalActor(happiness - 2)
case LearnAkka =>
context.log.info(s"($happiness) Learning Akka, yes!!")
EmotionalFunctionalActor(happiness + 100)
case HowHappy(replyTo) =>
replyTo ! Value(happiness)
Behaviors.same
case _ =>
context.log.warn("Received something i don't know")
Behaviors.same
}
}
}
EmoSpec.scala
package example
import akka.actor.testkit.typed.scaladsl.ActorTestKit
import akka.actor.typed.scaladsl.AskPattern.Askable
import akka.util.Timeout
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import scala.concurrent.duration.DurationInt
class EmoSpec extends AnyFlatSpec
with BeforeAndAfterAll
with Matchers {
val testKit = ActorTestKit()
override def afterAll(): Unit = testKit.shutdownTestKit()
"Happiness Leve" should "Increase by 1" in {
val emotionActor = testKit.spawn(EmotionalFunctionalActor())
val probe = testKit.createTestProbe[EmotionalFunctionalActor.SimpleThing]()
implicit val timeout: Timeout = 2.second
implicit val sched = testKit.scheduler
import EmotionalFunctionalActor._
emotionActor ! EatChocolate
probe.expectMessage(EatChocolate)
emotionActor ? HowHappy
probe.expectMessage(EmotionalFunctionalActor.Value(1))
val current = probe.expectMessageType[EmotionalFunctionalActor.Value]
current shouldBe 1
}
}
It's not really clear what problems you're encountering, so this answer is a bit of a shot in the dark with some observations.
You appear to be using the "command-then-query" pattern in testing this aspect of the behavior, which is OK (but see below for a different approach which I've found works really well). There are two basic ways you can approach this and your test looks like a bit of a mixture of the two in a way that probably is not working.
Regardless of approach, when sending the initial EatChocolate message to the actor:
emotionActor ! EatChocolate
That message is sent to actor, not to the probe, so probe.expectMessage won't succeed.
There are two flavors of ask in Akka Typed. There's a Future-based one for outside of an actor, where the asking machinery injects a special ActorRef to receive the reply and returns a Future which will be completed when the reply is received. You can arrange for that Future to send its result to the probe:
val testKit = ActorTestKit()
implicit val ec = testKit.system.executionContext
// after sending EatChocolate
val replyFut: Future[EmotionalFunctionalActor.SimpleThing] = emotionActor ? HowHappy
replyFut.foreach { reply =>
probe.ref ! reply
}
probe.expectMessage(EmotionalFunctionalActor.Value(1))
More succinctly, you can dispense with Askable, Futures, and an ExecutionContext and use probe.ref as the replyTo field in your HowHappy message:
emotionActor ! HowHappy(probe.ref`)
probe.expectMessage(EmotionalFunctionalActor.Value(1))
This is more succinct and will probably be less flaky (being less prone to timing issues) than the Future-based approach. Conversely, since the HowHappy message appears designed for use with the ask pattern, the Future-based approach may better fulfill a "test as documentation" purpose for describing how to interact with the actor.
If using the Future-based approach with ScalaTest, it might be useful to have your suite extend akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit: this will provide some of the boilerplate and also mix in ScalaTest's ScalaFutures trait, which would let you write something like
val replyFut: Future[EmotionalFunctionalActor.SimpleThing] = emotionActor ? HowHappy
assert(replyFut.futureValue == EmotionalFunctionalActor.Value(1))
I tend to prefer the BehaviorTestKit, especially for cases where I'm not testing how two actors interact (this can still be done, it's just a bit laborious with the BehaviorTestKit). This has the advantage of not having any timing at all and generally has less overhead for running tests.
val testKit = BehaviorTestKit(EmotionalFunctionalActor())
val testInbox = TestInbox[EmotionalFunctionalActor.SimpleThing]()
testKit.run(HowHappy(testInbox.ref))
testInbox.expectMessage(EmotionalFunctionalActor.Value(1))
As a side note, when designing a request-response protocol, it's generally a good idea to restrict the response type to the responses that might actually be sent, e.g.:
final case class HowHappy(replyTo: ActorRef[Value]) extends SimpleThing
This ensures that the actor can't reply with anything but a Value message and means that the asker doesn't have to handle any other type of message. If there's a couple of different message types it could respond with, it might be worth having a trait which is only extended (or mixed in) by those responses:
trait HappinessReply
final case class Value(happiness: Int) extends HappinessReply
final case class HowHappy(replyTo: ActorRef[HappinessReply]) extends SimpleThing
Further, the reply often won't make sense as a message received by the sending actor (as indicated in this case by it being handled by the "Received something I don't know" case). In this situation, Value shouldn't extend SimpleThing: it might even just be a bare case class and not extend anything.
Related
I have a Route defined using akka-http that uses an actor inside to send messages.
My route looks like this:
path("entity") {
post {
entity(as[Enrtity]) {
entity =>
val result: Future[Message] = mainActor.ask {
ref: akka.actor.typed.ActorRef[Message] =>
Message(
entity = entity,
replyRef = ref
)
}
complete("OK")
}
}
}
My test spec:
class APITest
extends ScalaTestWithActorTestKit(ManualTime.config)
with ScalatestRouteTest
with AnyWordSpecLike {
val manualTime: ManualTime = ManualTime()
// my tests here ...
}
Compiling the test fails since there are conflicting actor systems:
class APITest inherits conflicting members:
[error] implicit def system: akka.actor.typed.ActorSystem[Nothing] (defined in class ActorTestKitBase) and
[error] implicit val system: akka.actor.ActorSystem (defined in trait RouteTest)
Overriding the actor system doesn't help either since the inherited actor systems are of both typed and untyped ones.
How can I resolve this easily?
Update:
This is related to conflicting inherited members with different types, but we might be able to solve what I want to achieve in this context differently.
I spent a little time here while moving over to typed. For anyone still looking, there's a nice hint at https://developer.lightbend.com/guides/akka-http-quickstart-scala/testing-routes.html
// the Akka HTTP route testkit does not yet support a typed actor system (https://github.com/akka/akka-http/issues/2036)
// so we have to adapt for now
lazy val testKit = ActorTestKit()
implicit def typedSystem = testKit.system
override def createActorSystem(): akka.actor.ActorSystem =
testKit.system.classicSystem
Looking at the first comment at https://github.com/akka/akka-http/issues/2036 it notes
Perhaps just a docs addition to show that you don't need to use the ActorTestKit from Akka Typed and can just use TestProbes e.g. https://gist.github.com/chbatey/964b80adc2cd124fa4bf4624927b5be0
or val probe = TestProbe[Ping]() > val probe = testKit.createTestProbe[Ping]()
I have been going through documentations on ScalaTest but able to figure out what type of approach i should take for testing the app.
Code is divided amoung controller and service.
eg. Controller Code example
#Singleton
class Controller1 #Inject()(service1: ServiceClass1, authAction : AuthAction)
extends InjectedController {
//returns a list[]
def getSomeValue() = authAction {
val res = service1.getValue1()
val json = Json.toJson(res)
Ok(json)
}
}
Service Code Example -:
def getValue1() = {
implicit val graph = db.g
val infos = graph.V.hasLabel[someModel].toList()
infos.map(vertex => {
val someModel = vertex.toCC[someModel]
val item = info(someId =
someModel.someId.getOrElse("").toString,
category = SomeModel.category,
description = someModel.description)
item
})
}
I am very new to Testing and Scala both, I also understand the code but not able to understand where to begin.
This is just a sample code which is very similar.
It seems like what you're looking for is a way to mock service1.getValue1() in your Controller1.
Scalatest supports a couple different ways to do this: http://www.scalatest.org/user_guide/testing_with_mock_objects
In your case, to test def getSomeValue(); you'd need to define a mock and set the right expectations so that when called from the test, the mock returns the expected responses.
If you'd like to use scala mock, you'd need to add it as a dependency in your sbt build config. You can do that by adding this dependency to your tests:
"org.scalamock" %% "scalamock" % "4.4.0" % Test
And then, your test could be something like this:
import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class Controller1Spec extends AnyFlatSpec with Matchers with MockFactory {
"Controller1" should "respond with a valid response" in {
val mockService = mock[ServiceClass1]
(mockService.getValue1 _).when().returning("Some Response").once()
val mockAuthAction = mock[AuthAction] //assuming you've got an action called AuthAction
//you'd need to mock this one too, in order for it to work
(mockAuthAction.invokeBlock _) expects(_) onCall((r,b) => b(r))
new Controller1(mockService, mockAuthAction) shouldBe Ok("Some Response")
}
}
There's a number of posts on mocking for Scala which you should be able to find, like this one here: https://scalamock.org/user-guide/advanced_topics/
I want to test that an actor A send a message to an actor B after have received a message.
I'm using Play! 2.5 and I use the factories since I need to inject some of my classes and things like wSClient inside the actors.
The Actor A looks like:
object ActorA {
trait Factory {
def apply(ec: ExecutionContext, actorBRef: ActorRef): Actor
}
}
class ActorA #Inject()(implicit val ec: ExecutionContext,
#Named("actor-b") actorBRef: ActorRef)
extends Actor with ActorLogging with InjectedActorSupport {
override def receive: Receive = {
case i: Long =>
log info s"received $i"
actorBRef ! (i+1)
}
And the actor B is even more simple:
object ActorB {
trait Factory {
def apply(): Actor
}
}
class ActorB extends Actor with ActorLogging {
override def receive: Receive = {
case _ =>
log error "B received an unhandled message"
}
}
But my test doesn't pass, it is said that the expected message doesn't arrive, I get a Timeout in the test (but it is well logged by the actor B) so the problem comes from the test (and probably the Probe).
Here is the test:
val actorBProbe = TestProbe()
lazy val appBuilder = new GuiceApplicationBuilder().in(Mode.Test)
lazy val injector = appBuilder.injector()
lazy val factory = injector.instanceOf[ActorA.Factory]
lazy val ec = scala.concurrent.ExecutionContext.Implicits.global
lazy val factoryProps = Props(factory(ec, actorBProbe.ref))
val ActorARef = TestActorRef[ActorA](factoryProps)
"Actor B" must {
"received a message from actor A" in {
ActorARef ! 5L
actorBProbe.expectMsg(6L)
}
}
I also created a minimum Play! application with the code above available here.
In your test, actorBProbe is not the ActorB ref passed to ActorA constructor (of ref ActorARef). What really happens is that Guice creates a different ActorB (named actor-b), and passes its ref to ActorA (of ref ActorARef) constructor.
The test ends up with ActorB actor-b receiving 6L (as evident in log). While actorBProbe receives nothing.
The confusion really comes from mixing Guice lifecyle with Actors. In my experience, it creates more pains than I can bear.
To prove, simply print hash code of ActorRef's, you'll see they are different. Illustrated as followings:
val actorBProbe = TestProbe()
println("actorBProbe with ref hash: " + actorBProbe.ref.hashCode())
And,
class ActorA ... {
override def preStart =
log error "preStart actorBRef: " + actorBRef.hashCode()
// ...
}
In fact, even ec inside ActorA is not the same ec in the test code.
The following is a way to "force" the test to pass and at the same time prove that actorBProbe wasn't really being used by ActorB.
In stead of relying on Guice to "wire in" ActorB, we tell Guice to leave it alone by replacing #Named("actor-b") with #Assisted, like this,
import ...
import com.google.inject.assistedinject.Assisted
class ActorA #Inject()(...
/*#Named("actor-b")*/ #Assisted actorBRef: ActorRef)
...
Re-run the test, it'll pass. But this is probably not what you wanted to begin with.
I am trying to understand the akka-Testkit", and hope it is ok to ask about it.
I found some tutorials and blogs that either access a state- or a lastMsg- attribute on the underlyingActor on the TestActorRef. However, a TestActorRef from the the "akka-testkit_2.11" % "2.4.10" does not have these attributes. I looked at the example on the akka website, and maybe I am missing something, but they show testing of among other an echo actor, but not with any simple actor implementations.
So, could someone help me understand how to test a worker that will respond with the same number if n % 3 == 0 (which is the case in the example). I would prefer not to use a future and the ask pattern if possible, and would like to make a test on the response that the actor will give (from that actors perspective by accessing its state or something similar).
class ProjectEulerScalaTestAkka extends TestKit(ActorSystem("testing")) with WordSpecLike with MustMatchers {
"A simple actor" must {
val actorRef = TestActorRef[Worker]
"receive messages" in {
actorRef ! 3
actorRef.underlyingActor.state//must not equal("world")
}
}
}
related:
How do I test an Akka actor that sends a message to another actor?
For now I am using a synchronized testing approach;
import akka.actor.ActorSystem
import akka.testkit.{TestActorRef, TestKit}
import org.scalatest.Matchers
import org.scalatest.WordSpecLike
import akka.pattern.ask
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.Success
class ProjectEulerScalaTestAkka extends TestKit(ActorSystem("testing")) with WordSpecLike with Matchers {
implicit val time = akka.util.Timeout(100 seconds)
"A simple actor" must {
val actorRef = TestActorRef[Worker]
"receive messages" in {
val f = (actorRef ? 3).asInstanceOf[Future[Int]]
val reply = f.value.get
reply should equal (Success(3))
}
}
}
What I did was mock the interface of the Actor that I was sending the message to, capturing the message and sending back a success message to the testActor reference.
success object with the captured message payload
case class SuccessWith(capturedMessage:Any = null)
a mock actor that you send your message to which, in turn, returns some value to the test actor
case class MockActor(requester: ActorRef) extends Actor {
override def receive: Receive = {
case i: Int => {
requester ! i
}
}
}
set up the actor you're wanting to unit test
val actorRef = system.actorOf(Props(new YourActor(args)))
and then your test
"A simple actor" must {
"receive messages" in {
val f = actorRef ! 3
expectMsg(Success(3))
}
}
}
New to play,scala, and reactivemongo and the documentation is not very noob friendly.
I see the Bulk Insert section at See Bulk Insert
but I don't know why they aren't showing it contained in a method?
I am expecting a request with JSON data containing multiple objects in it. How do I set up a bulk insert that handles multiple inserts with errors that can be returned.
For example by single insert method is as follows:
def createFromJson = Action(parse.json) {
request =>
try {
val person = request.body.validate[Person].get
val mongoResult = Await.result(collection.insert(person),Duration.apply(20,"seconds"))
if(mongoResult.hasErrors) throw new Exception(mongoResult.errmsg.getOrElse("something unknown"))
Created(Json.toJson(person))
}
catch {
case e: Exception => BadRequest(e.getMessage)
}
}
Here is a full example how you can do it:
class ExampleController #Inject()(database: DefaultDB) extends Controller {
case class Person(firstName: String, lastName: String)
val personCollection: BSONCollection = database.collection("persons")
implicit val PersonJsonReader: Reads[Person] = Json.reads[Person]
implicit val PersonSeqJsonReader: Reads[Seq[Person]] = Reads.seq(PersonJsonReader)
implicit val PersonJsonWriter: Writes[Person] = Json.writes[Person]
implicit val PersonSeqJsonWriter: Writes[Seq[Person]] = Writes.seq(PersonJsonWriter)
implicit val PersonBsonWriter = Macros.writer[Person]
def insertMultiple = Action.async(parse.json) { implicit request =>
val validationResult: JsResult[Seq[Person]] = request.body.validate[Seq[Person]]
validationResult.fold(
invalidValidationResult => Future.successful(BadRequest),
// [1]
validValidationResult => {
val bulkDocs = validValidationResult.
map(implicitly[personCollection.ImplicitlyDocumentProducer](_))
personCollection.bulkInsert(ordered = true)(bulkDocs: _*).map {
case insertResult if insertResult.ok =>
Created(Json.toJson(validationResult.get))
case insertResult =>
InternalServerError
}
}
)
}
}
The meat of it all sits in the lines after [1]. validValidationResult is a variable of type Seq[Person] and contains valid data at this point. Thats what we want to insert into the database.
To do that we need to prepare the documents by mapping each document through the ImplicitlyDocumentProducer of your target collection (here personCollection). Thats leaves you with bulkDocs of type Seq[personCollection.ImplicitlyDocumentProducer]. You can just use bulkInsert() with that:
personCollection.bulkInsert(ordered = true)(bulkDocs: _*)
We use _* here to splat the Seq since bulkInsert() expects varargs and not a Seq. See this thread for more info about it. And thats basically it already.
The remaing code is handling play results and validating the received request body to make sure it contains valid data.
Here are a few general tips to work with play/reactivemongo/scala/futures:
Avoid Await.result. You basically never need it in production code. The idea behind futures is to perform non-blocking operations. Making them blocking again with Await.result defeats the purpose. It can be useful for debugging or test code, but even then there are usually better ways to go about things. Scala futures (unlike java ones) are very powerful and you can do a lot with them, see e.g. flatMap/map/filter/foreach/.. in the Future scaladoc. The above code for instance makes use of exactly that. It uses Action.async instead of Action at the controller method. This means it has to return a Future[Result] instead of a Result. Which is great because ReactiveMongo returns a bunch of Futures for all operations. So all you have to do is execute bulkInsert, which returns a Future and use map() to map the returned Future[MultiBulkWriteResult] to a Future[Result]. This results in no blocking and play can work with the returned future just fine.
Of course the above example can be improved a bit, I tried to keep it simple.
For instance you should return proper error messages when returning BadRequest (request body validation failed) or InternalServerError (database write failed). You can get more info about the errors from invalidValidationResult and insertResult. And you could use Formats instead of that many Reads/Writes (and also use them for ReactiveMongo). Check the play json documentation as well as the reactive mongo doc for more info on that.
Although the previous answer is correct.
We can reduce the boilerplate using JSONCollection
package controllers
import javax.inject._
import play.api.libs.json._
import play.api.mvc._
import play.modules.reactivemongo._
import reactivemongo.play.json.collection.{JSONCollection, _}
import utils.Errors
import scala.concurrent.{ExecutionContext, Future}
case class Person(name: String, age: Int)
object Person {
implicit val formatter = Json.format[Person]
}
#Singleton
class PersonBulkController #Inject()(val reactiveMongoApi: ReactiveMongoApi)(implicit exec: ExecutionContext) extends Controller with MongoController with ReactiveMongoComponents {
val persons: JSONCollection = db.collection[JSONCollection]("person")
def createBulkFromJson = Action.async(parse.json) { request =>
Json.fromJson[Seq[Person]](request.body) match {
case JsSuccess(newPersons, _) =>
val documents = newPersons.map(implicitly[persons.ImplicitlyDocumentProducer](_))
persons.bulkInsert(ordered = true)(documents: _*).map{ multiResult =>
Created(s"Created ${multiResult.n} persons")
}
case JsError(errors) =>
Future.successful(BadRequest("Could not build an array of persons from the json provided. " + errors))
}
}
}
In build.sbt
libraryDependencies ++= Seq(
"org.reactivemongo" %% "play2-reactivemongo" % "0.11.12"
)
Tested with play 2.5.1 although it should compile in previous versions of play.
FYI, as previous answers said, there are two ways to manipulate JSON data: use ReactiveMongo module + Play JSON library, or use ReactiveMongo's BSON library.
The documentation of ReactiveMongo module for Play Framework is available online. You can find code examples there.