Unit Testing with Scala 3 - STTP/Tapir/ZIO

We’ve got a new project that’s Scala 3/ZIO 2-based and have discovered that idioms for unit testing that worked with Scala 2/ZIO 1 (Scala 2 is the main factor here) are not working. The endpoints work fine as a real server - it’s just testing that’s been a bugaboo.

Sparing details, here is some sample code provided graciously by Adam (before I mentioned this is Dotty-only) that does not compile. Below is the code and the compiler error. This is a Dotty issue, not an sttp/Tapir one, but if anyone has a workaround for this, much appreciated. I’ve been unable to find a compiler workaround even with brute force (casting, etc.).

Sample Code:

import sttp.tapir.EndpointIO.annotations.jsonbody
import sttp.tapir.ztapir._
import sttp.client3._
import sttp.client3.impl.zio.RIOMonadAsyncError
import sttp.client3.testing.SttpBackendStub
import sttp.tapir.{EndpointInput, Schema}
import sttp.tapir.server.stub.TapirStubInterpreter
import sttp.tapir.json.zio._
import zio.{Console, ZIO, ZIOAppDefault}
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}

object TestWithJsonBodyUsingZioJson extends ZIOAppDefault {
  case class Payload(one: String, two: String)
  implicit val encoder: JsonEncoder[Payload] = DeriveJsonEncoder.gen[Payload]
  implicit val decoder: JsonDecoder[Payload] = DeriveJsonDecoder.gen[Payload]
  implicit val schema: Schema[Payload] = Schema.derived[Payload]

  case class RequestInput(@jsonbody payload: Payload)

  val input = EndpointInput.derived[RequestInput]
  val myEndpoint = endpoint.post.in(input).out(stringBody).zServerLogic(r => ZIO.succeed(s"Got request: $r"))

  val stub = TapirStubInterpreter(SttpBackendStub(new RIOMonadAsyncError[Any]))
    .whenServerEndpoint(myEndpoint)
    .thenRunLogic()
    .backend()
  val body = """
              {
                "one": "",
                "two": ""
              }
            """.stripMargin
  val response = basicRequest
    .contentType("application/json")
    .body(body)
    .post(uri"http://test.com/test-request")
    .send(stub)

  override def run = response.flatMap { r =>
    Console.printLine(r.toString())
  }
}

Error:

[error] -- [E007] Type Mismatch Error: /Users/ehq178/dev/pfc-card-dynamic-mapper/server/src/test/scala/com/capitalone/prometheus/Temp.scala:24:24 

[error] 24 |    .whenServerEndpoint(myEndpoint)

[error]    |                        ^^^^^^^^^^

[error]    |Found:    (TestWithJsonBodyUsingZioJson.myEndpoint : Any)

[error]    |Required: sttp.tapir.server.ServerEndpoint.Full[A, U, I, E, O, Nothing, 

[error]    |  [_] =>> zio.RIO[Any, _]

[error]    |]

[error]    |

[error]    |where:    A is a type variable with constraint 

[error]    |          E is a type variable with constraint 

[error]    |          I is a type variable with constraint 

[error]    |          O is a type variable with constraint 

[error]    |          U is a type variable with constraint 

Ok, typing explicitly to

root.sttp.tapir.ztapir.ZServerEndpoint[Any, Any]

makes the compiler happy.

1 Like

Indeed. The problem seems to be with typing the following:

val myEndpoint = endpoint.post.in(stringBody).out(stringBody)
  .zServerLogic(r => ZIO.succeed(s"Got request: $r"))

This gets typed as ZServerEndpoint[Nothing, Any]. That is, the R type parameter to zServerLogic gets inferred as Nothing. Adding an explicit type annotation, or the type parameter solves the problem:

val myEndpoint = endpoint.post.in(stringBody).out(stringBody)
  .zServerLogic[Any](r => ZIO.succeed(s"Got request: $r"))

It would seem that the R should be inferred as Any, as that’s what is dictated by the given ZIO.succeed, which is parameter passed here: def zServerLogic[R](logic: INPUT => ZIO[R, ERROR_OUTPUT, OUTPUT]). But because of R’s variance this get widened to Nothing.

Not sure if this is fixable, I’ll add a note in the documentation for now, to explicitly provide the type of the environment when using Scala 3.

1 Like