Scala无标签最终模式下用户仓库通用属性测试方案问询
问题
首先定义了领域类与仓储代数:
// 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]] }
当前开发使用InMemoryInterpreter,后续会新增Redis等其他解释器,希望用Scalatest做属性测试且不绑定特定实现,但现有测试与内存解释器紧耦合,无法复用:
trait UserRepositorySpec_1 extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks { "UserRepository" must { "create and retrieve users" in { // 问题:与特定解释器紧耦合,无法复用测试RedisUserRepositoryInterpreter等实现 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 => ??? } } } }
需要实现不绑定特定解释器的通用属性测试,让所有UserRepository解释器都能复用测试用例。
解决方案
1. 抽离通用生成器
把User的ScalaCheck生成器单独放到可复用的对象中,避免重复定义:
object UserGenerators { import org.scalacheck.Gen val userGen: Gen[User] = for { id <- Gen.alphaNumStr.suchThat(_.nonEmpty) // 确保ID非空,符合业务约束 name <- Gen.alphaNumStr.suchThat(_.nonEmpty) age <- Gen.posNum[Int] } yield User(id, name, age) }
2. 完善通用测试特质
为F[_]添加必要的类型约束(如Monad),同时抽象出repo实例和F的同步执行方法,让具体测试类实现这些细节:
import org.scalatest.wordspec.AnyWordSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import cats.Monad import UserGenerators._ trait UserRepositorySpec[F[_]: Monad] extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks { // 由具体测试类提供当前测试的仓储实例 def repo: UserRepositoryAlgebra[F] // 由具体测试类实现F到同步值的转换逻辑 def run[A](fa: F[A]): A "UserRepository" must { "create a user and retrieve it correctly" in { forAll(userGen) { user => val result = run { for { _ <- repo.createUser(user) maybeUser <- repo.getUser(user.id) } yield maybeUser } result shouldBe Some(user) } } "return None when retrieving a non-existent user" in { forAll(Gen.alphaNumStr.suchThat(_.nonEmpty)) { nonExistentId => val result = run(repo.getUser(nonExistentId)) result shouldBe None } } } }
3. 为每个解释器实现具体测试类
以InMemoryUserRepository[IO]为例:
import cats.effect.IO import cats.effect.unsafe.implicits.global class InMemoryUserRepositorySpec extends UserRepositorySpec[IO] { override def repo: UserRepositoryAlgebra[IO] = new InMemoryUserRepository[IO] override def run[A](fa: IO[A]): A = fa.unsafeRunSync() }
后续新增RedisUserRepositoryInterpreter时,只需新建测试类复用通用逻辑,还可添加特有测试:
import cats.effect.IO import cats.effect.unsafe.implicits.global class RedisUserRepositorySpec extends UserRepositorySpec[IO] { // 可在此处理Redis实例的初始化/清理逻辑 override def repo: UserRepositoryAlgebra[IO] = new RedisUserRepositoryInterpreter[IO] override def run[A](fa: IO[A]): A = fa.unsafeRunSync() // 添加Redis特有的测试用例 "RedisUserRepository" must { "handle concurrent user creation correctly" in { // 特定测试逻辑 } } }
核心优势
- 完全解耦:通用测试只依赖
UserRepositoryAlgebra代数和F[_]的类型约束,不绑定任何具体实现。 - 符合DRY原则:新增解释器时只需实现
repo和run方法,自动继承所有通用属性测试。 - 一致性保障:所有仓储实现都会验证核心定律(创建后可查询、不存在返回None等),确保行为统一。
内容的提问来源于stack exchange,提问作者Dexter
相关产品推荐
相关产品推荐

