Setting Up and Config

Writing functional tests that are concise, easily understandable, and maintainable is a challenge in any language. A Java project often needs varying levels of granularity for maximum functional and unit test coverage. While JUnit is a great (and widely-accepted) tool for unit tests, functional testing can sometimes require a more mature, dedicated DSL. Presently, our most widely adopted solution, is to use the built-in functionality that comes with RSpec/JRuby to make building functional tests a little bit more straightforward.

At Revinate, we run a microservice-based architecture with Docker applications written mostly in Java and deployed to Kubernetes with Jenkins. Recently, for projects written in Scala, we’ve been exploring ScalaTest. Although most would agree that Scala is a more flexible language than Java, it is less familiar or popular, so writing tests in Scala is a new frontier for many. Below, find a little more info on how we do functional testing in Scala at Revinate.

The first step when starting out is to setup a Gradle source set, so you have a place to write your code. To declare a source set called functionalTest, add this block to build.gradle:

sourceSets {
    functionalTest {
        scala {
            compileClasspath += main.output
            runtimeClasspath += main.output
        }
    }
}

You also need to declare the classpath dependency. The most common strategy is to inherit everything from the "test" config, thus:

configurations {
    functionalTestCompile.extendsFrom testCompile
    functionalTestRuntime.extendsFrom testRuntime
}

Next, you need your test dependencies. This is a minimalistic set:

dependencies {
    // production dependencies
    // ...

    // unit test dependencies
    // ...

    // functional test dependencies
    functionalTestCompile 'org.scala-lang:scala-library:2.11.8'
    functionalTestCompile 'com.typesafe.play:play-ws_2.11:2.5.9'
    functionalTestCompile 'org.scalatest:scalatest_2.11:3+'
}

The prefix functionalTest here is enabled automatically because you declared the source set. If you choose to name your source set with any other value then this part should be changed to match. And last but not least, you need to be able to compile Scala, so add this one line:

apply plugin: 'scala'

Writing Scala tests

If your source set was setup properly, then the source directories src/functionalTest/scala/ and src/functionalTest/resources/ are ready to use. The most popular test framework today is ScalaTest. Check their documentation for more details.

Since the goal is to write functional tests, choose a BDD (behavior-driven development) style. We have a preference for using FreeSpec because of its more open, flexible format. Name your test with the suffix Spec. Use GivenWhenThen marks to describe what is happening.

Running Scala tests

IntelliJ can easily run Scala tests. Just make sure your project is imported properly and the Scala SDK is set up. For Gradle you need a task to execute those tests under a different source set:

task functionalTest(type: Test) {
    testClassesDir = sourceSets.functionalTest.output.classesDir
    classpath = sourceSets.functionalTest.runtimeClasspath
    testLogging {
        events 'passed', 'skipped', 'failed'
        exceptionFormat 'short'
    }
}

Gradle is configured for JUnit by default, so ScalaTest specs do not work without some extra config. The easiest way to get ScalaTest up and running is to use the JUnit runner. By default Gradle does not log the test progress unless something breaks. This is why the testLogging block, above, is enabled.

Play Framework dependencies

Although we use Spring Boot to write our applications, we can use parts of the Play Framework to help with our tests.

HTTP client

Creating an HTTP request in Java is not easy. There is a superfluous amount of options and most of them are very low-level. Even if you have a good library, you will need to write a lot of code to get details like authentication and encoding to work.

In order to leverage Scala’s powerful syntax, it is more efficient to use a Scala native library. Play has a very flexible and powerful production grade HTTP client. Check the documentation for examples. To configure the Play HTTP client, add this class to your codebase:

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import org.asynchttpclient.AsyncHttpClientConfig
import play.api.libs.ws.ahc.{AhcConfigBuilder, AhcWSClient}
import play.api.libs.ws.{WSAuthScheme, WSClient}

object HttpClient {
  implicit val system = ActorSystem()
  implicit val materializer = ActorMaterializer()

  val config: AsyncHttpClientConfig = new AhcConfigBuilder().build()
  val client: WSClient = new AhcWSClient(config)
}

JSON processing

The HTTP client can read responses into a wrapped JSON object. Check the JSON basics documentation for more details on how to traverse JSON trees.

Configuration management

We often have several ways to run our tests: local, docker, etc. In order to configure which endpoints to hit and which databases to connect, you can use Typesafe Config to organize your config files. This library is a transitive dependency of play-ws.

The config library allows a mix of JSON and properties for your settings.conf file. The easiest way to go is to use equals everywhere like a properties file and group hierarchical elements in JSON format. Here is an example config:

application {
  host = localhost
  host = ${?DOCKER_HOST_IP}
  username = user
  password = passwd
}

In this config, the host is declared twice. This is possible because the config is evaluated from top to bottom like a script and if an environment variable called DOCKER_HOST_IP is declared, the second variable overrides the previously declared value. Sometimes we run the application locally, so we may want to override a file. The library does not have a built-in config override strategy, but you can build one with a few lines. As a more detailed example, set settings.local.conf to:

application.host = localhost

And TestConfig.scala to:

import com.typesafe.config.ConfigFactory

object TestConfig {
  val conf = {
    val baseConfig = ConfigFactory.load("settings.conf")
    System.getenv("PROFILE") match {
      case null => baseConfig
      case profile: String => ConfigFactory.load(s"settings.$profile.conf").withFallback(baseConfig)
    }
  }
}

The configuration override is loaded based on an environment variable called PROFILE. You just have to make sure IntelliJ or any other IDE you may be using has this environment variable set on the test executor. To access a config value, you can do:

val applicationHost:String = TestConfig.conf.getString("application.host")