Integration Testing HTTP Requests With Scala and Betamax

- - Scala, Testing

At Sharethrough, we TDD everything. When investigating a new language, tool or library, included in our initial analysis is “How testable is it?” and/or “How testable can we make it?” We’re currently utilizing Typesafe’s Play (via Scala) for our service layer. In order to gain experience with various testing strategies in Scala, we looked for API-testing approaches that went beyond mocking or injecting HTTP requests, and then released a small working project.

When transitioning from Ruby to Scala, what you’ll first notice is that your old testing toolset no longer applies (e.g. no more Foo.stub(:new)) or that you may have developed some bad habits (e.g. testing private methods). What you may also notice is a lack of some terrifically useful libraries. Having made use of the incredible VCR gem, we set out to find something similar in the Java / Scala ecosystem and came across Betamax.

Betamax works similarly to VCR - you inject “tapes” which it then uses to record and play back HTTP interactions. These interactions are played back based on configurable attributes in the request. It also features a terrifically simple annotation-based API and makes use of the Java http.* system properties so there’s no configuration to be concerned with in your test suite, though it is eminently configurable.

The Journey

First things first, let’s get those dependencies set up:

"co.freeside" % "betamax" % "1.1.2" % "test"
"org.codehaus.groovy" % "groovy-all" % "1.8.8" % "test"

In the current release of Betamax, the Groovy libaries aren’t in the POM so you’ll need to add that second line. Rob (Betamax maintainer) will be adding that in the next release so by the time you read this, it may not be necessary.

Next, for reasons I haven’t ascertained, using the @Rule and @Betamax annotations in Scala doesn’t actually work. From my limited reading on the matter I understand annotations in Scala to be just different enough from Java as to be a source of mild annoyance (though the JUnit @Test annotation works if you prefer to use JUnit in Scala; see the links far below).

Implementation

Rob’s advice was to utilize the Betamax library directly, rather than relying on the annotation helpers. An example follows.

1
2
3
4
5
6
7
8
9
10
val recorder = new Recorder
val proxyServer = new ProxyServer(recorder)

recorder.insertTape("YouTubeVideo.apply")
proxyServer.start()

YouTubeVideo("5hWIr9_noRo")

recorder.ejectTape()
proxyServer.stop()

In this example, YouTubeVideo.apply is the name of the ‘tape’ that the proxy server will use to read and respond to your HTTP requests. It matches your HTTP requests against the requests ‘recorded’ in the tape and ‘plays back’ the response, storing this as a YAML file under test/resources. I chose the name of the tape as ${ClassName}.${MethodName}, though I’m sure that naming strategy will evolve.

Upon additional invocations of your test(s), Betamax will match the HTTP request to those in the tapes and play back the stored responses. If you’d like to re-record a different response, you’ll need to delete the YAML file and run your tests again.

SBT and Specs2 - Parallelism

So here we are - Betamax is up and running for one spec. What happens when you add more than one? Since Betamax starts a proxy server (at localhost:5555 by default), you can imagine what happens if you attempt to run Betamax-based specifications in parallel - the port is obviously in use and cannot be bound to twice.

We’ve dealt with similar issues in testing our Scalding jobs with Spec2, and the answer is to disable this parallelism in SBT.

1
parallelExecution in Test := false

Now there are other approaches one might consider, for example, Betamax-based specs blocking on the availability of a proxy server, etc. If your integration suite is part of a large(r) suite then perhaps contributing back to Betamax would be a good idea - improving its integration with Specs2 or its API to better support parallel specs in Scala :)

That All Seems A Bit Messy

With Scala, we can definitely do better. First, refactor that boilerplate into a helper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def withTape(tapeName:String, functionUnderTest:() => Any) = {
  synchronized {
    val recorder = new Recorder
    val proxyServer = new ProxyServer(recorder)

    recorder.insertTape(tapeName)
    proxyServer.start()

    try {
      functionUnderTest()
    } finally {
      recorder.ejectTape()
      proxyServer.stop()
    }
  }
}

Note that the try...finally is necessary in case your specs fail - the proxy server needs to be shutdown :) Second, make use of the helper.

1
2
3
4
5
6
7
8
9
10
11
12
13
".apply" should {
  "fetch and parse JSON from the Twitter endpoint" in {
    val url = "http://www.buzzfeed.com/despicableme2/15-reasons-we-wish-we-were-steve-carell/"

    var tw:Twitter = null
    BetamaxHelper.withTape("Twitter.apply", () => {
      tw = Twitter(url)
    })

    tw.url    must_== url
    tw.tweets must_== 29
  }
}

Third, now that we’re in a helper (a single source of knowledge about Betamax), we can synchronize access to it and remove the need to run all of our tests sequentially (i.e. remove the parallelExecution rule in your build.sbt).

As always, Sharethrough is hiring! If you’re passionate about writing quality code and interested in using Scala (or Rails, or Scalding, or Chef, etc.) drop us a line!

Appendix

During my research, some Scala testing related tidbits you may find useful or interesting:

Comments