如何使用ScalaTest、ZIO和Akka测试持久性演员

huangapple go评论93阅读模式
英文:

How to test persistent actors with ScalaTest, ZIO and Akka

问题

I have translated the code portion as requested:

  1. val actorRef = testKit.spawn(/* Real behavior */)
  2. "AkkaZioAndScalatest" should {
  3. "collaborate to write readable tests" in {
  4. val subject = new UseCaseWithAkkaAndZio(actorRef)
  5. val result = zio.Runtime.default.unsafeRun(for {
  6. _ <- subject.apply("Hello")
  7. msg <- actorRef.askIO(GetMessage(_))
  8. } yield {
  9. msg
  10. })
  11. result should be ("Hello")
  12. }
  13. }
  1. class UseCaseWithAkkaAndZio(actorRef: ActorRef) {
  2. def apply(msg:String):UIO[Unit] = {
  3. // askIO is an implicit method that `ask` and convert the Future to UIO
  4. actorRef.askIO(SetMessage(msg))
  5. }
  6. }

If you have further questions or need more translations, please let me know.

英文:

I have one "use case" that I would like to test. This one returns a zio.IO resulting from a call to an Actor (scala.concurrent.Future). And, I would like to use ScalaTest's AnyWordSpec.

  1. val actorRef = testKit.spawn(/* Real behavior */)
  2. &quot;AkkaZioAndScalatest&quot; should {
  3. &quot;collaborate to write readable tests&quot; in {
  4. val subject = new UseCaseWithAkkaAndZio(actorRef)
  5. val result = zio.Runtime.default.unsafeRun(for {
  6. _ &lt;- subject.apply(&quot;Hello&quot;)
  7. msg &lt;- actorRef.askIO(GetMessage(_))
  8. } yield {
  9. msg
  10. })
  11. result should be (&quot;Hello&quot;)
  12. }
  13. }
  1. class UseCaseWithAkkaAndZio(actorRef: ActorRef) {
  2. def apply(msg:String):UIO[Unit] = {
  3. // askIO is an implicit method that `ask` and convert the Future to UIO
  4. actorRef.askIO(SetMessage(msg))
  5. }
  6. }

I have turned around this test for a while without being able to have something working.

