Тестирование интеграции HTTP-сервера с набором тестов ZIO

#scala #integration-testing #http4s #zio

#scala #интеграция-тестирование #http4s #zio

Вопрос:

Я пытаюсь выяснить идиому для написания интеграционного теста для приложения Http4s, которое поддерживает две конечные точки. Я запускаю Main класс приложения в a ZManaged , разветвляя его на новое волокно, а затем выполняю interruptFork при выпуске ZManaged . Затем я преобразую это в a ZLayer и передаю его в provideCustomLayerShared() целом suite , который имеет несколько testM s.

  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) 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 для запущенного приложения.