Akka Http Client- How to create Unit test for Http Single Request in Scala

unit-test-akka-http

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!

Like this Article?

Sign up for my newsletter to get notified for new articles!


Related Posts

5 Anti Pattern for Writing Code in a Functional Programming Language

No 1. Nested Asynchronous Function

Will the Newest Generation of Programmers End Up Taking Older Software Engineers Jobs since they are Younger and know the Latest Technology

Will I lose my Job To the Younger Generation if I am not Passionate about Technology

Use This Mantra to Decide whether You Want to go to Big Tech or a Startup

If you want to Go Deep, Go for Big Tech. If you want to Go Wide, Go for Startup.

Why Do Functional Programmers Prefer For-Comprehension Over Imperative Code Block

Short Answer - Readability

An Interview Question that Truly Tests your Experience as a Software Engineer

questions that is not based on Leetcode