In virtually every test framework you could do this by calling this synchronously
// 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))
Since MUnit also natively support Future, you could also do it like:
// 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))
}
The fact that you have F
there gives you the flexibility of picking the implementation that is the easiest to test, per test: cats.effect.IO
, cats.effect.SyncIO
, monix.eval.Task
, etc. And different test frameworks only differ in how you organize your tests in suites, what kind of matchers you can use and sometimes with available integrations, but you can see are able to write tests even without integrations.
If every single implementation if your algebra had output depending only on input, and which would follow some contracts you could define laws for it
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))
}
}
and then you could test it e.g with 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()
However, is seems that your interface is implementation dependent and on its own, it doesn't provide any contracts that could be tested this way. I mention it only because in other questions you asked about "laws".