Unit testing is often discussed in software development. When it comes to writing functions, we tend to think about how we can integrate that function in the entry point of the application. When it comes to unit testing that function, having all branch coverage inside that function can be hard and tricky.
Akka HTTP has a helpful testkit to test routes that make unit testing simple. Testing without TCP connection should not be complicated. You can mock the IO function, such as singleRequest
. The key to a readable and accessible test suite lies in how you structure the functions.
In this tutorial, I want to share how you can structure the Http singleRequest
function in Akka HTTP so that it is easier to mock and get 100% branch coverage.
The Problem
In this tutorial, I use a fake response of a simple dummy rest-client which retrieves dummy employee information. The task is to create a client service that gets the employee information through Http SingleRequest and parse all the fields in the response. Usually, we write a function like this:
def getEmployees(url: String): Future[List[Employee]] = {
for {
response <- http().singleRequest(HttpRequest(uri = url))
employeeString <- Unmarshal(response.entity).to[String]
employeeObject <- parser.decode[List[Employee]](employeeString) match {
case Right(employeeObj) => Future(employeeObj)
case Left(err) => Future.failed(err)
}
} yield { employeeObject }
The function creates a fetch call through http().singleRequest
and deserialize the JSON string to an Employee model class with Circe.
This function becomes hard for unit testing because the IO function coupled with other operations in the function.
The Intuition
To make the application as loosely coupled as possible. We can wrap the IO function into a trait so that the functionality can be overridden or mocked.
In this case, http().singleRequest
is the component that creates IO. Therefore, we can extract the functionality out and wrap it into a trait.
trait HttpClient {
def sendRequest(httpRequest: HttpRequest)(implicit actorSystem: ActorSystem): Future[HttpResponse]
}
Because we extracted the singleRequest
as an abstract function, we can mock the function in the unit test with Scalamock.
Order of Execution
We can write the regular implementation first; then we can refactor the function by extracting the http().singleRequest
.
We will use Circe to encode and decode JSON String, and Scalamock to mock the IO functions.
Let’s create a domain model Employee:
case class Employee(id: String, employeeName: String, employeeSalary: String, employeeAge: String, profileImage: String)
Once we create a domain model, let’s create the main class. Writing the main class helps you design how you want to write the function. It also helps to understand how another caller uses your function. Therefore, the main class is a simple way to test the function.
object EmployeeRestClient extends App {
implicit def actorSystem: ActorSystem = ActorSystem()
implicit val executionContext: ExecutionContext = actorSystem.dispatcher
val newRestClient = new EmployeeRestClient() with ClientHandler {
override implicit def actorSystem: ActorSystem = ActorSystem()
override implicit def executionContext: ExecutionContext = actorSystem.dispatcher
}
newRestClient.getEmployees("http://dummy.restapiexample.com/api/v1/employees").onComplete { res =>
res match {
case Success(employees) => employees.map(println)
case Failure(exception) => println(s"Failed to fetch ... ${exception.getMessage}")
}
newRestClient.shutDown()
}
}
Inside this function, we extract out the fetch endpoint URL by wrapping it with the getEmployee
function.
newRestClient.getEmployees("http://dummy.restapiexample.com/api/v1/employees").onComplete { res =>
res match {
case Success(employees) => employees.map(println)
case Failure(exception) => println(s"Failed to fetch ... ${exception.getMessage}")
}
def getEmployees(url: String): Future[List[Employee]] = {
for {
response <- http().singleRequest(HttpRequest(uri = url))
employeeString <- Unmarshal(response.entity).to[String]
employeeObject <- parser.decode[List[Employee]](employeeString) match {
case Right(employeeObj) => Future(employeeObj)
case Left(err) => Future.failed(err)
}
} yield { employeeObject }
}
The Refactor
Let’s refactor the code so that handleRequest
can be mock.
How?
Let’s create a trait, HttpClient, that has an abstract function sendRequest
. Then, let’s mock sendRequest
later in the unit test.
trait HttpClient {
def sendRequest(httpRequest: HttpRequest)(implicit actorSystem: ActorSystem): Future[HttpResponse]
}
Then, let’s use self-type annotation in the EmployeeRestClient
trait so that any instantiation or any class that extends the EmployeeRestClient
also need to extend the HttpClient trait. This is very useful for later when we instantiate the service.
trait EmployeeRestClient { this: HttpClient =>
implicit def actorSystem: ActorSystem
implicit def executionContext: ExecutionContext
def getEmployees(url: String): Future[List[Employee]] = {
for {
response <- sendRequest(HttpRequest(uri = url))
employeeString <- Unmarshal(response.entity).to[String]
employeeObject <- parser.decode[List[Employee]](employeeString) match {
case Right(employeeObj) => Future(employeeObj)
case Left(err) => Future.failed(err)
}
} yield { employeeObject }
}
}
Then, we create ClientHandler
trait which contains the http().singleRequest
Function. We will not test this trait, but this trait will be mocked in the unit test later.
trait ClientHandler extends HttpClient {
override def sendRequest(httpRequest: HttpRequest)(implicit actorSystem: ActorSystem): Future[HttpResponse] = {
Http().singleRequest(httpRequest)
}
def shutDown()(implicit actorSystem: ActorSystem): Unit = {
Http().shutdownAllConnectionPools()
}
}
This refactor separated JSON deserialization and singleRequest
. Then, we can test EmployeeRestClient
trait getEmployees
, by mocking the function on sendRequest
.
Testing
Let’s set up all the mocks.
Since we extract httpRequest
, we can create another trait that also extends HttpClient
and mock the function.
trait MockClientHandler extends HttpClient {
val mock = mockFunction[HttpRequest, Future[HttpResponse]]
override def sendRequest(httpRequest: HttpRequest)(implicit actorSystem: ActorSystem): Future[HttpResponse] =
mock(httpRequest)
}
You can specify the expected input and returns on the function.
Scalamock automatically mock the function for you. Therefore, no side effect or IO involved in running the getEmployee
.
employeeRestClient.mock
.expects(HttpRequest(uri = "http://dummy.restapiexample.com/api/v1/employees"))
.returning(Future.successful(HttpResponse(entity = HttpEntity(ByteString(stripString)))))
Once we mocked the function, we start setting up the test suite. The test suite involves a setup environment, mock expectation, and assertion. Once we create an expectation for the function in ScalaMock, it also creates an assertion. If there is a discrepancy between the function call and the expectation, that test suite fails.
Did you notice that the test suite is precisely like the Main function that we defined earlier?
Starting from the Main function makes it easy to construct the test suite.
The difference between the main and the test suite lies in the different ClientHandler
traits. In main, we extend EmployeeRestClient
with ClientHandler
. In the test suite, we mock the ClientHandler
.
By doing this, we can test getEmployee
functionality because we wrap the http().singleRequest
separately into another function.
"Employee Rest Client" should {
"work on HttpClient" in {
val stripString =
"""
|[{
|"id": "1",
|"employee_name": "Salome",
|"employee_salary": "457",
|"employee_age": "35",
|"profile_image": ""
|},
|{
|"id": "34",
|"employee_name": "ABC",
|"employee_salary": "666",
|"employee_age": "50",
|"profile_image": ""
|}]
|""".stripMargin
// mock Http
val employeeRestClient = new EmployeeRestClient with MockClientHandler {
override implicit def actorSystem: ActorSystem = system
override implicit def executionContext: ExecutionContext = ExecutionContext.Implicits.global
}
employeeRestClient.mock
.expects(HttpRequest(uri = "http://dummy.restapiexample.com/api/v1/employees"))
.returning(Future.successful(HttpResponse(entity = HttpEntity(ByteString(stripString)))))
val expectResult = parser.decode[List[Employee]](stripString) match {
case Right(employees) => employees
case Left(_) => throw new Exception
}
whenReady(employeeRestClient.getEmployees("http://dummy.restapiexample.com/api/v1/employees")) { res =>
res must equal(expectResult)
}
}
}
In Conclusion:
- Design your function from the main class
- Refactor your design by extract the IO portion of the function for mocking later.
- Mock the extracted IO function with any mock library you want, in this case, Scalamock.
- Construct your test suite like how you would call your function from the main class
That’s it! I hope this helps you write better unit test in your Scala projects or unit test HttpClient
request. The full implementation of this tutorial is on GitHub. Please comment below if there are any questions and comments. If you have any other great strategies, feel free to share your knowledge in the comment section below!
Top comments (0)