我有一个代数的解释器,我想为它写一个单元测试。
口译员如下:
final case class LiveDbConnector[F[_] : MonadError[*[_], Throwable]](env: Environment[F]) extends DbConnector[F] {
override def read(url: DbUrl, user: DbUser, pw: DbPw): F[DbParams] =
(for {
a <- OptionT(env.get(EnvVariable(url.v)))
b <- OptionT(env.get(EnvVariable(user.v)))
c <- OptionT(env.get(EnvVariable(pw.v)))
} yield DbParams(DbUrl(a.v), DbUser(b.v), DbPw(c.v)))
.value
.flatMap {
case Some(v) => v.pure[F]
case None => DbSettingError.raiseError[F, DbParams]
}
}
代数如下:
trait DbConnector[F[_]] {
def read(url: DbUrl, user: DbUser, pw: DbPw): F[DbParams]
}
实现如下:
final case class DbParams(url: DbUrl, user: DbUser, pw: DbPw)
object DbConnector {
def impl[F[_] : MonadError[*[_], Throwable]](env: Environment[F])
: DbConnector[F] =
new LiveDbConnector[F](env)
}
我使用测试框架 https://scalameta.org/munit/。
如何为我的上述代数编写单元测试?
在几乎每个测试框架中,您都可以通过同步调用来执行此操作
// given
val env: Environment[IO] = ...
val connector: DbConnector[IO] = DbConnector.impl[OP](env)
val url: DbUrl = ...
val user: DbUser = ...
val pw: DbPw = ...
// when
val result = connector.read(url, user, pw).attempt.unsafeRunSync
// then
val expected: DbParams = ...
assert(result == Right(expected))
由于MUnit本身也支持Future,你也可以这样做:
// given
val env: Environment[IO] = ...
val connector: DbConnector[IO] = DbConnector.impl[OP](env)
val url: DbUrl = ...
val user: DbUser = ...
val pw: DbPw = ...
// when
connector.read(url, user, pw).attempt.unsafeToFuture.map { result =>
// then
val expected: DbParams = ...
assert(result == Right(expected))
}
事实上,你在那里F
,使你能够灵活地选择每个测试最容易测试的实现:cats.effect.IO
、cats.effect.SyncIO
、monix.eval.Task
等。不同的测试框架仅在如何在套件中组织测试,可以使用哪种匹配器以及有时具有可用的集成方面有所不同,但您可以看到即使没有集成也能够编写测试。
如果你的代数的每个实现都有仅依赖于输入的输出,并且遵循一些合约,您可以为其定义定律
class DbConnectorLaws[F[_]: MonadError[*[_], Throwable](
connector: DbConnector[F]
) {
// explicitly expressed contracts that tested class should fulfill
private def expectedReadOuput(dbUrl: DbUrl, user: DbUser, pw: DbPw) = ...
def nameOfReadContract(dbUrl: DbUrl, user: DbUser, pw: DbPw): F[Unit] =
connector.read(dbUrl, user, pw).map { result =>
// Cats laws has some utilities for making it prettier
assert(result == expectedReadOuput(dbUrl, user, pw))
}
}
然后你可以测试它,例如使用Scalacheck。
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll
// actual test with laws (cats call them discipline)
trait DbConnectorTests {
val laws: DbConnectorLaws[IO] // simplified, study cats laws if you need it
def readContract()(
implicit
dbUrls: Arbitrary[DbUrl]
users: Arbitrary[DbUser]
pws: Arbitrary[DbPw]
// also other implicits if necessary
) = {
implicit val input = for {
url <- dbUrls
user <- users
pw <- pws
} yield (url, user, pw)
// simplified as well
forall { case (url: DbUrl, user: DbUser, pw: DbPw) =>
laws.nameOfReadContract(url, user, pw).unsafeRunSync // throws if assertion fail
}
}
}
val test = new DbConnectorTests { val laws = new DbConnectorLaws[IO](implementation) }
test.readContract()
但是,似乎您的接口依赖于实现并且本身,它不提供任何可以以这种方式测试的合约。我提到它只是因为你在其他问题中问到了"法律"。