FoundationDB Integration Tests in Scala

When Apple announced the FoundationDB Record Layer, I was keen to take it for a test drive.

The first order of business is to figure out a way to write integration tests. The docker-it-scala project gives us an easy way to launch Docker containers for testing, and the FoundationDB project provides an official Docker image.

Unfortunately, we can’t use the FoundationDB Docker image out of the box as it doesn’t come with a pre-configured database. To get around this, we can implement a custom ready checker that gives us the ability to run arbitrary fdbcli commands on startup.

Here’s our implementation of the FoundationDB Docker test kit. We’re using the Spotify Docker client.

import com.apple.foundationdb.record.provider.foundationdb.FDBDatabaseFactory
import com.spotify.docker.client.messages.PortBinding
import com.whisk.docker.testkit.ContainerState.{HasId, Ready}
import com.whisk.docker.testkit.{
  BaseContainer,
  ContainerCommandExecutor,
  ContainerSpec,
  DockerReadyChecker,
  FailFastCheckException,
  ManagedContainers
}
import com.whisk.docker.testkit.scalatest.DockerTestKitForAll
import org.scalatest.Suite
import scala.concurrent.{ExecutionContext, Future}

/*
 * Provides a FoundationDB Docker container for integration tests
 */
trait FoundationDbDockerTestKit extends DockerTestKitForAll {
  self: Suite =>

  val fdb = FDBDatabaseFactory
    .instance()
    // Assumes a src/test/resources/fdb.cluster file with the following contents:
    // docker:docker@127.0.0.1:4500
    .getDatabase(getClass.getResource("/fdb.cluster").getPath)

  private val fdbPort = 4500

  private lazy val fdbContainer = ContainerSpec("foundationdb/foundationdb:latest")
    .withPortBindings(fdbPort -> PortBinding.of("0.0.0.0", fdbPort))
    .withEnv("FDB_NETWORKING_MODE=host", s"FDB_PORT=$fdbPort")
    // The FoundationDB Docker image doesn't come with a pre-configured database
    .withReadyChecker(new FdbDockerReadyChecker("configure new single memory"))

  override val managedContainers: ManagedContainers = fdbContainer.toManagedContainer
}

We use a custom FdbDockerReadyChecker that runs fdbcli configure new single memory once FoundationDB is up and running. This creates a new database that uses the memory storage engine and single (no replication) mode. This is fine for integration tests.

Our ready checker looks like this:

/*
 * Ready checker for FoundationDB container, with the ability to run a fdbcli --exec command
 * once FoundationDB has started.
 */
class FdbDockerReadyChecker(onReadyFdbcliExec: String = "status") extends DockerReadyChecker {

  override def apply(container: BaseContainer)(implicit docker: ContainerCommandExecutor,
                                               ec: ExecutionContext): Future[Unit] = {
    val execOnReady: (String) => Future[Unit] = (containerId) => {
      Future {
        docker.client
          .execCreate(containerId, Array("/usr/bin/fdbcli", "--exec", onReadyFdbcliExec))
      } map { exec =>
        docker.client.execStart(exec.id()).readFully()
      } map (_ => ())
    }

    container.state() match {
      case Ready(info) =>
        execOnReady(info.id())
      case state: HasId =>
        docker
          .withLogStreamLinesRequirement(state.id, withErr = true)(
            _.contains("FDBD joined cluster.")
          )
          .flatMap(_ => execOnReady(state.id))
      case _ =>
        Future.failed(
          new FailFastCheckException("Can't initialise LogStream to container without ID")
        )
    }
  }
}

To use the above, you’ll need a fdb.cluster file with the following contents in your test resources directory:

docker:docker@127.0.0.1:4500

Finally, here’s what an integration test might look like:

import org.scalatest.{AsyncWordSpec, Matchers}
import monix.execution.Scheduler.Implicits.global

/*
 * Integration tests for a SampleRepository
 * The tests in this spec need a Docker engine to run FoundationDB
 */
class SampleRepositorySpec extends AsyncWordSpec with Matchers with FoundationDbDockerTestKit {

  val repository = new SampleRepository(fdb)

  "SampleRepository" when {
    "asked to get a record that doesn't exist" should {
      "return empty option" in {
        repository.get("inexistantrecord").runAsync map { r =>
          r shouldEqual None
        }
      }
    }
  }
}
comments powered by Disqus