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

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

How to test persistent actors with ScalaTest, ZIO and Akka

问题

I have translated the code portion as requested:

  val actorRef = testKit.spawn(/* Real behavior */)

  "AkkaZioAndScalatest" should {

    "collaborate to write readable tests" in {
      val subject = new UseCaseWithAkkaAndZio(actorRef)
      val result =  zio.Runtime.default.unsafeRun(for {
        _ <- subject.apply("Hello")
        msg <- actorRef.askIO(GetMessage(_))
      } yield {
        msg
      })

      result should be ("Hello")
    }
  }
class UseCaseWithAkkaAndZio(actorRef: ActorRef) {
  def apply(msg:String):UIO[Unit] = {
    // askIO is an implicit method that `ask` and convert the Future to UIO
    actorRef.askIO(SetMessage(msg))
  }
}

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.

  val actorRef = testKit.spawn(/* Real behavior */)

  &quot;AkkaZioAndScalatest&quot; should {

    &quot;collaborate to write readable tests&quot; in {
      val subject = new UseCaseWithAkkaAndZio(actorRef)
      val result =  zio.Runtime.default.unsafeRun(for {
        _ &lt;- subject.apply(&quot;Hello&quot;)
        msg &lt;- actorRef.askIO(GetMessage(_))
      } yield {
        msg
      })

      result should be (&quot;Hello&quot;)
    }
  }
class UseCaseWithAkkaAndZio(actorRef: ActorRef) {
  def apply(msg:String):UIO[Unit] = {
    // askIO is an implicit method that `ask` and convert the Future to UIO
    actorRef.askIO(SetMessage(msg))
  }
}

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).

package org

import akka.actor.typed.ActorRef
import akka.cluster.sharding.typed.scaladsl.EntityRef
import akka.util.Timeout
import zio._

import scala.concurrent.duration.DurationInt

package object example {

  implicit final class EntityRefOps[-M](actorRef: EntityRef[M]) {

    private val timeout: Timeout = Timeout(5.seconds)

    def askIO[E, A](p: ActorRef[Either[E, A]] =&gt; M): ZIO[Any, E, A] = {
      IO.fromFuture(_ =&gt; actorRef.ask(p)(timeout))
        .orDie
        .flatMap(e =&gt; IO.fromEither(e))
    }

  }

}
package org.example

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import akka.cluster.sharding.typed.scaladsl.EntityTypeKey
import akka.persistence.typed.PersistenceId
import akka.persistence.typed.scaladsl.{Effect, EventSourcedBehavior, RetentionCriteria}

import scala.concurrent.duration.DurationInt

object GreeterPersistentActor {

  sealed trait Command
  case class SetMessage(message: String, replyTo: ActorRef[Either[Throwable, Unit]]) extends Command
  case class GetMessage(replyTo: ActorRef[Either[Throwable, String]]) extends Command

  val TypeKey: EntityTypeKey[Command] = EntityTypeKey[Command](getClass.getSimpleName)

  type ReplyEffect = akka.persistence.typed.scaladsl.ReplyEffect[Event, State]

  trait Event
  case class MessageSet(message: String) extends Event

  case class State(message: Option[String]) {
    def handle(cmd:Command):ReplyEffect = cmd match {
      case c: SetMessage =&gt;
        val event = MessageSet(c.message)
        Effect.persist(event).thenReply(c.replyTo)(_ =&gt; Right(():Unit))
      case c: GetMessage =&gt;
        message match {
          case Some(value) =&gt;
            Effect.reply(c.replyTo)(Right(value))
          case None =&gt;
            Effect.reply(c.replyTo)(Left(new Exception(&quot;No message set&quot;)))
        }
    }

    def apply(evt:Event):State = evt match {
      case e:MessageSet =&gt; State(Some(e.message))
    }
  }

