使用ZIO测试套件对HTTP服务器进行集成测试

问题描述

我试图找出为支持两个端点的Http4s应用程序编写集成测试的习惯用法。 我正在通过在新光纤上分叉Main中的ZManaged应用程序类,然后在发布ZManaged时执行interruptFork。 然后,我将其转换为ZLayer并通过provideCustomLayerShared()在具有多个suite的整个testM上传递。

  1. 我在这里吗?
  2. 它不符合我的预期:
  • 尽管以上述方式管理的httpserver已提供给包含这两个测试的套件,但它会在第一个测试后释放,因此第二个测试将失败
  • 测试套件永远不会结束,只是在执行两个测试后才会挂起

以下代码的半熟性质致歉。

object MainTest extends DefaultRunnableSpec {

  def httpServer =
    ZManaged
      .make(Main.run(List()).fork)(fiber => {
        //fiber.join or Fiber.interrupt will not work here,hangs the test
        fiber.interruptFork.map(
          ex => println(s"stopped with exitCode: $ex")
        )
      })
      .toLayer

  val clockDuration = 1.second

  //did the httpserver start listening on 8080?
  private def isLocalPortInUse(port: Int): ZIO[Clock,Throwable,Unit] = {
    IO.effect(new Socket("0.0.0.0",port).close()).retry(Schedule.exponential(clockDuration) && Schedule.recurs(10))
  }

  override def spec: ZSpec[Environment,Failure] =
    suite("MainTest")(
      testM("Health check") {
        for {
          _ <- TestClock.adjust(clockDuration).fork
          _ <- isLocalPortInUse(8080)
          client <- Task(JavaNetClientBuilder[Task](blocker).create)
          response <- client.expect[HealthReplyDTO]("http://localhost:8080/health")
          expected = HealthReplyDTO("OK")
        } yield assert(response) {
          equalTo(expected)
        }
      },testM("Distances endpoint check") {
        for {
          _ <- TestClock.adjust(clockDuration).fork
          _ <- isLocalPortInUse(8080)
          client <- Task(JavaNetClientBuilder[Task](blocker).create)
          response <- client.expect[DistanceReplyDTO](
            Request[Task](method = Method.GET,uri = uri"http://localhost:8080/distances")
              .withEntity(DistanceRequestDTO(List("JFK","LHR")))
          )
          expected = DistanceReplyDTO(5000)
        } yield assert(response) {
          equalTo(expected)
        }
      }
    ).provideCustomLayerShared(httpServer)
}

测试的输出是第二个测试失败而第一个成功。 而且我进行了足够的调试,以确保在第二次测试之前已经关闭了HTTPServer。

stopped with exitCode: ()
- MainTest
  + Health check
  - Distances endpoint check
    Fiber failed.
    A checked error was not handled.
    org.http4s.client.UnexpectedStatus: unexpected HTTP status: 404 Not Found

无论我是否在sbt test上从Intellij运行测试,在所有这些操作之后测试过程都保持挂起状态,我必须手动终止它。

解决方法

我认为这里有两件事:

Z管理并获取

ZManaged.make的第一个参数是创建资源的acquire函数。问题在于资源获取(以及释放它们)是不间断的。而且,每当执行.fork时,分叉光纤都会从其父光纤继承其可中断性。因此Main.run()部分实际上永远不会被中断。

为什么fiber.interruptFork似乎起作用? interruptFork实际上并不等待光纤中断。只有interrupt会这样做,这就是为什么它将挂起测试的原因。

幸运的是,有一种方法可以完全满足您的需求:Main.run(List()).forkManaged。这将生成一个ZManaged,它将启动主函数并在释放资源时中断它。

以下代码很好地说明了问题:

import zio._
import zio.console._
import zio.duration._

object Main extends App {

  override def run(args: List[String]): URIO[ZEnv,ExitCode] = for {
    // interrupting after normal fork
    fiberNormal <- liveASecond("normal").fork
    _           <- fiberNormal.interrupt

    // forking in acquire,interrupting in relase
    _ <- ZManaged.make(liveASecond("acquire").fork)(fiber => fiber.interrupt).use(_ => ZIO.unit)

    // fork into a zmanaged
    _ <- liveASecond("forkManaged").forkManaged.use(_ => ZIO.unit)

    _ <- ZIO.sleep(5.seconds)
  } yield ExitCode.success

  def liveASecond(name: String) = (for {
    _ <- putStrLn(s"born: $name")
    _ <- ZIO.sleep(1.seconds)
    _ <- putStrLn(s"lived one second: $name")
    _ <- putStrLn(s"died: $name")
  } yield ()).onInterrupt(putStrLn(s"interrupted: $name"))

}

这将给出输出:

born: normal
interrupted: normal

born: acquire
lived one second: acquire
died: acquire

born: forkManaged
interrupted: forkManaged

如您所见,normalforkManaged都立即被打断。但是acquire中分叉的那一个就可以完成。

第二次考试

第二个测试似乎失败不是因为服务器已关闭,而是因为服务器似乎缺少了http4s端的“距离”路由。我注意到您收到404,这是HTTP状态代码。如果服务器关闭,您可能会得到类似Connection Refused的信息。收到404时,实际上是一些HTTP服务器正在应答。

因此,我的猜测是这条路线确实缺失。也许要在路线定义中检查拼写错误,或者可能是该路线没有组成主要路线。

,

最后,@ felher的Main.run(List()).forkManaged帮助解决了第一个问题。

关于GET从集成测试内部拒绝主体的第二个问题是通过将方法更改为POST来解决的。我没有进一步研究为什么从测试内部拒绝GET的原因,但是当对正在运行的应用程序进行正常卷曲时,我却没有这样做。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...