在Scala中,基于属性的测试用于tagless final,其中包括变量解释器。

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

In scala property based tests for tagless final with variable inerpreter

问题

Here is the translated code:

// 领域
case class User(id: String, name: String, age: Int)
// 代数
trait UserRepositoryAlgebra[F[_]] {
  def createUser(user: User): F[Unit]
  def getUser(userId: String): F[Option[User]]
}

// 我有一个用于开发周期的InMemoryInterpreter。随着时间的推移,会有更多的解释器出现。我的意图是尝试使用基于属性的scalatest,而不绑定到任何特定的解释器。基本上,UserRepository 需要有每个解释器都应满足的法则/属性。

// 我可以提出一个,如下所示:
trait UserRepositorySpec_1 extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks {

  "UserRepository" must {
    "create and retrieve users" in {
      //问题:这与特定解释器紧密耦合。如何测试RedisUserRepositoryInterpreter,例如,遵循DRY?
      val repo = new InMemoryUserRepository[IO]
      val userGen: Gen[User] = for {
        id <- Gen.alphaNumStr
        name <- Gen.alphaNumStr
        age <- Gen.posNum[Int]
      } yield User(id, name, age)
      forAll(userGen) { user =>
        (for {
          _ <- repo.createUser(user)
          mayBeUser <- repo.getUser(user.id)
        } yield mayBeUser).unsafeRunSync() must be(Option(user))
      }
    }
  }
}

// 我心目中有类似这样的东西。
trait UserRepositorySpec[F[_]] extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks {
  import generators._

  def repo: UserRepositoryAlgebra[F]

  "UserRepository" must {
    "create and find users" in {
      forAll(userGen){ user => ??? 

      }
    }
  }
}

Please note that I've retained the original code structure while providing the Chinese translation for the comments and code elements.

英文:

I have the following algebra

// domain
case class User(id: String, name: String, age: Int)
// algebra
trait UserRepositoryAlgebra[F[_]] {
  def createUser(user: User): F[Unit]
  def getUser(userId: String): F[Option[User]]
}

I have a InMemoryInterpreter for development cycle. There would be more interpreters coming up in time. My intention is to attempt scalatest with property based tests and not bind with any specific interpreter. Basically, there needs to be laws/properties for UserRepository that every interpreter should satisfy.

I could come up with one as

trait UserRepositorySpec_1 extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks {

  &quot;UserRepository&quot; must {
    &quot;create and retrieve users&quot; in {
      //problem: this is tightly coupling with a specific interpreter. How to test RedisUserRepositoryInterpreter, for example, follwing DRY ? 
      val repo               = new InMemoryUserRepository[IO]
      val userGen: Gen[User] = for {
        id   &lt;- Gen.alphaNumStr
        name &lt;- Gen.alphaNumStr
        age  &lt;- Gen.posNum[Int]
      } yield User(id, name, age)
      forAll(userGen) { user =&gt;
        (for {
          _         &lt;- repo.createUser(user)
          mayBeUser &lt;- repo.getUser(user.id)
        } yield mayBeUser).unsafeRunSync() must be(Option(user))
      }
    }
  }
}

I have something like this in mind.

trait UserRepositorySpec[F[_]] extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks {
  import generators._

  def repo: UserRepositoryAlgebra[F]

  &quot;UserRepository&quot; must {
    &quot;create and find users&quot; in {
      forAll(userGen){ user =&gt; ??? 
        
      }
    }
  }
}

答案1

得分: 2

Here's the translation of the code parts:

如何看待这个代码

