Search code examples
scalamockitoscala-catsio-monad

Mocking of BlazeClientBuilder[IO] to return mock client[IO]


I am using the BlazeClientBuilder[IO].resource method to get Client[IO]. Now, I want to mock the client for unit testing but cannot figure out how to do so. Is there a good way of mocking this and how would I do that?

class ExternalCall(val resource: Resource[IO, Client[IO]], externalServiceUrl: Uri) {
def retrieveData: IO[Either[Throwable, String]] = {
for {
  req <- IO(Request[IO](Method.GET, uri = externalServiceUrl))
  response <- resource.use(client => {
    client.fetch[String](req)(httpResponse => {
      if (!httpResponse.status.isSuccess)
        throw new Exception(httpResponse.status.reason)
      else
        httpResponse.as[String]
    })
  })
} yield Right(response)
}
}

Caller code

new ExternalCall(BlazeClientBuilder[IO](global).resource).retrieveData

Solution

  • It seems you only need to do something like

    val resourceMock = mock[Resource[IO, Client[IO]]]
    //stub whatever is necessary
    val call = new ExternalCall(resourceMock).retrieveData
    //do asserts and verifications as needed
    

    EDIT:

    You can see a fully working example below, but I'd like to stress that this is a good example of why it is a good practice to avoid mocking APIs that you don't own.

    A better way to test this would be to place the http4s related code witin a class you own (YourHttpClient or whatever) and write an integration test for that class that checks that the http4s client does the right thing (you can use wiremock to simulate a real http server).

    Then you can pass mocks of YourHttpClient to the components that depend on it, with the advantage that you control its API so it will be simpler and if http4s ever updates its API you only have one breaking class rather than having to fix tens or hundreds of mock interactions.

    BTW, the example is written using mockito-scala as using the Java version of mockito would have yielded code much harder to read.

        val resourceMock = mock[Resource[IO, Client[IO]]]
        val clientMock   = mock[Client[IO]]
        val response: Response[IO] = Response(Status.Ok,
                                              body = Stream("Mocked!!!").through(text.utf8Encode),
                                              headers = Headers(`Content-Type`(MediaType.text.plain, Charset.`UTF-8`)))
    
        clientMock.fetch[String](any[Request[IO]])(*) shouldAnswer { (_: Request[IO], f: Response[IO] => IO[String]) =>
          f(response)
        }
    
        resourceMock.use[String](*)(*) shouldAnswer { (f: Client[IO] => IO[String]) =>
          f(clientMock)
        }
    
        val data = new ExternalCall(resourceMock, Uri.unsafeFromString("http://www.example.com")).retrieveData
    
        data.unsafeRunSync().right.value shouldBe "Mocked!!!"