Tapir + zio-mock

Hi,
I’m currently struggling to wrap my head around how to get Tapir to work together with zio-mock.
We are heavily relying on zio-mock to keep our unit tests minimal, while still being able to make sure dependencies are called with the correct parameters.

I created a minimal example (slightly based on the awesome project builder you provide) to showcase what we try to achieve here.
The example contains an API and a Service that is injected with ZIO, to test the behavior of the API we have a MockService that should receive calls in the API tests and respond with a pre-defined result, so that we can check if Tapir is also handling the response correctly.

However the mock claims that it’s never called. Even more confusing, if we create an empty mock, it suddenly claims that it is being called.

Do you have any experience in using Tapir with zio-mock or are there any other recommended mocking frameworks that work with Tapir?

2 Likes

@x4121 After some initial analysis I noticed that zio-mock probably expects that sut.provide(...) is where the actual expectations get met. In your case, the test logic is

  private def testLogic(apiLayer: ULayer[ApiImpl]) = for {
    endpoint <- sut.provide(apiLayer)
    interpreter = TapirStubInterpreter(backendStub).whenServerEndpointRunLogic(endpoint).backend()
    response <- basicRequest
      .get(uri"http://test.com/do?something=$something")
      .response(asJson[ApiResult])
      .send(interpreter)
  } yield assertTrue(response.body == Right(result))

and the mock generates a failure, because sut.provide is just an intermediary step before actually calling the endpoint. I don’t know yet how to correctly address this, but that’s something I was able to diagnose so far.

@x4121 Following-up: If you change testLogic to a ZIO[Api, ....] and call .provide on it later, making the entire logic effectively a sut, things start to work correctly:

  private val testLogic: ZIO[Api, Throwable, TestResult] = for {
    endpoint <- Api.doSomethingEndpoint()
    interpreter = TapirStubInterpreter(backendStub).whenServerEndpointRunLogic(endpoint).backend()
    response <- basicRequest
      .get(uri"http://test.com/do?something=$something")
      .response(asJson[ApiResult])
      .send(interpreter)
  } yield assertTrue(response.body == Right(result))

and

    test("return result (expect mock call)") {
      val mock: ULayer[Service] = MockService.DoStuff(assertion = equalTo(something), result = value(result)).toLayer
      val layer = buildLayer(mock)

      testLogic.provide(layer) // <<< delayed provide
    }

The second test fails, but that’s probably caused by tapir-zio-mock-example/src/test/scala/com/x4121/MockService.scala at main · x4121/tapir-zio-mock-example · GitHub, which looks like forcing DoStuff, which is then used instead of MockService.empty.

Hi @kciesielski,

thanks for taking your time to look into this. I’m not quite sure of the internal workings of zio-mock, but your explanation makes sense.
Sorry for the late reply, I was away for a few days.

Did you accidentally copy&paste the old version of testLogic in your response, it still seems to be the initial version that I posted (which isn’t a ZIO[Api, ...])?
I’m still very new to ZIO and am not quite sure how you transformed the function. Would appreciate it a lot if you could post your version.
I tried it myself but am then greeted with a new error:

src/test/scala/com/x4121/ApiSpec.scala:39:46: could not find implicit value for parameter testConstructor: zio.test.TestConstructor[Nothing,zio.ZIO[Any,Throwable,Any]]
[error]     test("return result (expect mock call)") {

Thanks for spotting this @x4121, I have indeed copied the initial version instead of the fixed one. I edited the post and testLogic is now a ZIO[Api, ...], could you try it?

Oh wow… yes it’s working. Thanks so much, that helped a lot :slight_smile: