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 new IOSystem 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 a Map[String, JValue] and is tested for equality against a given Map[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))
}
The source code for this page can be found here.