1

I have an interpreter for an algebra and I would like to write a unit test for it.

The interpreter is as follows:

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]
      }
}

the algebra is as follows:

trait DbConnector[F[_]] {
  def read(url: DbUrl, user: DbUser, pw: DbPw): F[DbParams]
}

the implementation is as follows:

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)

} 

I use the test framework https://scalameta.org/munit/.

How to write unit test for my above algebra?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
softshipper
  • 32,463
  • 51
  • 192
  • 400

1 Answers1

0

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".

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64