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 {
"UserRepository" must {
"create and retrieve users" 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 <- 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))
}
}
}
}
I have something like this in mind.
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 => ???
}
}
}
}
How about this:
Now, you can do this (using
cats.IOas effect):But you could also do this (using
cats.Idas "pseudo-effect"):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
FlatMapinstance as constructor parameter (and once one uses an abstract class, why not pass therepounder test as well). Also, it requires less care regarding initialization order.I you prefer to use a trait, you could do it like this:
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.