```scala
import org.scalacheck.Gen
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.must._
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks

import cats.implicits._
import cats._

abstract class AbstractUserRepositorySpec[F[_] : FlatMap](repo: UserRepositoryAlgebra[F])
  extends AnyWordSpec
    with Matchers
    with ScalaCheckPropertyChecks {

  protected def run[A](value: F[A]): A

  "UserRepository" must {
    "create and retrieve users" in {
      val userGen: Gen[User] = for {
        id <- Gen.alphaNumStr
        name <- Gen.alphaNumStr
        age <- Gen.posNum[Int]
      } yield User(id, name, age)
      forAll(userGen) { user =>
        val result: F[Option[User]] =
          for {
            _ <- repo.createUser(user)
            mayBeUser <- repo.getUser(user.id)
          } yield mayBeUser
        run(result) must be(Option(user))
      }
    }
  }
}

现在,你可以这样做(使用cats.IO作为效果):

import cats.effect._

// 故意出错,因为我们想看到测试失败
class BuggyIOUserRepository extends UserRepositoryAlgebra[IO] {
  def createUser(user: User): IO[Unit] = IO.unit
  def getUser(userId: String): IO[Option[User]] = IO.none
}

class BuggyIOUserRepositorySpec
  extends AbstractUserRepositorySpec(new BuggyIOUserRepository) {
  protected def run[A](value: IO[A]): A = {
    import cats.effect.unsafe.implicits.global
    value.unsafeRunSync()
  }
}

但你也可以这样做(使用cats.Id作为“伪效果”):

// 故意出错,因为我们想看到测试失败
class BuggyIdUserRepository extends UserRepositoryAlgebra[Id] {
  def createUser(user: User): Id[Unit] = ()
  def getUser(userId: String): Id[Option[User]] = None
}

class BuggyIdUserRepositorySpec
  extends AbstractUserRepositorySpec(new BuggyIdUserRepository) {
  protected def run[A](value: Id[A]): A = value
}

如果代码中有什么不清楚的地方,欢迎在评论中提问。

附录:抽象类与特质 我使用了抽象类,因为(至少在Scala 2.x中),特质不能有构造参数,而且(至少对我来说)通过构造参数传递隐式的FlatMap实例更方便(一旦使用了抽象类,为什么不传递测试中的repo)。此外,它在初始化顺序方面需要更少的关注。

如果你更喜欢使用特质,可以这样做:

trait AbstractUserRepositorySpec[F[_]]
  extends AnyWordSpec
    with Matchers
    with ScalaCheckPropertyChecks {

  protected def repo: UserRepositoryAlgebra[F]
  protected implicit flatMapForF: FlatMap[F]
  protected def run[A](value: F[A]): A

  "UserRepository" must { 
    // ...
  }

但是这种方法需要在提供FlatMap[F]时多加注意:

你可能会尝试这样做:override protected implicit flatMapForF: FlatMap[F] = implicitly,但这将导致隐式解析中的无限循环。抽象类的变体避免了这样的陷阱。

英文:

How about this:

import org.scalacheck.Gen
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.must._
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks

import cats.implicits._
import cats._

abstract class AbstractUserRepositorySpec[F[_] : FlatMap](repo: UserRepositoryAlgebra[F])
  extends AnyWordSpec
    with Matchers
    with ScalaCheckPropertyChecks {

  protected def run[A](value: F[A]): A

  &quot;UserRepository&quot; must {
    &quot;create and retrieve users&quot; in {
      val userGen: Gen[User] = for {
        id &lt;- Gen.alphaNumStr
        name &lt;- Gen.alphaNumStr
        age &lt;- Gen.posNum[Int]
      } yield User(id, name, age)
      forAll(userGen) { user =&gt;
        val result: F[Option[User]] =
          for {
            _ &lt;- repo.createUser(user)
            mayBeUser &lt;- repo.getUser(user.id)
          } yield mayBeUser
        run(result) must be(Option(user))
      }
    }
  }
}

Now, you can do this (using cats.IO as effect):

import cats.effect._
// buggy on purpose because we want to see the test fail
class BuggyIOUserRepository extends UserRepositoryAlgebra[IO] {
def createUser(user: User): IO[Unit] = IO.unit
def getUser(userId: String): IO[Option[User]] = IO.none
}
class BuggyIOUserRepositorySpec
extends AbstractUserRepositorySpec(new BuggyIOUserRepository) {
protected def run[A](value: IO[A]): A = {
import cats.effect.unsafe.implicits.global
value.unsafeRunSync()
}
}

But you could also do this (using cats.Id as "pseudo-effect"):

// buggy on purpose because we want to see the test fail
class BuggyIdUserRepository extends UserRepositoryAlgebra[Id] {
def createUser(user: User): Id[Unit] = ()
def getUser(userId: String): Id[Option[User]] = None
}
class BuggyIdUserRepositorySpec
extends AbstractUserRepositorySpec(new BuggyIdUserRepository) {
protected def run[A](value: Id[A]): A = value
}

If something from the code is unclear to you, feel free to ask in comments.

Addendum: abstract class vs trait I used and abstract class because (at least in scala 2.x), traits cannot have constructor parameters and (at least for me) it is much more convenient to pass the implicit FlatMap instance as constructor parameter (and once one uses an abstract class, why not pass the repo under test as well). Also, it requires less care regarding initialization order.

I you prefer to use a trait, you could do it like this:

trait AbstractUserRepositorySpec[F[_]]
extends AnyWordSpec
with Matchers
with ScalaCheckPropertyChecks {
protected def repo: UserRepositoryAlgebra[F]
protected implicit flatMapForF: FlatMap[F]
protected def run[A](value: F[A]): A
&quot;UserRepository&quot; must { 
// ...
}

But this approach will require a bit of care when providing the FlatMap[F]:

You might be tempted to do override protected implicit flatMapForF: FlatMap[F] = implicitly, but that would lead to an endless loop in implicit resolution. The abstract class variant avoids such caveats.

huangapple
  • 本文由 发表于 2023年5月21日 23:24:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76300629.html
匿名

发表评论

匿名网友

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

确定