- Tests are concise and quick to write
- Specs can be written in natural language, user stories, or however you normally describe software functionality
- Output is natural language, so identifying the source of any bugs, regressions, etc is easy
- Test code is generally such that developers who are unfamiliar with Scala can understand it quickly
- Scala and Specs make it trivial to write focused, single-assertion tests with little boilerplate
- Write empty specs as requirements emerge and bugs are found then implement them when you have time
- You get to use Scala at work
This example will use Maven and a very simple Java REST service built with Jersey. In the real world you'd probably be using Spring and a lot of other libraries, but I've tried to distill this example down to the barest essentials in order to focus on Specs and how it plays with Maven.
Envision a beer service. It accepts requests for beer by type and size in ounces. It should return a 404 Not Found response if the beer is not a type it knows (Corona, Guiness or Fat Tire). If someone tries to order a beer smaller than a pint, the service should respond with a 400 Bad Request response. And if the service is able to fulfill the request, it should return a Json Beer representation.
As you work with your design team to specify these behaviors, you can write a first pass at your specs. The great thing about specs is how well they translate from the plain English description above:
"Calling GET /beer" should {
"when called with" in {
"a sizeOunces argument of 4" in {
"return status 400" >> { }
}
"a beer name 'Budweiser'" in {
"return status 404" >> { }
}
"beer name 'Guiness' and sizeOunces 16" in {
"return status 200" >> { }
"return a Beer representation" in {
"with" in {
"name 'Guiness'" >> {}
"sizeOunces 16" >> {}
}
}
}
}
}
You can read each spec from its outermost part to inner, for example: "Calling GET /beer should when called with beer name 'Guiness' and sizeOunces 16 return status 200". You can leave these specs empty until you implement the corresponding functionality. Some build environments like Hudson display the unimplemented tests separately from the passing or failing tests. These unimplemented tests are a great aspect of using Specs. They convey to others what the expected functionality is, though it may not yet be implemented, and it serves as a checklist for you the developer. As new requirements emerge, you can immediately write empty specs that reflect them.
Setting up pom.xml
In order to use Scala in a Maven project, you'll need to set up a few plugins, dependencies. etc. I'll highlight the ones that may be unfamiliar to Java developers, and the entire pom.xml is shown below.
Repositories
I'm not going to get into repository configuration much, since everyone's setup is different. One repository to be aware of when doing Scala development is Scala Tools. It's built into SBT, but you'll need to add it to your Maven repository or project pom to use it in a Maven project.
<repository>
<id>scala-tools.org</id>
<name>Scala-tools Maven2 Repository</name>
<url>http://scala-tools.org/repo-releases</url>
</repository>
Dependencies
Scala
This example uses version 2.8.0 of the Scala library.
<!-- Scala -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.8.0</version>
</dependency>
Specs
Specs is a library for Behavior Driven Development in Scala. It integrates easily with mock libraries, ScalaTest for generating test data, and various IDEs and build tools. Scala Specs can be run as JUnit tests, allowing integration into Java development environments like Eclipse, IntelliJ and NetBeans as well as CI systems like Hudson and Bamboo. It has an excellent lightweight syntax that is easy to read and write. I'll only be scratching the surface of Specs' functionality here, but you should check out the excellent documentation for more examples.
<!-- Specs -->
<dependency>
<groupId>org.scala-tools.testing</groupId>
<artifactId>specs_2.8.0</artifactId>
<version>1.6.5</version>
</dependency>
Dispatch
Dispatch is a wrapper around HttpClient that provides a very terse DSL syntax and the ability to make Http calls in a Scalactic way. The overloaded operators throw some people off so if you prefer to use HttpClient or any other Http library, that works too. I have found that once I got familiar with Dispatch syntax, I quite like it, and it meshes well with Specs to allow me to quickly bang out a lot of test calls.
<!-- Dispatch -->
<dependency>
<groupId>net.databinder</groupId>
<artifactId>dispatch-http_2.8.0</artifactId>
<version>0.7.7</version>
<scope>test</scope>
</dependency>
Lift-Json
Dispatch provides support for Json through the lift-json library, which is a part of the Lift framework. It provides straightforward serialization and deserialization of Scala case classes as well as clear XPath-like parsing of Json.
<dependency>
<groupId>net.databinder</groupId>
<artifactId>dispatch-lift-json_2.8.0</artifactId>
<version>0.7.7</version>
<scope>test</scope>
</dependency>
And the rest...
You'll also need to add JUnit and Jersey for this example project. See the complete pom.xml below for specifics.
Build
You'll need to tell Maven to look for Scala tests.
<testSourceDirectory>${project.basedir}/src/test/scala</testSourceDirectory>
Compile the Scala Tests
Configure the Scala compiler plugin so that your Scala tests get built like so:
<!-- Compile Scala -->
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
Surefire
Clarify that you would indeed like to run the Scala tests by telling the Maven Surefire plugin when and what to run. In this example we flout JUnit convention and run things that end in Spec. Observant types will notice that the configuration says **/*Spec.java even though we're writing tests in Scala. Just go with it.
<!-- Tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
<argLine>-Xmx512m</argLine>
<includes>
<include>**/*Spec.java</include>
</includes>
</configuration>
</plugin>
Start and Stop Jetty
This configuration will run Jetty for the duration of the build. The sophisticated among you may wish to start and stop the server at different phases of the build. Determining an appropriate strategy is left as an exercise to the reader.
<!-- Run Jetty -->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.24</version>
<configuration>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>2342</port>
</connector>
</connectors>
<stopKey>stop</stopKey>
<stopPort>9999</stopPort>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>process-classes</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
Beer Service
With the Maven setup behind us, we'll throw together a simple Jersey service so there's something to test. Here's a Beer JavaBean and a service class to serve some Beers.
/src/main/java/com/janxspirit/Beer.java
package com.janxspirit;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Beer {
private String name;
private int sizeOunces;
public Beer() {}
public Beer(String name, int sizeOunces) {
this.name = name;
this.sizeOunces = sizeOunces;
}
@XmlElement(name = "name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@XmlElement(name = "sizeOunces")
public int getSizeOunces() {
return sizeOunces;
}
public void setSizeOunces(int sizeOunces) {
this.sizeOunces = sizeOunces;
}
}
/src/main/java/com/janxspirit/BeerResource.java
package com.janxspirit;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.Path;
import javax.ws.rs.GET;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
@Path("/")
public class RestResource {
private List beerList;
public RestResource() {
beerList = new ArrayList();
beerList.add("Corona");
beerList.add("Guiness");
beerList.add("Fat Tire");
}
@GET
@Produces("application/json")
@Path("beers/{name}")
public Response getBeer(@PathParam("name") String name, @QueryParam("sizeOunces") int sizeOunces) {
if (name == null || !beerList.contains(name)) {
return Response.status(Status.NOT_FOUND).entity(String.format("Sorry we don't have any %s", name)).build();
}
if (sizeOunces < 8) {
return Response.status(Status.BAD_REQUEST).entity("Sorry no kid-size beers.").build();
}
Beer beer = new Beer(name, sizeOunces);
System.out.println(String.format("Pouring %s ounces of %s", sizeOunces, name));
return Response.ok(beer).build();
}
}
/src/main/java/JsonContextResolver.java
In order to get the default Json library to render the responses correctly, you need to add a class like the following. There are other ways, but in general its unfortunate that Jersey doesn't do the right thing out of the box at least presenting integer attributes unquoted in Json. We explicitly set the sizeOunces property to be non-String below.
package com.janxspirit;
import com.sun.jersey.api.json.JSONConfiguration;
import com.sun.jersey.api.json.JSONJAXBContext;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import javax.xml.bind.JAXBContext;
@Provider
public class JsonContextResolver implements ContextResolver<JAXBContext> {
private JAXBContext context;
private Class[] types = {Beer.class};
public JsonContextResolver() throws Exception {
this.context =
new JSONJAXBContext(
JSONConfiguration.mapped().nonStrings("sizeOunces").build(), types);
}
public JAXBContext getContext(Class<?> objectType) {
for (Class type : types) {
if (type == objectType) {
return context;
}
}
return null;
}
}
web.xml
A last bit of setup is to configure your web.xml to fire up the Jersey servlet. This goes in the usual place - src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>scala_test_jersey</display-name>
<servlet>
<servlet-name>RestResourcer</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>com.janxspirit</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>RestResourcer</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
Writing the Specs
Now the fun part! This is actually much less involved than all the boilerplate above, but hopefully the setup is helpful in getting you to the good stuff more quickly.
The first thing I did was create a simple case class and a function to convert the Dispatch responses into a simple object with a status code and body. I'd love to hear any better approaches to this, but in any event, this does work.
def toResponse(req: Request): StatusCodeBody = {
val http = new Http
http x (req as_str) {
case (status, _, ent, _) => StatusCodeBody(status, EntityUtils.toString(ent.get))
}
}
case class StatusCodeBody (status: Int, body: String)
Now we fill in the empty spec bodies. In most cases that's a one-liner to make the Http call and then a simple equality test against the response object. Here's what a simple call to the service looks like:
val app = "scala_test_jersey"
val getBeerRes = toResponse(:/("localhost", 2342) / app / "beers" / "Budweiser")
"return status 404" >> { getBeerRes.status must_== 404 }
As you can see, assembling an Http call with dispatch is very straightforward. If you don't like the overloaded operators, I suggest you use trusty old HttpClient.
Here's all you need to do to add a query param to a request:
val getBeerRes = toResponse(:/("localhost", 2342) / app / "beers" / "Corona" <<? Map("sizeOunces" -> 4))
"return status 400" >> { getBeerRes.status must_== 400 }
These examples also show one of the Specs matchers 'must_==' The Specs library provides a lot of matchers and if you need some new ones, it's very easy to create them.
For the last two specs we'll do another simple check of the Http response code as well as deserialize the Json response body and inspect the result. To do that, I've created a simple case class. Lift-Json takes care of the rest. Here's what that looks like:
case class JsonBeer(name: String, sizeOunces: Int)
val getBeerRes = toResponse(:/("localhost", 2342) / app / "beers" / "Guiness" <<? Map("sizeOunces" -> 16))
"return status 200" >> { getBeerRes.status must_== 200 }
"return a Beer representation" in {
val beer = parse(getBeerRes.body).extract[JsonBeer]
"with" in {
"name 'Guiness'" >> {beer.name mustEqual "Guiness"}
"sizeOunces 16" >> {beer.sizeOunces must_== 16}
}
}
Pretty slick.
And now the moment of truth. You can run
mvn clean test
or get fancier and run it in your IDE. This screenshot shows output in Netbeans.
There's a bit more setup for the spec class. I've set these tests to share variables and run sequentially. The complete code for BeerSpec.scala and pom.xml is below.
I hope this setup guide has been helpful and encourages some folks to dip their toe in with Scala testing. Happy coding!
/src/test/scala/BeerSpec.scala
import org.specs._
import dispatch._
import net.liftweb.json.JsonParser._
import net.liftweb.json.JsonAST._
import net.liftweb.json.JsonDSL._
import org.apache.http.util.EntityUtils
class BeerSpec extends SpecificationWithJUnit {
implicit val formats = net.liftweb.json.DefaultFormats // Brings in default date formats etc.
val app = "scala_test_jersey"
setSequential()
shareVariables()
"Calling GET /beers" should {
"when called with" in {
"name 'Budweiser'" in {
val getBeerRes = toResponse(:/("localhost", 2342) / app / "beers" / "Budweiser")
"return status 404" >> { getBeerRes.status must_== 404 }
}
"name 'Corona' and sizeOunces 4" in {
val getBeerRes = toResponse(:/("localhost", 2342) / app / "beers" / "Corona" <<? Map("sizeOunces" -> 4))
"return status 400" >> { getBeerRes.status must_== 400 }
}
"beer name 'Guiness' and sizeOunces 16" in {
val getBeerRes = toResponse(:/("localhost", 2342) / app / "beers" / "Guiness" <<? Map("sizeOunces" -> 16))
"return status 200" >> { getBeerRes.status must_== 200 }
"return a Beer representation" in {
val beer = parse(getBeerRes.body).extract[JsonBeer]
"with" in {
"name 'Guiness'" >> {beer.name mustEqual "Guiness"}
"sizeOunces 16" >> {beer.sizeOunces must_== 16}
}
}
}
}
}
def toResponse(req: Request): StatusCodeBody = {
val http = new Http
http x (req as_str) {
case (status, _, ent, _) => StatusCodeBody(status, EntityUtils.toString(ent.get))
}
}
}
case class JsonBeer(name: String, sizeOunces: Int)
case class StatusCodeBody (status: Int, body: String)
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.janxspirit</groupId>
<artifactId>scala_test_jersey</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>scala_test_jersey</name>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<id>scala-tools.org</id>
<name>Scala-tools Maven2 Repository</name>
<url>http://scala-tools.org/repo-releases</url>
</repository>
</repositories>
<dependencies>
<!-- Scala -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.8.0</version>
</dependency>
<!-- Dispatch -->
<dependency>
<groupId>net.databinder</groupId>
<artifactId>dispatch-http_2.8.0</artifactId>
<version>0.7.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.databinder</groupId>
<artifactId>dispatch-lift-json_2.8.0</artifactId>
<version>0.7.7</version>
<scope>test</scope>
</dependency>
<!-- Specs -->
<dependency>
<groupId>org.scala-tools.testing</groupId>
<artifactId>specs_2.8.0</artifactId>
<version>1.6.5</version>
</dependency>
<!-- JUnit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
</dependency>
<!-- Jersey -->
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-server</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<build>
<testSourceDirectory>${project.basedir}/src/test/scala</testSourceDirectory>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<version>2.9.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- Compile Scala -->
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Compile Java -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<!-- Tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
<argLine>-Xmx512m</argLine>
<includes>
<include>**/*Spec.java</include>
</includes>
</configuration>
</plugin>
<!-- Run Jetty -->
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.24</version>
<configuration>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>2342</port>
</connector>
</connectors>
<stopKey>stop</stopKey>
<stopPort>9999</stopPort>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>process-classes</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Gregg, thanks for the useful article! Nice way to introduce Scala into Java projects. I'll take it into account.
ReplyDeleteThanks... this is excellent! I'm always looking for ways to sneak Scala in ;). Bonus points for explaining every step in just the right amount of detail! Thanks alot!
ReplyDeleteGreat article!
ReplyDeleteUnfortunately "Dispatch" library is horribly not working with maven. That is one downside of using custom tools...