Как правильно проверить запланированные вызовы в тесте ZIO - PullRequest
0 голосов
/ 29 марта 2020

Я новичок в ZIO и ZIO Test, и я хотел бы протестировать службу планирования, которую я написал в ZIO v1.0.0RC17:

Служба (ы):

import zio.{RIO, Schedule}
import zio.clock.Clock
import zio.duration._

trait ModuleA {
  def moduleA: ModuleA.Service
}

object ModuleA {
  trait Service {
    def schedule(arg: Int): RIO[Clock, Unit]
  }
}

trait ModuleALive extends ModuleA {

  def moduleB: ModuleB.Service

  override def moduleA: ModuleA.Service = new ModuleA.Service {
    override def schedule(arg: Int): RIO[Clock, Unit] = {
      moduleB.run(arg).repeat(Schedule.spaced(1 day)).map(_ => ())
    }
  }
}

trait ModuleB {
  def moduleB: ModuleB.Service
}

object ModuleB {
  trait Service {
    def run(arg: Int): RIO[Clock, Unit]
  }
}

Сервис ModuleA должен в основном запускать метод Service ModuleB один раз в день с аргументом, переданным в ModuleA.Service.run.

Тест, который я хотел бы написать:

import java.util.concurrent.atomic.AtomicInteger

import zio.clock.Clock
import zio.duration._
import zio.test.environment.TestClock
import zio.test.{DefaultRunnableSpec, assertCompletes, suite, testM}
import zio.{RIO, Task, ZIO}

object ExampleSpec extends DefaultRunnableSpec(ExampleSuite.suite1)

object ExampleSuite {

  val counter: AtomicInteger = new AtomicInteger(0)

  trait ModuleBTest extends ModuleB {
    override def moduleB: ModuleB.Service = new ModuleB.Service {
      override def run(arg: Int): RIO[Clock, Unit] = ZIO.effectTotal(counter.incrementAndGet())
    }
  }

  object ModuleATest extends ModuleALive with ModuleBTest

  def verifyExpectedInvocationCount(expectedInvocationCount: Int): Task[Unit] = {
    val actualInvocations = counter.get()
    if (counter.get() == expectedInvocationCount)
      ZIO.succeed(())
    else
      throw new Exception(s"expected invocation count: $expectedInvocationCount but was $actualInvocations")
  }

  val suite1 = suite("a")(
    testM("a should correctly schedule b") {
      for {
        _ <- ModuleATest.moduleA.schedule(42).fork
        _ <- TestClock.adjust(12 hours)
        _ <- verifyExpectedInvocationCount(1)
        _ <- TestClock.adjust(12 hours)
        _ <- verifyExpectedInvocationCount(2)
      } yield assertCompletes
    }
  )
}

Я упростил тест с использованием счетчика, на самом деле я хотел бы использовать mockito для проверки количества вызовов и правильного аргумента. Однако этот тест не работает. В моем понимании это происходит из-за состояния гонки, вызванного накладными расходами времени, как описано в https://zio.dev/docs/howto/howto_test_effects#testing -clock .

Теперь есть примеры того, как решить эту проблему с помощью Promise. Я попробовал это, заменив счетчик обещанием, подобным следующему:

import java.util.concurrent.atomic.AtomicInteger

import zio.test.{DefaultRunnableSpec, assertCompletes, suite, testM}
import zio.{Promise, Task, UIO, ZIO}

object ExampleSpec extends DefaultRunnableSpec(ExampleSuite.suite1)

object ExampleSuite {

  val counter: AtomicInteger = new AtomicInteger(0)
  var promise: UIO[Promise[Unit, Int]] = Promise.make[Unit, Int]

  trait ModuleBTest extends ModuleB {
    override def moduleB: ModuleB.Service = new ModuleB.Service {
      override def run(arg: Int) = promise.map(_.succeed(counter.incrementAndGet))
    }
  }

  object ModuleATest extends ModuleALive with ModuleBTest

