项目作者: ITV

项目描述 :
Scala library to control docker containers programmatically
高级语言: Scala
项目地址: git://github.com/ITV/servicebox.git
创建时间: 2018-03-21T09:34:03Z
项目社区:https://github.com/ITV/servicebox

开源协议:Other

下载


Service box

A library to define and run test dependencies using scala and Docker containers.

WARNING: This library in unmaintained!

Containers and integration testing

Scala’s powerful type system can help avoiding a range of otherwise common bugs,
removing the need for pedantic, low-level unit testing. However, testing
how the various components integrate into a larger system is still necessary. It is in fact at this higher level that errors are
typically discovered (e.g. serialisation/deserialisation, missing configuration values, SQL queries working differently
across different database vendors, etc.).

Servicebox allows to define external dependencies in idiomatic scala, managing their lifecycle and execution within
Docker containers. The main goal is to support developers in writing effective and reliable integration tests, by making
easier to setup a CI/CD environment that closely resembles the production one.

Status

Build Status
Latest version

This library is at an early development stage and its API is likely to change significantly over the upcoming releases.

Getting started

You can install servicebox by adding the following dependencies to your build.sbt file:

  1. val serviceboxVersion = "<CurrentVersion>"
  2. libraryDependencies ++= Seq(
  3. "com.itv" %% "servicebox-core" % serviceboxVersion,
  4. "com.itv" %% "servicebox-docker" % serviceboxVersion, //docker support
  5. )

To start with, you must specify your service dependencies as follows:

  1. import cats.effect.{ContextShift, IO}
  2. import scala.concurrent.duration._
  3. import cats.data.NonEmptyList
  4. import com.itv.servicebox.algebra._
  5. import com.itv.servicebox.interpreter._
  6. import com.itv.servicebox.docker
  7. import doobie._
  8. import doobie.implicits._
  9. import scala.concurrent.ExecutionContext
  10. object Postgres {
  11. case class DbConfig(host: String, dbName: String, password: String, port: Int)
  12. implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
  13. val xa = Transactor.fromDriverManager[IO](
  14. "org.postgresql.Driver",
  15. "jdbc:postgresql:world",
  16. "postgres",
  17. ""
  18. )
  19. def pingDb(value: DbConfig): IO[Unit] = IO {
  20. sql"select 1".query[Unit].unique.transact(xa)
  21. }
  22. def apply(config: DbConfig): Service.Spec[IO] = {
  23. // this will be re-attempted if an error is raised when running the query
  24. def dbConnect(endpoints: Endpoints): IO[Unit] =
  25. for {
  26. _ <- IOLogger.info("Attempting to connect to DB ...")
  27. ep = endpoints.toNel.head
  28. serviceConfig = config.copy(host = ep.host, port = ep.port)
  29. _ <- pingDb(serviceConfig)
  30. _ <- IOLogger.info("... connected")
  31. } yield ()
  32. Service.Spec[IO](
  33. "Postgres",
  34. NonEmptyList.of(
  35. Container.Spec("postgres:9.5.4",
  36. Map("POSTGRES_DB" -> config.dbName, "POSTGRES_PASSWORD" -> config.password),
  37. Set(PortSpec.autoAssign(5432)),
  38. None,
  39. None)),
  40. Service.ReadyCheck[IO](dbConnect, 50.millis, 5.seconds)
  41. )
  42. }
  43. }

A Service.Spec[F[_]] consists of one or more container descriptions, together with a ReadyCheck: an effectfull function
which will be called repeatedly (i.e. every 50 millis) until it either returns successfully or it times out.

Once defined, one or several service specs might be executed through a Runner:

  1. import scala.concurrent.ExecutionContext.Implicits.global
  2. implicit val tag: AppTag = AppTag("com.example", "some-app")
  3. val config = Postgres.DbConfig("localhost", "user", "pass", 5432)
  4. val postgresSpec = Postgres(config)
  5. //evaluate only once to prevent shutdown hook to be fired multiple times
  6. lazy val runner = {
  7. val instance = docker.runner()(postgresSpec)
  8. sys.addShutdownHook {
  9. instance.tearDown.unsafeRunSync()
  10. }
  11. instance
  12. }

The service Runner exposes two main methods: a tearDown, which will kill all the containers
defined in the spec, and a setUp:

  1. val registeredServices = runner.setUp.unsafeRunSync()

This returns us a wrapper of a Map[Service.Ref, Service.Registered[F]]
providing us with some convenience methods to resolve running services/containers:

  1. val pgLocation = registeredServices.locationFor(postgresSpec.ref, 5432).unsafeRunSync()

Notice that, while in the Postgres spec we define a container port, the library will automatically bind it to
an available host port (see InMemoryServiceRegistry for details). Remember that, in order to use the service
in your tests, you will have to point your app to the dynamically assigned host/port

  1. pgLocation.port

Detailed example

Please refer to this module for a more detailed usage example illustrating how to integrate the library
with scalatest.

Key components

The library currently consists of the following modules:

  • An “algebra” to define test dependencies (aka Service) as aggregates of one or several Container.
  • An InMemoryServiceRegistry that can automatically assign available host ports to a service containers.
  • A Scheduler, which provides a simple interface suitable to repeatedly check if a service is ready
  • “Interpreters” to setup/check/teardown services using Docker as container technology, and scala.concurrent.Future or cats.effect.IO
    as the effect system.

Component diagram

Modules

  • core: the core algebra, with built-in support for scala.concurrent.Future.
  • core-io: optional support for cats.effect.IO
  • docker: a docker interpreter for the core algebra.