如何使用ScalaTest、ZIO和Akka测试持久化角色

7fyelxc5  于 2023-08-05  发布在  Scala
关注(0)|答案(1)|浏览(113)

我有一个“用例”,我想测试。这一个返回一个调用Actor(scala.concurrent.Future)的结果zio.IO。我想使用ScalaTest的AnyWordSpec

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")
    }
  }

个字符
我已经扭转了这个测试有一段时间没有能够有一些工作。
此时,使用上面的代码示例,我得到了这个错误:
[info] zio.FiberFailure:光纤故障。
[info]产生了未检查的错误。
[info] java.util.concurrent.TimeoutException:收件人[Actor[akka://UseCaseWithAkkaAndZioTest/user/$a#2047466881]]已终止。
[info] at akka.actor.typed.scaladsl.AskPattern$PromiseRef.(AskPattern.scala:140)
[新闻资讯]
有人可以帮助这个想法吗?我不得不承认,我在这里感到有点失落。
谢啦,谢啦

编辑要完整,如@Gaston所建议的,这里是可复制的示例。

请注意,它在使用Behavior时工作,但在使用EventSourcedBehavior时失败。(感谢Gaston,我在编写示例时发现了这一点)。

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]] => M): ZIO[Any, E, A] = {
      IO.fromFuture(_ => actorRef.ask(p)(timeout))
        .orDie
        .flatMap(e => 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 =>
        val event = MessageSet(c.message)
        Effect.persist(event).thenReply(c.replyTo)(_ => Right(():Unit))
      case c: GetMessage =>
        message match {
          case Some(value) =>
            Effect.reply(c.replyTo)(Right(value))
          case None =>
            Effect.reply(c.replyTo)(Left(new Exception("No message set")))
        }
    }

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

  def apply(persistenceId: PersistenceId, message: Option[String]): Behavior[Command] = EventSourcedBehavior
    .withEnforcedReplies[Command, Event, State](
      persistenceId,
      State(message),
      (state, cmd) => state.handle(cmd),
      (state, evt) => state.apply(evt)
    )
    .withTagger(_ => Set("Sample"))
    .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=>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 {

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

      result should be("Hello")
    }
  }

}
// build.sbt
ThisBuild / scalaVersion := "2.13.11"

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

jjjwad0x1#

有一个testing persistent actors的测试工具包。
EventSourcedBehaviorTestKit必须混合到套件测试中

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

字符串

编辑:

另一个问题是删除zio.Runtime.unsafeRun
我的实际解决方案是创建几个Matcher[ZIO[...],允许我编写测试,如:

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

trait ZioMatchers {

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

  ...
}

相关问题