5

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
Kumar Waghmode
  • 509
  • 2
  • 18
  • Why exactly do you want to do this? Mocking shouldn't be a goal in itself—it's one tool designed to help isolate some component for testing. It's possible (or even likely) that there's a better way to write these tests. – Travis Brown Feb 08 '19 at 09:18
  • 1
    I am using http4sclient to fetch external data using the builder, now I want to mock the external data for testing various scenarios. – Kumar Waghmode Feb 08 '19 at 11:19
  • Can you provide a snippet of code of how you are injecting and using `BlazeClientBuilder[IO].resource`? – ultrasecr.eth Feb 08 '19 at 11:21
  • Added code snippet – Kumar Waghmode Feb 08 '19 at 12:12
  • 1
    How would you stub it (rather than mock) to return a hardcoded response for any request? – Toby Nov 01 '19 at 09:24

2 Answers2

2

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!!!"
ultrasecr.eth
  • 1,437
  • 10
  • 13
2

You can easly mock Client using following snippet

import fs2.Stream
import org.http4s.Response
import org.http4s.client.Client

def httpClient(body: String): Client[IO] = Client.apply[IO] { _ =>
    Resource.liftF(IO(Response[IO](body = Stream.emits(body.getBytes("UTF-8")))))
}

In order to have the client as resource you need to wrap it with IO and lift to Resource

Resource.liftF(IO(httpClient("body")))
slavik
  • 1,223
  • 15
  • 17
  • 1
    In Cats Effect 3, `Resource.liftF` is replaced with `Resource.eval`. Other than that, this works really well, and I have full control over how the response looks, not only the response body! – Onema Aug 29 '22 at 20:21