  def apply(persistenceId: PersistenceId, message: Option[String]): Behavior[Command] = EventSourcedBehavior
    .withEnforcedReplies[Command, Event, State](
      persistenceId,
      State(message),
      (state, cmd) =&gt; state.handle(cmd),
      (state, evt) =&gt; state.apply(evt)
    )
    .withTagger(_ =&gt; Set(&quot;Sample&quot;))
    .withRetention(
      RetentionCriteria.snapshotEvery(100, 2)
    )
    .onPersistFailure(
      SupervisorStrategy.restartWithBackoff(200.millis, 5.seconds, 0.2d)
    )

}
package org.example

import akka.cluster.sharding.typed.scaladsl.EntityRef
import zio._

class UseCaseWithAkkaAndZio(entityRefFor: String=&gt;EntityRef[GreeterPersistentActor.Command]) {
  def apply(entityId: String, msg: String): IO[Throwable, Unit] = {
    entityRefFor(entityId).askIO(GreeterPersistentActor.SetMessage(msg, _))
  }
}
package org.example

import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import akka.cluster.sharding.typed.scaladsl.{EntityRef, EntityTypeKey}
import akka.cluster.sharding.typed.testkit.scaladsl.TestEntityRef
import akka.persistence.typed.PersistenceId
import org.scalatest.wordspec.AnyWordSpecLike

class UseCaseWithAkkaAndZioSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike {


  &quot;AkkaZioAndScalatest&quot; should {
    &quot;collaborate to write readable tests&quot; in {
      val entityId = &quot;Sample&quot;
      val actorRef = testKit.spawn(GreeterPersistentActor.apply(PersistenceId(&quot;Sample&quot;, entityId), None))
      val entityRefFor:String=&gt;EntityRef[GreeterPersistentActor.Command] = TestEntityRef(
        EntityTypeKey[GreeterPersistentActor.Command](&quot;Greeter&quot;),
        _,
        actorRef
      )
      val subject = new UseCaseWithAkkaAndZio(entityRefFor)
      val result = zio.Runtime.default.unsafeRun(for {
        _ &lt;- subject.apply(entityId, &quot;Hello&quot;)
        msg &lt;- entityRefFor(entityId).askIO(GreeterPersistentActor.GetMessage(_))
      } yield {
        println(s&quot;Answer is $msg&quot;)
        msg
      })

      result should be(&quot;Hello&quot;)
    }
  }

}
// build.sbt
ThisBuild / scalaVersion := &quot;2.13.11&quot;

lazy val root = (project in file(&quot;.&quot;))
  .settings(
    name := &quot;sample&quot;,
    libraryDependencies +=  &quot;com.typesafe.akka&quot; %% &quot;akka-actor-typed&quot; % &quot;2.8.2&quot;,
    libraryDependencies +=  &quot;com.typesafe.akka&quot; %% &quot;akka-persistence-typed&quot; % &quot;2.8.2&quot;,
    libraryDependencies +=  &quot;com.typesafe.akka&quot; %% &quot;akka-cluster-sharding-typed&quot; % &quot;2.8.2&quot;,
    libraryDependencies +=  &quot;com.typesafe.akka&quot; %% &quot;akka-actor-testkit-typed&quot; % &quot;2.8.2&quot; % Test,
    libraryDependencies +=  &quot;dev.zio&quot; %% &quot;zio&quot; % &quot;1.0.18&quot;,
    libraryDependencies +=  &quot;org.scalatest&quot; %% &quot;scalatest&quot; % &quot;3.2.16&quot; % Test,
  )

答案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

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

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 :

  val effect:IO[Throwable, String] = ???
effect should completeMatching {
case str:String =&gt; // Yes. This is useless, but for the example
}
trait ZioMatchers {
def completeMatching(matcher: PartialFunction[Any, Any]): Matcher[IO[Any, Any]] = (effect: IO[Any, Any]) =&gt; {
val (complete, matches, result) = zio.Runtime.default
.unsafeRun(
effect
.flatMap {
case res if matcher.isDefinedAt(res) =&gt; ZIO.succeed((true, true, res))
case other                           =&gt; ZIO.succeed(true, false, other)
}
.catchAll(err =&gt; ZIO.succeed(false, false, err))
)
...
}
...
}

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:

确定