Skip to content

Instantly share code, notes, and snippets.

@tomwadeson
Created November 5, 2019 16:56
Show Gist options
  • Save tomwadeson/e21ac57e8fde89011938c8f5dfdce4fa to your computer and use it in GitHub Desktop.
Save tomwadeson/e21ac57e8fde89011938c8f5dfdce4fa to your computer and use it in GitHub Desktop.
A demonstration of specifying a `FooRepository` capability and ensuring that in-memory and postgres-based interpreters are compliant
package foo
import cats.effect.{IO, Resource, Sync}
import cats.effect.concurrent.Ref
import cats.implicits._
import doobie.util.transactor.Transactor
/**
* The `Foo` domain model.
*
* We'd probably choose to separate the id from the rest of the record, but that's not important for our purposes here
*/
final case class Foo(id: String, name: String, age: Int)
/**
* The `FooRepository` "algebra" or "capability".
*/
trait FooRepository[F[_]] {
def create(foo: Foo): F[Unit]
def findById(id: String): F[Option[Foo]]
}
object FooRepository {
/**
* An in-memory/reference implementation of the `FooRepository` capability.
*
* Use this for:
* - testing code that depends on a `FooRepository`
* - demoing an early version of your product to get feedback before you're ready to deal with operational stuff
*
* On testing, you'll find this implementation useful but not sufficient to test your higher-level code: you'll
* probably also want a `StubFooRepository` that you can configure to inject failure or interesting behaviour so that
* you're able to test error handling and retry logic, etc.
*
* (Feel free to use some concrete effect type here instead of an `F[_] : Sync`—that's fine.)
* (Also feel free to define some nominal `InMemoryFooRepository` class elsewhere if you'd prefer to do that.)
*/
def inMemory[F[_]: Sync]: F[FooRepository[F]] =
Ref[F]
.of(Map.empty[String, Foo])
.map(foos =>
new FooRepository[F] {
override def create(foo: Foo): F[Unit] =
foos.update(_ + (foo.id -> foo))
override def findById(id: String): F[Option[Foo]] =
foos.get.map(_.get(id))
})
}
/**
* An implementation of `FooRepository` that uses doobie to interact with postgres
*/
final class DoobiePostgresFooRepository[F[_]] private (xa: Transactor.Aux[F, Unit]) extends FooRepository[F] {
override def create(foo: Foo): F[Unit] = ???
override def findById(id: String): F[Option[Foo]] = ???
}
object DoobiePostgresFooRepository {
/**
* Assume this has all we need to create a `Transactor`
*/
trait Config
def create[F[_]](config: Config): DoobiePostgresFooRepository[F] =
new DoobiePostgresFooRepository[F](null)
}
import org.scalatest.PropSpec
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.ScalacheckShapeless._
/**
* A specification for our `FooRepository` capability. *ALL* `FooRepository` interpreters *must* be tested against this
* spec.
*
* Incidentally, interpreting directly in `IO` is *fine*!
* These tests represent the edge of the world; there's no benefit to be had from interpreting in `State` or whatever.
*/
trait FooRepositorySpec extends PropSpec with ScalaCheckPropertyChecks {
property("`Foo`s are persisted") {
forAll { foo: Foo =>
(makeFooRepository use { fooRepository =>
for {
_ <- fooRepository.create(foo)
found <- fooRepository.findById(foo.id)
} yield {
assert(found contains foo)
}
}).unsafeRunSync()
}
}
property("`findById` is stable") {
forAll { foo: Foo =>
(makeFooRepository use { fooRepository =>
for {
_ <- fooRepository.create(foo)
found <- fooRepository.findById(foo.id).replicateA(20)
} yield {
assert(found.forall(_ contains foo))
}
}).unsafeRunSync()
}
}
property("`Foo`s are unique by id") {
// ...
}
def makeFooRepository: Resource[IO, FooRepository[IO]]
}
/**
* There's nothing fancy required to test our in-memory interpreter.
*
* BTW: I'm of the opinion that this is a unit test!
*/
class InMemoryFooRepositorySpec extends FooRepositorySpec {
override def makeFooRepository: Resource[IO, FooRepository[IO]] =
Resource.liftF(FooRepository.inMemory[IO])
}
/**
* There's a bunch of stuff we likely need to do to test our postgres-based interpreter: spin up a Docker container;
* apply database migrations to build the schema; configure thread- and connection-pools, etc.
*/
class DoobiePostgresFooRepositorySpec extends FooRepositorySpec {
/**
* Packaging all of this up in a `Resource` that's acquired and released for each test will be very slow.
* Either put up with it; embrace a bit of impurity (`unsafeRunSync` in `beforeAll` and `afterAll`); or experiment
* with https://github.com/kubukoz/flawless or ZIO Test.
*/
override def makeFooRepository: Resource[IO, FooRepository[IO]] =
for {
postgresDocker <- makePostgresDocker
config <- makeConfiguration(postgresDocker)
_ <- Resource.liftF(applyDatabaseMigrations(config))
} yield DoobiePostgresFooRepository.create[IO](config)
trait PostgresDocker
def makePostgresDocker: Resource[IO, PostgresDocker] = ???
def makeConfiguration(postgresDocker: PostgresDocker): Resource[IO, DoobiePostgresFooRepository.Config] = ???
def applyDatabaseMigrations(config: DoobiePostgresFooRepository.Config): IO[Unit] = ???
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment