#scala #integration-testing #http4s #zio
#scala #интеграция-тестирование #http4s #zio
Вопрос:
Я пытаюсь выяснить идиому для написания интеграционного теста для приложения Http4s, которое поддерживает две конечные точки. Я запускаю Main
класс приложения в a ZManaged
, разветвляя его на новое волокно, а затем выполняю interruptFork при выпуске ZManaged
. Затем я преобразую это в a ZLayer
и передаю его в provideCustomLayerShared()
целом suite
, который имеет несколько testM
s.
- Я на правильном пути?
- Он ведет себя не так, как я ожидаю :
- Хотя 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) amp;amp; 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
И независимо от того, запускаю ли я тесты из Intellij на sbt testOnly, процесс тестирования остается зависшим после всего этого, и мне приходится вручную завершать его.
Ответ №1:
Я думаю, что здесь есть две вещи:
Управление Z и получение
Первым параметром ZManaged.make
является acquire
функция, которая создает ресурс. Проблема в том, что сбор ресурсов (а также их освобождение) выполняется без прерывания. И всякий раз, когда вы выполняете a .fork
, разветвленное волокно наследует свою прерываемость от своего родительского волокна. Таким Main.run()
образом, часть фактически никогда не может быть прервана.
Почему кажется, что это работает, когда вы это делаете fiber.interruptFork
? interruptFork
на самом деле не ожидает прерывания оптоволокна. interrupt
Это сделает Only, поэтому тест будет зависать.
К счастью, есть метод, который будет делать именно то, что вы хотите : Main.run(List()).forkManaged
. Это сгенерирует a 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-сервер фактически отвечает.
Итак, я предполагаю, что маршрут действительно отсутствует. Возможно, проверьте наличие опечаток в определении маршрута или, возможно, маршрут просто не включен в основной маршрут.
Ответ №2:
В итоге @felher Main.run(List()).forkManaged
помогли решить первую проблему.
Вторая проблема, связанная с GET, когда тело было отклонено изнутри интеграционного теста, была устранена путем изменения метода на POST. Я не стал вдаваться в подробности того, почему GET отклонялся изнутри теста, но не при выполнении обычного curl для запущенного приложения.