At this time, with the code sample above, I get this error:
> [info] zio.FiberFailure: Fiber failed.<br />
> [info] An unchecked error was produced.<br />
> [info] java.util.concurrent.TimeoutException: Recipient[Actor[akka://UseCaseWithAkkaAndZioTest/user/$a#2047466881]] had already been terminated.<br />
> [info] at akka.actor.typed.scaladsl.AskPattern$PromiseRef.<init>(AskPattern.scala:140) <br />
> [info] ...

Can someone help with this idea? I have to admit that I feel a bit lost here.

Thanks


EDIT To be complete, and as suggested by @Gaston here is the reproducible example.

Please note that it works when using a Behavior but fails when using a EventSourcedBehavior. (Thanks Gaston, I have discovered that when writing the example).

  1. package org
  2. import akka.actor.typed.ActorRef
  3. import akka.cluster.sharding.typed.scaladsl.EntityRef
  4. import akka.util.Timeout
  5. import zio._
  6. import scala.concurrent.duration.DurationInt
  7. package object example {
  8. implicit final class EntityRefOps[-M](actorRef: EntityRef[M]) {
  9. private val timeout: Timeout = Timeout(5.seconds)
  10. def askIO[E, A](p: ActorRef[Either[E, A]] =&gt; M): ZIO[Any, E, A] = {
  11. IO.fromFuture(_ =&gt; actorRef.ask(p)(timeout))
  12. .orDie
  13. .flatMap(e =&gt; IO.fromEither(e))
  14. }
  15. }
  16. }
  1. package org.example
  2. import akka.actor.typed.scaladsl.Behaviors
  3. import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
  4. import akka.cluster.sharding.typed.scaladsl.EntityTypeKey
  5. import akka.persistence.typed.PersistenceId
  6. import akka.persistence.typed.scaladsl.{Effect, EventSourcedBehavior, RetentionCriteria}
  7. import scala.concurrent.duration.DurationInt
  8. object GreeterPersistentActor {
  9. sealed trait Command
  10. case class SetMessage(message: String, replyTo: ActorRef[Either[Throwable, Unit]]) extends Command
  11. case class GetMessage(replyTo: ActorRef[Either[Throwable, String]]) extends Command
  12. val TypeKey: EntityTypeKey[Command] = EntityTypeKey[Command](getClass.getSimpleName)
  13. type ReplyEffect = akka.persistence.typed.scaladsl.ReplyEffect[Event, State]
  14. trait Event
  15. case class MessageSet(message: String) extends Event
  16. case class State(message: Option[String]) {
  17. def handle(cmd:Command):ReplyEffect = cmd match {
  18. case c: SetMessage =&gt;
  19. val event = MessageSet(c.message)
  20. Effect.persist(event).thenReply(c.replyTo)(_ =&gt; Right(():Unit))
  21. case c: GetMessage =&gt;
  22. message match {
  23. case Some(value) =&gt;
  24. Effect.reply(c.replyTo)(Right(value))
  25. case None =&gt;
  26. Effect.reply(c.replyTo)(Left(new Exception(&quot;No message set&quot;)))
  27. }
  28. }
  29. def apply(evt:Event):State = evt match {
  30. case e:MessageSet =&gt; State(Some(e.message))
  31. }
  32. }
  33. def apply(persistenceId: PersistenceId, message: Option[String]): Behavior[Command] = EventSourcedBehavior
  34. .withEnforcedReplies[Command, Event, State](
  35. persistenceId,
  36. State(message),
  37. (state, cmd) =&gt; state.handle(cmd),
  38. (state, evt) =&gt; state.apply(evt)
  39. )
  40. .withTagger(_ =&gt; Set(&quot;Sample&quot;))
  41. .withRetention(
  42. RetentionCriteria.snapshotEvery(100, 2)
  43. )
  44. .onPersistFailure(
  45. SupervisorStrategy.restartWithBackoff(200.millis, 5.seconds, 0.2d)
  46. )
  47. }
  1. package org.example
  2. import akka.cluster.sharding.typed.scaladsl.EntityRef
  3. import zio._
  4. class UseCaseWithAkkaAndZio(entityRefFor: String=&gt;EntityRef[GreeterPersistentActor.Command]) {
  5. def apply(entityId: String, msg: String): IO[Throwable, Unit] = {
  6. entityRefFor(entityId).askIO(GreeterPersistentActor.SetMessage(msg, _))
  7. }
  8. }
  1. package org.example
  2. import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
  3. import akka.cluster.sharding.typed.scaladsl.{EntityRef, EntityTypeKey}
  4. import akka.cluster.sharding.typed.testkit.scaladsl.TestEntityRef
  5. import akka.persistence.typed.PersistenceId
  6. import org.scalatest.wordspec.AnyWordSpecLike
  7. class UseCaseWithAkkaAndZioSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike {
  8. &quot;AkkaZioAndScalatest&quot; should {
  9. &quot;collaborate to write readable tests&quot; in {
  10. val entityId = &quot;Sample&quot;
  11. val actorRef = testKit.spawn(GreeterPersistentActor.apply(PersistenceId(&quot;Sample&quot;, entityId), None))
  12. val entityRefFor:String=&gt;EntityRef[GreeterPersistentActor.Command] = TestEntityRef(
  13. EntityTypeKey[GreeterPersistentActor.Command](&quot;Greeter&quot;),
  14. _,
  15. actorRef
  16. )
  17. val subject = new UseCaseWithAkkaAndZio(entityRefFor)
  18. val result = zio.Runtime.default.unsafeRun(for {
  19. _ &lt;- subject.apply(entityId, &quot;Hello&quot;)
  20. msg &lt;- entityRefFor(entityId).askIO(GreeterPersistentActor.GetMessage(_))
  21. } yield {
  22. println(s&quot;Answer is $msg&quot;)
  23. msg
  24. })
  25. result should be(&quot;Hello&quot;)
  26. }
  27. }
  28. }
  1. // build.sbt
  2. ThisBuild / scalaVersion := &quot;2.13.11&quot;
  3. lazy val root = (project in file(&quot;.&quot;))
  4. .settings(
  5. name := &quot;sample&quot;,
  6. libraryDependencies += &quot;com.typesafe.akka&quot; %% &quot;akka-actor-typed&quot; % &quot;2.8.2&quot;,
  7. libraryDependencies += &quot;com.typesafe.akka&quot; %% &quot;akka-persistence-typed&quot; % &quot;2.8.2&quot;,
  8. libraryDependencies += &quot;com.typesafe.akka&quot; %% &quot;akka-cluster-sharding-typed&quot; % &quot;2.8.2&quot;,
  9. libraryDependencies += &quot;com.typesafe.akka&quot; %% &quot;akka-actor-testkit-typed&quot; % &quot;2.8.2&quot; % Test,
  10. libraryDependencies += &quot;dev.zio&quot; %% &quot;zio&quot; % &quot;1.0.18&quot;,
  11. libraryDependencies += &quot;org.scalatest&quot; %% &quot;scalatest&quot; % &quot;3.2.16&quot; % Test,
  12. )

答案1

得分: 0

以下是翻译好的内容:

  1. There is a testkit for testing persistent actors.

  2. EventSourcedBehaviorTestKit must be mixed into the suite test.

  3. Another question that was part of the original one was to remove the zio.Runtime.unsafeRun.

  4. My actual solution is to create a few Matcher[ZIO[...] that allows me to write test like:

  5. val effect: IO[Throwable, String] = ???
    effect should completeMatching {
    case str: String => // Yes. This is useless, but for the example
    }

  6. trait ZioMatchers {

  7. def completeMatching(matcher: PartialFunction[Any, Any]): Matcher[IO[Any, Any]] = (effect: IO[Any, Any]) => {
    val (complete, matches, result) = zio.Runtime.default
    .unsafeRun(
    effect
    .flatMap {
    case res if matcher.isDefinedAt(res) => ZIO.succeed((true, true, res))
    case other => ZIO.succeed(true, false, other)
    }
    .catchAll(err => ZIO.succeed(false, false, err))
    )
    ...
    }

    ...

英文:

There is a testkit for testing persistent actors.

EventSourcedBehaviorTestKit must be mixed into the suite test

  1. class UseCaseWithAkkaAndZioSpec extends ScalaTestWithActorTestKit(EventSourcedBehaviorTestKit.config) with AnyWordSpecLike {
  2. // the tests
  3. }

EDIT:

Another question that was part of the original one was to remove the zio.Runtime.unsafeRun.

My actual solution is to create a few Matcher[ZIO[...] that allows me to write test like :

  1. val effect:IO[Throwable, String] = ???
  2. effect should completeMatching {
  3. case str:String =&gt; // Yes. This is useless, but for the example
  4. }
  5. trait ZioMatchers {
  6. def completeMatching(matcher: PartialFunction[Any, Any]): Matcher[IO[Any, Any]] = (effect: IO[Any, Any]) =&gt; {
  7. val (complete, matches, result) = zio.Runtime.default
  8. .unsafeRun(
  9. effect
  10. .flatMap {
  11. case res if matcher.isDefinedAt(res) =&gt; ZIO.succeed((true, true, res))
  12. case other =&gt; ZIO.succeed(true, false, other)
  13. }
  14. .catchAll(err =&gt; ZIO.succeed(false, false, err))
  15. )
  16. ...
  17. }
  18. ...
  19. }

huangapple
  • 本文由 发表于 2023年6月27日 18:10:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/76563810.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定