You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 06:10:34