Building a Simple REST API with Scala & Play! (Part 3)

In this 3 part series, we’ll cover creating a basic Play! REST API on top of Reactive Mongo. Full source code for this tutorial is available at https://github.com/jrodenbostel/getting-started-play-scala.

Welcome back!

We finished parts 1 & 2, which started with a description of the tools we’ll be using, and concluded with a fully functioning REST API built in Play! on top of a Reactive Mongo back-end. In part 3, we’ll cover the use of Spec2 and Mockito to write automated unit and integration tests for our application.

Integration Testing

Spec2 provides a DSL for BDD-style test specs. One variation of these specs, WithBrowser, actually runs the code in a headless browser, allowing you to end-to-end test your application in automated fashion. To get started, open the ‘/test/IntegrationSpec.scala’ file, which was created as part of our seed project. Update it to include the following:

import org.junit.runner.RunWith
import org.specs2.mutable.Specification
import org.specs2.runner.JUnitRunner
import play.api.test.WithBrowser

@RunWith(classOf[JUnitRunner])
class IntegrationSpec extends Specification {

  "Application" should {

    "work from within a browser" in new WithBrowser {

      browser.goTo("http://localhost:" + port)

      browser.pageSource must contain("Your database is ready.")
    }

    "remove data through the browser" in new WithBrowser {

      browser.goTo("http://localhost:" + port + "/cleanup")

      browser.pageSource must contain("Your database is clean.")
    }
  }
}

You’ll notice a spec for each Application Controller function, which simply visits the relevant URI and matches the response string. Tests can be executed from the Activator UI or by running ‘activator test’ from the command line from within your project folder.

Unit Testing

Unit testing is a bit more complex because we’ll test each controller by mocking the relevant repository method. Assertions for unit tests are more straightforward, as in most cases, we’re simply inspecting HTTP status codes. Update the ‘test/ApplicationSpec.scala’ file to include the following:

import controllers.{routes, Widgets}
import org.junit.runner.RunWith
import org.specs2.mock.Mockito
import org.specs2.mutable.Specification
import org.specs2.runner.JUnitRunner
import play.api.libs.json.{JsArray, Json}
import play.api.mvc.{Result, _}
import play.api.test.Helpers._
import play.api.test.{WithApplication, _}
import play.modules.reactivemongo.ReactiveMongoApi
import reactivemongo.api.commands.LastError
import reactivemongo.bson.BSONDocument
import repos.WidgetRepoImpl

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{ExecutionContext, Future}

@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification with Results with Mockito {

  val mockRecipeRepo = mock[WidgetRepoImpl]
  val reactiveMongoApi = mock[ReactiveMongoApi]
  val documentId = "56a0ddb6c70000c700344254"
  val lastRequestStatus = new LastError(true, None, None, None, 0, None, false, None, None, false, None, None)

  val oatmealStout = Json.obj(
        "name" -> "Widget One",
        "description" -> "My first widget",
        "author" -> "Justin"
      )

  val posts = List(
    oatmealStout,
    Json.obj(
      "name" -> "Widget Two: The Return",
      "description" -> "My second widget",
      "author" -> "Justin"
    ))

  class TestController() extends Widgets(reactiveMongoApi) {
    override def widgetRepo: WidgetRepoImpl = mockRecipeRepo
  }

  val controller = new TestController()

  "Application" should {

    "send 404 on a bad request" in {
      new WithApplication() {
        route(FakeRequest(GET, "/boum")) must beSome.which(status(_) == NOT_FOUND)
      }
    }

    "Recipes#delete" should {
      "remove recipe" in {
        mockRecipeRepo.remove(any[BSONDocument])(any[ExecutionContext]) returns Future(lastRequestStatus)

        val result: Future[Result] = controller.delete(documentId).apply(FakeRequest())

        status(result) must be equalTo ACCEPTED
        there was one(mockRecipeRepo).remove(any[BSONDocument])(any[ExecutionContext])
      }
    }

    "Recipes#list" should {
      "list recipes" in {
        mockRecipeRepo.find()(any[ExecutionContext]) returns Future(posts)

        val result: Future[Result] = controller.index().apply(FakeRequest())

        contentAsJson(result) must be equalTo JsArray(posts)
        there was one(mockRecipeRepo).find()(any[ExecutionContext])
      }
    }

    "Recipes#read" should {
      "read recipe" in {
        mockRecipeRepo.select(any[BSONDocument])(any[ExecutionContext]) returns Future(Option(oatmealStout))

        val result: Future[Result] = controller.read(documentId).apply(FakeRequest())

        contentAsJson(result) must be equalTo oatmealStout
        there was one(mockRecipeRepo).select(any[BSONDocument])(any[ExecutionContext])
      }
    }

    "Recipes#create" should {
      "create recipe" in {
        mockRecipeRepo.save(any[BSONDocument])(any[ExecutionContext]) returns Future(lastRequestStatus)

        val request = FakeRequest().withBody(oatmealStout)
        val result: Future[Result] = controller.create()(request)

        status(result) must be equalTo CREATED
        there was one(mockRecipeRepo).save(any[BSONDocument])(any[ExecutionContext])
      }
    }

    "Recipes#update" should {
      "update recipe" in {
        mockRecipeRepo.update(any[BSONDocument], any[BSONDocument])(any[ExecutionContext]) returns Future(lastRequestStatus)

        val request = FakeRequest().withBody(oatmealStout)
        val result: Future[Result] = controller.update(documentId)(request)

        status(result) must be equalTo ACCEPTED
        there was one(mockRecipeRepo).update(any[BSONDocument], any[BSONDocument])(any[ExecutionContext])
      }
    }
  }
}

You’ll notice the class starts with the creation of Mocks and example data – this is very straightforward and was pulled from examples of real BSON data from Mongo. In each spec, you’ll also notice the same pattern: mocks are configured, methods are exercised, and assertions are made. Very similar to other BDD-style test frameworks like Jasmine and Rspec.

Conclusion

Reactive programming and related frameworks, especially those of the functional variety, represent a significant shift in the way we write applications. There are many new patterns, tools, and programming styles that a developer must become familiar with in order to write applications in the reactive style effectively. Many of us, myself included, are just starting to get opportunities to do so. Although these tools may not be appropriate for every solution, becoming familiar with the underlying concepts will help individuals become more well-rounded as developers and help ensure that scalable, resilient, responsive architectures are adopted when necessary.

Advertisements

2 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s