How to Add Integration Tests to a Play Framework Application Using Scala

Learn how to create integration tests for a Scala Play application that make real HTTP calls to the API.

Brought to you by

Semaphore

Introduction

While unit tests usually make up the core of an application's tests, integration tests are often just as important. An integration test is named as such because it integrates with some other piece of technology. In this tutorial, you will learn how to integrate a Play Framework web application with an HTTP server to test the application's API.

You're in the right place if you have a good understanding of Scala programming, are comfortable with web framework concepts such as request/response, controllers, and routes, and want to learn more about web application integration testing. No specific knowledge of Play is required, but it may be helpful.

Prerequisites

Before starting, make sure you have the following installed:

Setting Up the Application

Since the focus of this tutorial is on testing, we're just going to create a small demo application to get us started. We'll be setting up an API that exposes some basic functionality for a library to manage customers and books.

Creating a New Project

The Play Framework provides many methods for setting up a new application. The easiest is probably using Typesafe's activator command (included with Play).

Create a new Play application:

activator new play-demo-library

When prompted, choose the option for "play-scala". This will create the play-demo-library directory, download the required libraries, and create the initial project structure.

Adding Routes

Starting out by defining the routes of the application can help guide development. Open the routes file under the conf folder and add the following routes:

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                   controllers.Application.index

# Books
GET /books                  controllers.Books.getBooks
GET /books/search           controllers.Books.search(author:Option[String], title:Option[String])
POST /book/:id/checkout     controllers.Books.checkout(id: Int)
POST /book/:id/checkin      controllers.Books.checkin(id: Int)

# Customers
POST /customer/new          controllers.Customers.addCustomer
GET /customer/:id           controllers.Customers.getCustomer(id: Int)

Adding Controllers

We defined the controllers that will be used in the previous section, so now we need to create those controllers.

The Application controller will be very small. If you used the Typesafe activator, this file should already exist. If it does not, create Application.scala in the controllers package. Modify the file so that it contains just the index method:

package controllers

import play.api.mvc._

class Application extends Controller {

  def index = Action {
    Ok(views.html.index())
  }

}

The index method is implemented using an Action which is Play's default way of handling requests. We will use many more Actions in the tutorial. The Ok method is a shortcut for returning an HTTP 200 response. Finally, this method instructs Play to load the index view. The index view is the only view that we'll use in this application, since the focus is on the API. We'll create that view now.

Empty the views package if one was created. If not, create the package. Create index.scala.html and add a welcome message:

Welcome to Semaphore Community Library!

The next controller defined in the routes file is the Books controller, so go ahead and create Books.scala in the controllers package:

package controllers

import play.api.mvc._

class Books extends Controller {

}

To get a Play application to build, we need to have all the controller methods created. It would be nice if we could simply stub out the methods to verify our setup, before implementing logic. Conveniently, the TODO marker included with Play will help us do just that.

Add the following methods to the Books controller:

def getBooks = TODO

def search(author:Option[String], title:Option[String]) = TODO

def checkout(id:Int) = TODO

def checkin(id:Int) = TODO

Finally, create Customers.scala in the controllers package:

package controllers

import play.api.mvc._

class Customers extends Controller {

  def getCustomer(id:Int) = TODO

  def addCustomer = TODO

}

At this point, the application should build. To test it, navigate to the play-demo-library directory and, use sbt to run the application:

sbt run

After the project compiles, navigate to http://localhost:9000/ in your browser. If all goes well, you will see the welcome message.

Adding Models and a Repository

We now have the structure for the web application API, but we don't have any functionality. Before we can start building the logic, we will need some objects to work with.

Create a models package and the following classes:

Book.scala

package model

case class Book(id:Int, title:String, author:String, var available:Boolean) { }

Customer.scala

package model

case class Customer(id:Int, name:String, address:String) { }

To provide access to these objects, create a dal package and a LibraryRepository.scala object inside the package. The name "dal" is short for "data access layer" and is a convention you will often see used in applications that integrate with a database.

In a real application, you would populate these objects from a database, but for the purpose of this tutorial, we will just create a few objects directly in the code:

package dal

import model.{Customer, Book}

object LibraryRepository {

  val customers = scala.collection.mutable.ListBuffer[Customer](
    Customer(1, "Bruce Wayne", "1 Wayne Enterprise Ave, Gotham"),
    Customer(2, "Clark Kent", "Hall of Justice, Metropolis"),
    Customer(3, "Diana Prince", "400 W. Maple, Gateway City, CA")
  )

