问题描述
我试图找出为支持两个端点的Http4s应用程序编写集成测试的习惯用法。
我正在通过在新光纤上分叉Main
中的ZManaged
应用程序类,然后在发布ZManaged
时执行interruptFork。
然后,我将其转换为ZLayer
并通过provideCustomLayerShared()
在具有多个suite
的整个testM
上传递。
- 我在这里吗?
- 它不符合我的预期:
- 尽管以上述方式管理的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
如您所见,normal
和forkManaged
都立即被打断。但是acquire
中分叉的那一个就可以完成。
第二次考试
第二个测试似乎失败不是因为服务器已关闭,而是因为服务器似乎缺少了http4s端的“距离”路由。我注意到您收到404,这是HTTP状态代码。如果服务器关闭,您可能会得到类似Connection Refused
的信息。收到404时,实际上是一些HTTP服务器正在应答。
因此,我的猜测是这条路线确实缺失。也许要在路线定义中检查拼写错误,或者可能是该路线没有组成主要路线。
,最后,@ felher的Main.run(List()).forkManaged
帮助解决了第一个问题。
关于GET从集成测试内部拒绝主体的第二个问题是通过将方法更改为POST来解决的。我没有进一步研究为什么从测试内部拒绝GET的原因,但是当对正在运行的应用程序进行正常卷曲时,我却没有这样做。