  def verifyExpectedInvocationCount(expectedInvocationCount: Int, actualInvocations: Int): Task[Unit] = {
    if (actualInvocations == expectedInvocationCount)
      ZIO.succeed(())
    else
      throw new Exception(s"expected invocation count: $expectedInvocationCount but was $actualInvocations")
  }

  val suite1 = suite("a")(
    testM("a should correctly schedule b") {
      for {
        _ <- ModuleATest.moduleA.schedule(42).fork
        p <- promise
        actualInvocationCount <- p.await
        _ <- verifyExpectedInvocationCount(expectedInvocationCount = 1, actualInvocationCount)
      } yield assertCompletes
    }
  )
}

Используя это, тест не завершится. Тем не менее, я почти уверен, что неправильно использовал обещание.

Как правильно подойти к этому сценарию тестирования?

1 Ответ

1 голос
/ 30 марта 2020

В вашем примере тип promise равен UIO[Promise[Unit, Int]], поэтому вы каждый раз создаете новое обещание. В результате, обещание, которое выполняет ваш эффект, отличается от того, которое ожидает ваш тест, что приводит к отсутствию завершения.

Чтобы проверить это, вы можете сделать что-то вроде этого:

import zio.clock.Clock
import zio.duration._
import zio.test.environment.TestClock
import zio.test.{ assertCompletes, suite, testM, DefaultRunnableSpec }
import zio._

object ExampleSpec extends DefaultRunnableSpec {

  trait ModuleA {
    def moduleA: ModuleA.Service
  }

  object ModuleA {
    trait Service {
      def schedule(arg: Int): RIO[Clock, Unit]
    }
  }

  trait ModuleALive extends ModuleA {

    def moduleB: ModuleB.Service

    override def moduleA: ModuleA.Service = new ModuleA.Service {
      override def schedule(arg: Int): RIO[Clock, Unit] =
        moduleB.run(arg).repeat(Schedule.spaced(1.day)).map(_ => ())
    }
  }

  trait ModuleB {
    def moduleB: ModuleB.Service
  }

  object ModuleB {
    trait Service {
      def run(arg: Int): RIO[Clock, Unit]
    }
  }

  trait ModuleBTest extends ModuleB {
    val counter: Ref[Int]
    val invocations: Queue[Int]
    override def moduleB: ModuleB.Service = new ModuleB.Service {
      override def run(arg: Int): UIO[Unit] =
        counter.updateAndGet(_ + 1).flatMap(invocations.offer).unit
    }
  }

  object ModuleATest {
    def apply(ref: Ref[Int], queue: Queue[Int]): ModuleALive with ModuleBTest =
      new ModuleALive with ModuleBTest {
        val counter     = ref
        val invocations = queue
      }
  }

  def verifyExpectedInvocationCount(invocations: Queue[Int], expected: Int): Task[Unit] =
    invocations.take.flatMap { actual =>
      if (actual == expected)
        ZIO.succeed(())
      else
        ZIO.fail(new Exception(s"expected invocation count: $expected but was $actual"))
    }

  def spec = suite("a")(
    testM("a should correctly schedule b") {
      for {
        counter     <- Ref.make(0)
        invocations <- Queue.unbounded[Int]
        moduleATest = ModuleATest(counter, invocations)
        _           <- moduleATest.moduleA.schedule(42).fork
        _           <- TestClock.adjust(12.hours)
        _           <- verifyExpectedInvocationCount(invocations, 1)
        _           <- TestClock.adjust(12.hours)
        _           <- verifyExpectedInvocationCount(invocations, 2)
      } yield assertCompletes
    }
  )
}

Поскольку мы хотим дождаться завершения нескольких эффектов, я использую Queue для их координации. Обратите внимание на несколько других моментов:

  • Вы можете заменить метод verifyExpectedInvocationsCount на использование утверждений в тесте ZIO, чтобы получить более качественные отчеты об ошибках.
  • В этом примере используется старая среда кодирования. Со слоями было бы значительно проще составить эти сервисы и поменять их в тестовой реализации на один из них.
  • Если вы хотите проверить, что эффект не заканчивается (например, другое значение никогда не помещается в очередь, если вы не ждете достаточно времени) вы можете использовать TestAspect#nonTermination.
...