  val books = List(
    Book(1,"Moby Dick", "Herman Melville", available = true),
    Book(2,"A Tale of Two Cities", "Charles Dickens", available = true),
    Book(3,"David Copperfield", "Charles Dickens", available = true),
    Book(42,"Hitchhiker's Guide to the Galaxy", "Douglas Adams", available = true),
    Book(24601,"Les Miserables", "Victor Hugo", available = true)
  )
}

The first functionality we'll add to the repository is searching for books. Add the following methods:

def getBooks:List[Book] = books

def getBooksByTitle(title:String):List[Book] = {
  books.filter(_.title.toUpperCase.contains(title.toUpperCase))
}

def getBooksByAuthor(author:String):List[Book] = {
  books.filter(_.author.toUpperCase.contains(author.toUpperCase))
}

The getBooks method is straight-forward: return all of the books. The two search methods are very similar. They filter the books by either the author or title, and convert the text to upper case, so that searching is case insensitive. This function uses contains to allow for partial searching. If you would prefer, you could use equals or startsWith to make the searches stricter.

In the API, we allowed users to search with both author and title. We'll reuse the logic we've already built by doing a set intersection of the author and title searches. Add the next search method:

def getBooksByAuthorAndTitle(author:String, title:String):List[Book] = {
  (getBooksByTitle(title).toSet & getBooksByAuthor(author).toSet).toList
}

Next, create the methods to check out and check in books:

def checkoutBook(book:Book):Unit = book.available = false

def checkinBook(book:Book):Unit = book.available = true

Last, we'll add a method to retrieve a book by ID. It's usually a good practice to include get-by-ID methods for your objects.

def getBook(id:Int):Option[Book] = books.find(b => b.id == id)

Now that we've implemented the repository methods for the books, we'll move on to the customers. If you remember from our routes, we need the ability to create a new customer and pull up a customer's information by ID. Add these final two methods to your repository:

def getCustomer(id:Int):Option[Customer] = customers.find(c => c.id == id)

def addCustomer(name:String, address:String):Customer = {
  val nextId = customers.maxBy(_.id).id + 1
  val newCustomer = Customer(nextId, name, address)
  customers += newCustomer
  newCustomer
}

Because this demo application does not use a database, the customer creation method has to know how to generate the next available ID. In a real application, your database should manage the ID generation.

At this point, we've defined how we want users to access data, and we've provided the internal logic to access the objects. The last part of the application that we need to implement is the link from our API to the objects — the controllers.

Implementing the Controllers

Earlier, we stubbed out the controller methods with Play's TODO marker. Now, we'll add in the actual logic for those methods.

This application will expose a JSON API and we can use Play's JSON library to help us. We'll again start with books, so open your Books.scala controller class and add the following imports:

import dal.LibraryRepository
import model.Book
import play.api.libs.json._

We need to provide a way for Play to know how to construct a JavaScript object from a Book. This is done with an implicit format. Add this line at the top of the class:

implicit val bookFormat = Json.format[Book]

We're now ready to implement getBooks. Again using an Action, we'll code this method to load the books from the repository, output them to JSON, and return the object with a HTTP 200 response. This sounds like a lot, but Play makes it simple:

def getBooks = Action {request =>
  Ok(Json.prettyPrint(Json.obj("books" -> LibraryRepository.getBooks)))
}

Let's break this down. Starting in the middle, we map the string books to the actual Book objects from the repository. Then, we wrap that map inside of a JavaScript object using Json.obj. By using Json.prettyPrint, the JSON text will be formatted for easy reading in the browser. Finally, remember that Play's convenient Ok method is shorthand for the HTTP 200 response. Go ahead and try navigating to http://localhost:9000/books in your browser to see the output.

The next route to implement is the search function. If you look closely, both parameters are optional. This means the user can search by author, by title, or by both. The parameters provided will determine which repository method to use. Complete the search method as follows:

def search(author:Option[String], title:Option[String]) = Action {request =>
  val results = {
    if(author.isDefined && title.isDefined) {
      LibraryRepository.getBooksByAuthorAndTitle(author.get, title.get)
    }
    else if(author.isDefined) LibraryRepository.getBooksByAuthor(author.get)
    else if(title.isDefined) LibraryRepository.getBooksByTitle(title.get)
    else List()
  }
  Ok(Json.prettyPrint(Json.obj("books" -> results)))
}

This is the first method we've seen where something could go wrong. The route will technically allow the user to not provide any search parameters. Instead of returning an error, the function returns an empty list. In the next methods, we'll actually return an error.

