This is my first meaningful blog post and is inspired by This Stack Overflow question. I answered the question but wasn’t entirely happy with it (and apparently neither was the community!). So I decided to look at the problem in more depth as I expect to use Akka Http in the future, and mocking is an important part of testing components which connect to external services.
In this case, the client is to connect to the Police UK Data service for street level crimes, chosen because it is a freely available data API, and fairly interesting, so it’s a reasonable target for a programmatic client.
The API accepts URLs such as:
/api/crimes-street/all-crime?lat=52.62&lng=-1.13&date=2013-01
And returns JSON such as
[
{
"category":"anti-social-behaviour",
"location_type":"Force",
"location":{
"latitude":"52.624424",
"street":{
"id":882275,
"name":"On or near Stuart Street"
},
"longitude":"-1.150485"
},
"context":"",
"outcome_status":null,
"persistent_id":"",
"id":20597953,
"location_subtype":"",
"month":"2013-01"
},
{
"category":"burglary",
"location_type":"Force",
"location":{
"latitude":"52.632832",
"street":{
"id":883513,
"name":"On or near Dane Street"
},
"longitude":"-1.148312"
},
"context":"",
"outcome_status":{
"category":"Investigation complete; no suspect identified",
"date":"2013-01"
},
"persistent_id":"80e81a544ebfb96de7527e94d1e8498feca479607d3e2ab303a96a4694fd2ce0",
"id":20600200,
"location_subtype":"",
"month":"2013-01"
}
]
The output is a list of ‘Crime’ objects. For simplicity the client does not attempt to model the entire set of fields per Crime, but aims to populate the following case class:
case class Crime(category: String,
street: String,
outcome: Option[String])
The full deserialization code (which uses Spray JSON) is not shown here since it is not the focus of this post, but the full source is available on Github.
trait CrimeJsonProtocol extends DefaultJsonProtocol {
implicit object crimeFormat extends JsonFormat[Crime] {
...
}
}
In order to loosely couple the Http logic from the business logic, the Cake Pattern is used. First we have an Http Service trait:
trait HttpRequestService {
def makeRequest(uri: String): Future[String]
}
Since this is just a simple example, the makeRequest
method takes a String
URI
and returns a Future
containing the response text from the server, also as a String
.
Since a future can complete with Success
or Failure
,
it can represent any invalid HTTP responses
as failures. In a more complex scenario, a richer method signature would probably
be required, for example allowing the HTTP Method to be set, and allowing content to
be provided e.g. for PUT and POST requests. And it would make sense to return some
model object representing the Http response rather than just a String
. However,
it is advisable not to design the signature to return the HttpResponse
object from
a particular HTTP library, such as akka.http.scaladsl.model.HttpResponse
, since
this makes it harder to switch out one HttpRequestService
for another based
on a different HTTP Client library, and some of the benefit of the Cake Pattern would
be lost.
Next, we create a Trait representing the service implementing the API specific logic:
trait PoliceUKDataService extends CrimeJsonProtocol {
this: HttpRequestService =>
implicit def ec: ExecutionContext
def getData(latitude: Double, longitude: Double, month: String): Future[Seq[Crime]] = {
val uri = s"https://data.police.uk/api/crimes-street/all-crime?lat=${latitude}&lng=${longitude}&date=${month}"
makeRequest(uri).map(_.parseJson.convertTo[List[Crime]])
}
}
The following line is the essence of the Cake Pattern:
this: HttpRequestService =>
It is called a “Self-type annotation” and it means that any class or object
which extends the PoliceUKDataService
trait must also extend the
HttpRequestService
trait (or a sub-trait of it). Essentially it dictates that
the PoliceUKDataService
must always have access to the behaviour of an
HttpRequestService
and an attempt to use PoliceUKDataService
without mixing
in an HttpRequestService
would cause a compile time error.
The implicit ExecutionContext
is necessary since we are mapping the result of
the Future
returned from makeRequest
. Any operation with a Future
requires one.
It is desirable to unit test the getData
method of PoliceUKDataService
without calling out to the Police Service API. Being able to do so is the
reason for this post. It is possible to go ahead and implement tests for it even before
having any working implementation of an HttpRequestService
.
Here, ScalaMock is used to create a mock of the makeRequest
method.
import org.scalamock.scalatest.MockFactory
import org.scalatest.{Matchers, WordSpec}
import scala.concurrent._
import scala.concurrent.duration._
class PoliceUKDataClientSpec extends WordSpec
with Matchers with MockFactory {
trait MockHttpRequestService
extends HttpRequestService {
val mock =
mockFunction[String, Future[String]]
override def makeRequest(uri: String): Future[String] =
mock(uri)
}
"PoliceUKDataClient" should {
"Return a list of crimes from a uri" in {
val client = new PoliceUKDataService
with MockHttpRequestService {
override def ec =
scala.concurrent.ExecutionContext.Implicits.global
}
client.mock
.expects("https://data.police.uk/api/crimes-street/all-crime?lat=51.5&lng=0.13&date=2013-01")
.returning(Future.successful(
"""
|[
| {
| "category":"anti-social-behaviour",
| "location_type":"Force",
| "location":{
| "latitude":"52.624424",
| "street":{"id":882275,"name":"On or near Stuart Street"},
| "longitude":"-1.150485"
| },"context":"",
| "outcome_status":null,
| "persistent_id":"",
| "id":20597953,
| "location_subtype":"",
| "month":"2013-01"
| },
| {
| "category":"burglary",
| "location_type":"Force",
| "location":{
| "latitude":"52.627058",
| "street":{"id":882207,"name":"On or near Beaconsfield Road"},
| "longitude":"-1.154260"
| },
| "context":"",
| "outcome_status":{"category":"Investigation complete; no suspect identified","date":"2013-03"},
| "persistent_id":"",
| "id":20600818,
| "location_subtype":"",
| "month":"2013-01"
| }
|]
""".stripMargin))
val crimesF = client.getData(51.5, 0.13, "2013-01")
val crimes = Await.result(crimesF, 1 second)
crimes should be (
List(
Crime("anti-social-behaviour",
"On or near Stuart Street", None ),
Crime("burglary",
"On or near Beaconsfield Road",
Some("Investigation complete; no suspect identified"))))
}
}
}
By implementing makeRequest
as a mock, we can dictate what it returns for a given
input. Simple! Further tests e.g. for failure cases could be implemented in the same
way.
Now we have confidence in the correctness of our API Service, we can implement our client application using Akka Http. First an implementation of the Http Service:
trait AkkaHttpRequestService extends HttpRequestService {
implicit def system: ActorSystem
implicit def materializer: ActorMaterializer
implicit def ec: ExecutionContext
lazy val http = Http()
def makeRequest(uri: String): Future[String] = {
println(uri)
val req = HttpRequest(HttpMethods.GET, uri)
val resp = http.singleRequest(req)
resp.flatMap {r => println(r); Unmarshal(r.entity).to[String] }
}
def shutdown() = {
Http().shutdownAllConnectionPools().onComplete{ _ =>
system.shutdown()
system.awaitTermination()
}
}
}
Then we mix the API service and Akka Http Service together to build a client:
object PoliceUKDataClient extends App {
val client = new PoliceUKDataService
with AkkaHttpRequestService {
override implicit val system =
ActorSystem("PoliceUKData")
override implicit val materializer =
ActorMaterializer.create(system)
override implicit val ec: ExecutionContext =
system.dispatcher
}
implicit val ec = client.ec
val (lat, lon, date) = (args(0).toDouble, args(1).toDouble, args(2))
client.getData(lat, lon, date).onComplete { res =>
res match {
case Success(list) => list.foreach(println)
case Failure(ex) => println(ex.getMessage)
}
client.shutdown()
}
}
The full source code for this post is available on Github.
Comments and corrections always welcome.