Testing
Colossus comes with a testkit that contains some helper classes and functions that simplifying the testing of a colossus service. Testkit is built on ScalaTest. Add colossus-testkit
to your dependencies, using the same version as colossus itself. For example:
libraryDependencies += "com.tumblr" %% "colossus-testkit" % "VERSION" % Test
Testing Callbacks
CallbackAwait works similarly to Scala’s Await
for futures.
// callback executor is where the callback runs and also required an actor system to be in scope
implicit val callbackExecutor: CallbackExecutor = FakeIOSystem.testExecutor
val callbackUnderTest: Callback[Int] = Callback.complete(Try(42))
val result = CallbackAwait.result(callbackUnderTest, 1.second)
assert(result == 42)
Testing any Service
ServiceSpec abstract class provides helper functions to test expected responses. To use ServerSpec
an implementation of the service
method needs to be provided. In this example we are testing want to test this redis handler, but it could be any protocol:
class MyRequestHandler(context: ServerContext, db: ConcurrentHashMap[String, String]) extends RequestHandler(context) {
override def handle: PartialHandler[Redis] = {
case Command("GET", args) =>
args match {
case head +: _ =>
Option(db.get(head.utf8String)) match {
case Some(value) => Callback.successful(BulkReply(ByteString(value)))
case None => Callback.successful(NilReply)
}
case Nil =>
Callback.successful(ErrorReply("ERR wrong number of arguments for 'get' command"))
}
case Command("SET", args) =>
args match {
case key +: value +: _ =>
db.put(key.utf8String, value.utf8String)
Callback.successful(StatusReply("OK"))
case Nil =>
Callback.successful(ErrorReply("ERR wrong number of arguments for 'set' command"))
}
}
}
Unit tests could look like:
class AnyServiceSpec extends ServiceSpec[Redis] {
val fakeDd = new ConcurrentHashMap[String, String]()
override def service: ServerRef = {
RedisServer.start("example-server", 9123) { initContext =>
new Initializer(initContext) {
override def onConnect: ServerContext => MyRequestHandler = { serverContext =>
new MyRequestHandler(serverContext, fakeDd)
}
}
}
}
"My request handler" must {
"set can set a key value" in {
val request = Command("SET", "name", "ben")
val response = StatusReply("OK")
assert(fakeDd.isEmpty)
expectResponse(request, response)
assert(fakeDd.size() == 1)
}
"get can get a value" in {
val request = Command("GET", "name")
val response = BulkReply(ByteString("ben"))
expectResponse(request, response)
}
"get return nil when no value" in {
val request = Command("GET", "age")
expectResponseType[NilReply.type](request)
}
}
}
ServerSpec
extends ColossusSpec
, which provides the ability to spin up a server/iosystem. These functions can be used directly for more flexible testing needs:
withIOSystem
will spin up a newIOSystem
for the duration of a test and shut it down at the end.withServer
will shutdown the given server after the test completes.
Testing HTTP Service
HttpServiceSpec
adds to ServerSpec
some HTTP-specific utility functions for asserting that a HTTP handler behaves correctly. To illustrate, let’s test this request handler:
class MyHandler(context: ServerContext) extends RequestHandler(context) {
override def handle: PartialHandler[Http] = {
case request @ Get on Root / "ping" =>
Callback.successful(request.ok("pong"))
case request @ Get on Root / "ping" / data =>
Callback.successful(request.ok(s"""{"type":"pong","data":$data}""").withContentType(ContentType.ApplicationJson))
}
}
And now, the HttpServiceSpec
-based test class:
class MyHttpHandlerSpec extends HttpServiceSpec {
implicit val formats: Formats = org.json4s.DefaultFormats
override def service: ServerRef = {
HttpServer.start("example-server", 9123) { initContext =>
new Initializer(initContext) {
override def onConnect: ServerContext => MyHandler = { serverContext =>
new MyHandler(serverContext)
}
}
}
}
"My request handler" must {
"return 200 and correct body" in {
expectCodeAndBody(HttpRequest.get("ping"), HttpCodes.OK, "pong")
}
"return 200 and body that satisfies predicate" in {
expectCodeAndBodyPredicate(HttpRequest.get("ping/1"), HttpCodes.OK) { body =>
val actual = JsonMethods.parse(body).extract[Map[String, JValue]]
val expected = Map("data" -> JInt(1), "type" -> JString("pong"))
actual == expected
}
}
}
}
expectCodeAndBody
will hit the service instance with a request, and match the returned code and body with a given expected code and body.expectCodeAndBodyPredicate
takes a predicate that the body must satisfy. In the example, the returned JSON body is parsed into aMap[String, JValue]
and is tested for equality against a givenMap[String, JValue]
instead.
Testing Clients
Colossus clients should be passed into the request handler so they can be mocked in tests. For example, lets test this code:
class AnyClientHandler(context: ServerContext,
redisClient: RedisClient[Callback],
memcacheClient: MemcacheClient[Callback])
extends RequestHandler(context) {
override def handle: PartialHandler[Http] = {
case request @ Get on Root / "data" / name =>
redisClient
.get(ByteString(name))
.flatMap {
case Some(value) =>
Callback.successful(request.ok(s"The key $name has value ${value.utf8String}"))
case None =>
memcacheClient.get(ByteString(name)).map {
case Some(Value(_, data, _)) =>
request.ok(s"The key $name has value ${data.utf8String}")
case None =>
request.ok(s"The key $name has no value")
}
}
}
}
MockSender
functionality can be used in the following manner:
class AnyClientSpec extends HttpServiceSpec {
val basicRedis: Command => Callback[Reply] = MockSender.mockResponse[Redis](
Map(
Command("GET", "ben") -> Success(BulkReply(ByteString("1"))),
Command("GET", "bob") -> Success(NilReply),
Command("GET", "jen") -> Success(NilReply)
)
)
val basicMemcache: MemcacheCommand => Callback[MemcacheReply] = MockSender.mockResponse[Memcache](
Map(
MemcacheCommand.Get(ByteString("bob")) -> Success(MemcacheReply.Value(ByteString("ben"), ByteString("2"), 0)),
MemcacheCommand.Get(ByteString("jen")) -> Success(MemcacheReply.NoData)
)
)
override def service: ServerRef = {
HttpServer.start("example", 9123) { initContext =>
implicit val workerRef: WorkerRef = initContext.worker
val redis = Redis.client(MockSender[Redis, Callback](basicRedis))
val memcache = Memcache.client(MockSender[Memcache, Callback](basicMemcache))
new Initializer(initContext) {
override def onConnect: ServerContext => AnyClientHandler = { serverContext =>
new AnyClientHandler(serverContext, redis, memcache)
}
}
}
}
"Any client handler" must {
"return valid redis value" in {
expectCodeAndBody(HttpRequest.get("/data/ben"), HttpCodes.OK, "The key ben has value 1")
}
"return valid memcache value" in {
expectCodeAndBody(HttpRequest.get("/data/bob"), HttpCodes.OK, "The key bob has value 2")
}
"return no value for unknown key" in {
expectCodeAndBody(HttpRequest.get("/data/jen"), HttpCodes.OK, "The key jen has no value")
}
}
}
Alternatively, using mockito, the clients can be mocked:
class AnyClientSpec2 extends HttpServiceSpec with MockitoSugar {
val redis: RedisClient[Callback] = mock[RedisClient[Callback]]
val memcache: MemcacheClient[Callback] = mock[MemcacheClient[Callback]]
override def service: ServerRef = {
HttpServer.start("example", 9123) { initContext =>
implicit val workerRef: WorkerRef = initContext.worker
new Initializer(initContext) {
override def onConnect: ServerContext => AnyClientHandler = { serverContext =>
new AnyClientHandler(serverContext, redis, memcache)
}
}
}
}
"Any client handler" must {
"return valid redis value" in {
when(redis.get(ByteString("ben"))).thenReturn(Callback.successful(Some(ByteString("1"))))
expectCodeAndBody(HttpRequest.get("/data/ben"), HttpCodes.OK, "The key ben has value 1")
}
"return valid memcache value" in {
when(redis.get(ByteString("bob"))).thenReturn(Callback.successful(None))
when(memcache.get(ByteString("bob"))).thenReturn(
Callback.successful(Some(MemcacheReply.Value(ByteString("ben"), ByteString("2"), 0)))
)
expectCodeAndBody(HttpRequest.get("/data/bob"), HttpCodes.OK, "The key bob has value 2")
}
"return no value for unknown key" in {
when(redis.get(ByteString("jen"))).thenReturn(Callback.successful(None))
when(memcache.get(ByteString("jen"))).thenReturn(Callback.successful(None))
expectCodeAndBody(HttpRequest.get("/data/jen"), HttpCodes.OK, "The key jen has no value")
}
}
}
Testing Metrics
At its core, metrics are just a collection of maps containing strings and numbers. To test metrics, get the map and make sure it contains what you expect. Let’s say we have this code:
class SimpleMetricController()(implicit mn: MetricNamespace) {
private val hellos = Counter("hellos")
def sayHello(request: HttpRequest): HttpResponse = {
val color = request.head.parameters.getFirst("color").getOrElse("NA")
hellos.increment(Map("color" -> color))
request.ok("Hello!")
}
}
The test could be written like this:
"say hello" in {
implicit val metricContext: MetricContext = MetricContext("/", Collection.withReferenceConf(Seq(1.minute)))
val controller = new SimpleMetricController()
val response = controller.sayHello(HttpRequest.get("/hello?color=red"))
assert(response == HttpResponse.ok("Hello!"))
val metrics = metricContext.collection.tick(1.minute)
assert(metrics(MetricAddress.Root / "hellos") === Map(Map("color" -> "red") -> 1))
}