The final methods for the Books controller allow users to check in or check out books. There are three scenarios we need to handle:

  1. There is no book with the given ID.
  2. There is a book with the given ID, but it is already checked out/in.
  3. There is a book with the given ID and it can be checked out/in.

For all paths we will return a status code and message to the user. The happy path (scenario #3) will also include the book that was modified. Just like how the previous example mapped "books" to a result, these methods will map strings to values. Add the following methods to your Books controller:

def checkout(id:Int) = Action {request =>
  val bookOpt = LibraryRepository.getBook(id)
  if(bookOpt.isEmpty) {
    BadRequest(Json.prettyPrint(Json.obj(
      "status" -> "400",
      "message" -> s"Book not found with id $id.")))
  } else if(!bookOpt.get.available) {
    BadRequest(Json.prettyPrint(Json.obj(
      "status" -> "400",
      "message" -> s"Book #$id is already checked out.")))
  } else {
    LibraryRepository.checkoutBook(bookOpt.get)
    Ok(Json.prettyPrint(Json.obj(
      "status" -> 200,
      "book" -> bookOpt.get,
      "message" -> "Book checked out!")))
  }
}

def checkin(id:Int) = Action {request =>
  val bookOpt = LibraryRepository.getBook(id)
  if(bookOpt.isEmpty) {
    BadRequest(Json.prettyPrint(Json.obj(
      "status" -> "400",
      "message" -> s"Book not found with id $id.")))
  } else if(bookOpt.get.available) {
    BadRequest(Json.prettyPrint(Json.obj(
      "status" -> "400",
      "message" -> s"Book #$id is already checked in.")))
  } else {
    LibraryRepository.checkinBook(bookOpt.get)
    Ok(Json.prettyPrint(Json.obj(
      "status" -> 200,
      "book" -> bookOpt.get,
      "message" -> "Book checked back in!")))
  }
}

The errors are handled using Play's shortcut method BadRequest which is analogous to Ok, except that it returns HTTP error 400. If you'd like, try out some of these methods by loading the Books routes defined at the beginning of the tutorial in your browser.

Now it's time to move on to the two methods in the Customers controller. Open Customers.scala and add the following imports:

import dal.LibraryRepository
import model.Customer
import play.api.data._
import play.api.data.Forms._
import play.api.libs.json._

First up is the getCustomer method. It uses the same concepts we've already seen, so we'll save time by not going into detail here.

implicit val customerFormat = Json.format[Customer]

def getCustomer(id:Int) = Action { request =>
  val customerOpt = LibraryRepository.getCustomer(id)
  if (customerOpt.isDefined) {
    Ok(Json.prettyPrint(Json.obj(
      "status" -> "200",
      "customer" -> customerOpt.get
    )))
  } else {
    BadRequest(Json.prettyPrint(Json.obj(
      "status" -> "400",
      "message" -> s"Customer not found with id $id.")))
  }
}

The last thing to implement is the method to create new customers. Play handles POST requests most easily by using their Forms API. (Hint: You can also use this API to make actual webforms!) For our application, we can think of the form as a template for data. Our route defines a name and address as inputs, so those will be in the template. Add this value above the addCustomer method:

val addCustomerForm = Form(
  tuple(
    "name" -> text,
    "address" -> text
  )
)

Now go ahead and enter the following code for the addCustomer method:

def addCustomer = Action { implicit request =>
  val(name, address) = addCustomerForm.bindFromRequest().get
  val customer = LibraryRepository.addCustomer(name, address)
  Status(201)(Json.prettyPrint(Json.obj(
    "status" -> "201",
    "customer" -> customer,
    "message" -> "New customer created!"
  )))
}

The first thing you'll notice is we extract the name and address variables by leveraging the form we just created and Play's bindFormRequest method. Everything else here should look familiar, except for Status(201). Instead of using Ok this method returns HTTP 201 "CREATED" because the user is creating a new customer.

You now have a fully working API for this library demo! Spend a few minutes testing out the API yourself. If you want to test the POST routes, you might use curl or wget.

Adding Integration Tests

Now to the fun part — testing! The goal is to check the integration with the actual web server. To do so, we'll use the play.api.test library to make calls against the actual web service.

Testing the Application Index

To get your feet wet, we'll build a simple test to verify our index page is set up properly.

Start by emptying the test directory of the application and create a controllers package. Next, create ApplicationIntegrationSpec.scala inside that package as follows:

package controllers

import play.api.libs.ws._
import play.api.test._

class ApplicationIntegrationSpec extends PlaySpecification {

}

The Play testing tools give us access to behavior-driven testing vocabulary. This means we can define tests in terms of expected behavior, instead of the more literal unit testing vocabulary. As a first example, we can say that the application "should be" reachable. Create the following test:

"Application" should {
  "be reachable" in new WithServer {
    val response = await(WS.url("http://localhost:" + port).get()) //1

    response.status must equalTo(OK) //2
    response.body must contain("Semaphore Community Library") //3
  }
}

The line commented as "1" is a common pattern we'll see in these tests. Because we defined the test as WithServer, we can actually issue a real GET request to a URL. In this test, we want to try to reach the index page. Line 2 asserts that the response code is a 200 OK response. This is a basic check that is worth testing for all endpoints of the API. Finally, line 3 checks the value of the response body to make sure it is what we expect.

To run this test, use the sbt test command in the console.

sbt test

Books Controller Tests

The Application test was very basic because we essentially just wanted to test that the index page was available. To test the Books controller, we'll want to actually validate that the API works correctly, and that the JSON responses are correct. However, we won't spend a lot of energy on rigorously testing the logic behind the API. A fully-tested application should also include unit tests which test the core logic of the application without loading the HTTP server. The tests in this tutorial focus primarily on the interaction between the controllers and the web server.

Create BooksIntegrationSpec.scala in the controllers test package as follows:

package controllers

import dal.LibraryRepository
import model.Book
import play.api.libs.functional.syntax._
import play.api.libs.json.Reads._
import play.api.libs.json._
import play.api.libs.ws._
import play.api.test.{PlaySpecification, WithServer}

class BooksIntegrationSpec extends PlaySpecification {

}

You can see just from the imports that we'll be using more utilities than the Application test.

To start out, we need a way to read the JSON responses of the API back into Scala for ease of testing. To do that, we will use Play's JSON Reads functionality. If you want to learn more about the details of how this works, please see the documentation for Play. The very brief summary is that the Reads API provides a way to parse the structure of JSON response. Create the following value at the top of the class:

implicit val bookReader:Reads[Book] = (
  (JsPath \ "id").read[Int] and
  (JsPath \ "author").read[String] and
  (JsPath \ "title").read[String] and
  (JsPath \ "available").read[Boolean]
)(Book.apply _)

To save characters, add the following constant:

val Localhost = "http://localhost:"

Again using the vocabulary of behavior-driven tests, we can specify that the Books controller should do several things. The first test is against the /books/ route which should display all of the books. Here's how that test looks:

"Books Controller" should {
  "Provide access to all books" in new WithServer {
    val response = await(WS.url(Localhost + port + "/books").get())
    response.status must equalTo(OK)

    val books = (response.json \ "books").as[Seq[Book]]

    books.size must equalTo(5)
  }
}

The first two lines of the test should look very similar to the ApplicationIntegrationSpec. We make a call to the web service and then verify that the response was 200 OK. The next line parses out the books part of the JSON response and turns it into a Seq of Books by leveraging the bookReader defined at the top of the class. Finally, we make sure that all five books in our repository were returned.

Next, we're going to test the search functionality of the web service. Since we're going to be making a lot of similar calls to the web service, create this helper method in your code just below the Provide access to all books test:

def searchHelper(port:Int, params:(String, String)*) = {
  import play.api.Play.current
  val response = await(WS.url(Localhost + port + "/books/search").
    withQueryString(params:_*).get())
  response.status must equalTo(OK)

  (response.json \ "books").as[Seq[Book]]
}

This method will accept arbitrary key-value pairs as query parameters, make a GET request to the search API, and return the books from the search results.

The search tests are all very similar, so go ahead and create them now. Remember, these tests are all contained inside of "Books controller" should.

"Return search results when given an author" in new WithServer {
  val books = searchHelper(port, ("author","dickens"))
  books.size must equalTo(2)
}
"Return search results when given a title" in new WithServer {
  val books = searchHelper(port, ("title","galaxy"))
  books.size must equalTo(1)
}
"Return search results when given an author and title" in new WithServer {
  val books = searchHelper(port, ("title","miserables"),("author","hugo"))
  books.size must equalTo(1)
}
"Return an empty list when no search results found" in new WithServer {
  val books = searchHelper(port, ("author","wolfe"))
  books must be(List.empty)
}

Notice that these tests focus on calling the search API and doing simple verification. We make a call to the service, extract the books, and then run a test to make sure we got something back (or nothing, when appropriate). More in-depth logic checking of the search functions should be done at the unit testing level.

The last functionality to test in this controller is checking out and checking in books. When validating state changes we need to do the following:

  1. Prep the state for testing (this usually involves making direct changes via the repository)
  2. Run the web service call that changes the state
  3. Check that the response is what we expect
  4. Check that the internal state was actually changed

For these tests specifically, we need to (1) adjust a given book's availability, (2) make a web service call to check out/in that book, (3) inspect the JSON response, and (4) verify that the book was modified correctly. Here's how we do that in the first check out test:

"Check out books" in new WithServer {
  LibraryRepository.getBook(1).get.available = true //1

  val response = await(WS.url(Localhost + port + "/book/1/checkout")
    .post("")) //2
  response.status must equalTo(OK)
  val book = (response.json \ "book").as[Book]

  book.available must beFalse //3

  LibraryRepository.getBook(1).get.available must beFalse //4
}

There are comments in the method that correspond to each of the four steps outlined above. In step one, we access the book from the repository and make sure that it is available. You may be tempted to think this is unnecessary, but tests should be independent of other tests. It's possible that another test could modify this book, so we have to make sure it is properly prepared before testing. Step two is similar to what we have seen before, except we now make a POST instead of a GET. Step three inspects the JSON response (again, converted into a Scala object via bookReader). Finally, step four verifies that the book's state was actually changed in the database.

The test for checking in books is almost identical:

"Checkin books" in new WithServer {
  LibraryRepository.getBook(1).get.available = false

  val response = await(WS.url(Localhost + port + "/book/1/checkin")
    .post(""))
  response.status must equalTo(OK)
  val book = (response.json \ "book").as[Book]

  book.available must beTrue

  LibraryRepository.getBook(1).get.available must beTrue
}

The test is so similar to the check out test you might think it is overkill, but taking shortcuts is what often leads to bugs. Taking a few minutes to write a test that seems trivial is always worth the time.

So far, we've just tested the "happy path", but when coding the application, we had to handle situations where the user makes bad calls to the web service. We definitely want to test the unhappy paths too. The first thing that could go wrong is the user looks for a book that does not exist. Here's how we test that situation:

"Returns an error when checking in/out a nonexistent book" +
  "a book that does not exist" in new WithServer {
    await(WS.url(Localhost + port + "/book/99999/checkin")
      .post(""))
      .status must equalTo(BAD_REQUEST)

    await(WS.url(Localhost + port + "/book/99999/checkout")
      .post(""))
      .status must equalTo(BAD_REQUEST)
}

Notice that here we assert that the status code returned must be equalTo(BAD_REQUEST), i.e. HTTP 400.

The other problem that a user could encounter is trying to repeat a check out/in. To test this path, we'll check out a book, try to check it out again, check in the same book, and finally try to check it in again. After each attempt, we need to verify the response code:

"Returns an error when repeating a checkin/out" in new WithServer {
  LibraryRepository.getBook(1).get.available = true
  await(WS.url(Localhost + port + "/book/1/checkout")
    .post(""))
    .status must equalTo(OK)

  //Repeat the checkout
  await(WS.url(Localhost + port + "/book/1/checkout")
    .post(""))
    .status must equalTo(BAD_REQUEST)

  //Now check it back in
  await(WS.url(Localhost + port + "/book/1/checkin")
    .post(""))
    .status must equalTo(OK)

  //Repeat the checkin
  await(WS.url(Localhost + port + "/book/1/checkin")
    .post(""))
    .status must equalTo(BAD_REQUEST)
}

At this point, we have fully tested all of the Books controller API endpoints in a real web server. It's a good time to run sbt test again just to make sure everything is set up properly.

Customer Controller Tests

Now that you're familiar with the style of setting up controller tests, adding the tests for customers should be straightforward. Begin by creating CustomersIntegrationSpec.scala:

package controllers

import dal.LibraryRepository
import model.Customer
import play.api.libs.functional.syntax._
import play.api.libs.json.Reads._
import play.api.libs.json._
import play.api.libs.ws._
import play.api.test.{PlaySpecification, WithServer}

class CustomersIntegrationSpec extends PlaySpecification {

}

Just like with the Books controller test, we need to set up a few variables. Specifically, we need to set up a JSON parser for Customer objects and define the Localhost constant.

implicit val customerReader:Reads[Customer] = (
  (JsPath \ "id").read[Int] and
    (JsPath \ "name").read[String] and
    (JsPath \ "address").read[String]
  )(Customer.apply _)

val Localhost = "http://localhost:"

We will first add tests for retrieving a customer by ID. To cover both the happy and unhappy path, we'll write a test for a customer that we know exists and a test for a customer that we know does not exist.

"The Customers controller" should {
  "Provide access to a single customer" in new WithServer {
    val response = await(WS.url(Localhost + port + "/customer/1").get())
    response.status must equalTo(OK)

    val customer:Customer =  (response.json \ "customer").as[Customer]

    customer.name must equalTo("Bruce Wayne")
    customer.address must equalTo("1 Wayne Enterprise Ave, Gotham")
  }
  "Return an error when a" +
    "non-existent customer is requested" in new WithServer {
    val response = await(WS.url(Localhost + port + "/customer/99999").get())

    response.status must equalTo(BAD_REQUEST)
  }
}

The last route to tackle is the route that allows users to create new customers. These tests will be similar to the book check out/in tests in that we will want to inspect the internal repository to verify the change. We can verify that a new customer was created by counting the number of customers before and after making the POST request. Then we should check the values of that new customer and make sure they match the inputs. Here's the test that covers those requirements:

"Create new users" in new WithServer {
  val newCustomerName = "Joe Reeder"
  val newCustomerAddress = "123 Elm St."

  val currentCustomerCount = LibraryRepository.customers.size //1

  val response = await(WS.url(Localhost + port + "/customer/new")
    .post(Map("name"    -> Seq(newCustomerName),
              "address" -> Seq(newCustomerAddress))))

  LibraryRepository.customers.size must equalTo(currentCustomerCount + 1)  //2

  val newCustomer = LibraryRepository.customers.maxBy(c => c.id)

  newCustomer.name must equalTo(newCustomerName) //3
  newCustomer.address must equalTo(newCustomerAddress) //3
}

Step one above simply stores the number of customers currently in the repository, then we execute the POST, and in step two we verify that the customers increased by one. In step three, we actually inspect the values to make sure they match the parameters included in the request.

You may have noticed that the code to make a POST request is a bit more complicated than the request to update a book. That's because we actually pass parameters in the query as opposed to in the URL. The API that Play uses to do this is slightly confusing because a Seq is required even when you only have one value. Ignoring that, you can see that we map "name" and "address" to their respective values.

The last test to write will verify the JSON response that we get when creating a new customer. Our code should give the web service user a JSON representation of their newly created user. This is good practice, especially since the user will likely be interested in getting the ID of the customer that was just created. To test that, we will use the customerReader to convert the JSON of the response to a Customer object:

"Return the newly created user" in new WithServer {
  val newCustomerName = "Joe Reeder"
  val newCustomerAddress = "123 Elm St."

  val response = await(WS.url(Localhost + port + "/customer/new")
    .post(Map("name"    -> Seq(newCustomerName),
              "address" -> Seq(newCustomerAddress))))

  response.status must equalTo(CREATED)
  val newCustomer = (response.json \ "customer").as[Customer]

  newCustomer.name must equalTo(newCustomerName)
  newCustomer.address must equalTo(newCustomerAddress)
  newCustomer.id must greaterThan(3)
}

You'll also notice that this status should be 201 "CREATED" unlike the others which are usually 200 "OK". There is also a final check at the end to make sure the ID of the customer is something greater than three. This is a good sanity check because we know our repository should have three customers before running the test suite.

You should run sbt test once again to see the satisfying output of 14 passing tests!

Conclusion

We covered a lot of ground in this tutorial, so don't feel bad if you need to review some sections, especially if you are new to the Play Framework. After working through this tutorial, you should be able to:

  • Create a basic web service in Play using a variety of GET and POST routes
  • Translate Scala objects into JSON and vice-versa using Play's JSON API
  • Write HTTP integration tests that:
    • Use the vocabulary of behavioral tests
    • Make actual web service calls to your API
    • Validate web service responses (both happy and unhappy paths)
    • Verify state changes when applicable

If you would like some more practice, here are some ideas you could implement in this project:

  • Add tests that verify the messages included in the responses.
  • Include an optional parameter for the book search routes to limit the number of results returned. Add tests to make sure the parameter is truly optional and that when it is supplied, the results are appropriately limited. You might want to add some more books to the repository to help with this.
  • Modify the check out/in functions to include the customer and add tests to check your implementation.
8f2ca1930f47d542eecc702a9577ef58
Phillip Johnson

Phillip Johnson is a software developer based in Columbus, Ohio. He blogs at Let's Talk Data and tweets @phillipcjohnson.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.