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



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

  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-testOnly上运行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在测试中被拒绝,但在对运行的应用程序进行正常卷曲时却没有。

最新更新