Юнит-тестирование с монадой IO от кошачьего эффекта - PullRequest
0 голосов
/ 19 февраля 2019

Сценарий

В приложении, которое я сейчас пишу, я использую IO-монаду с эффектом кота в IOApp .

Если запущенос аргументом командной строки 'debug' я удаляю поток моей программы в цикл отладки, который ожидает ввода данных пользователем и выполняет все виды методов, относящихся к отладке.Как только разработчик нажимает enter без какого-либо ввода, приложение выходит из цикла отладки и выходит из метода main, закрывая приложение.

Основной метод этого приложения выглядит примерно так:

import scala.concurrent.{ExecutionContext, ExecutionContextExecutor}
import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._

object Main extends IOApp {

    val BlockingFileIO: ExecutionContextExecutor = ExecutionContext.fromExecutor(blockingIOCachedThreadPool)

    def run(args: List[String]): IO[ExitCode] = for {
        _ <- IO { println ("Running with args: " + args.mkString(","))}
        debug = args.contains("debug")
        // do all kinds of other stuff like initializing a webserver, file IO etc.
        // ...
        _ <- if(debug) debugLoop else IO.unit
    } yield ExitCode.Success

    def debugLoop: IO[Unit] = for {
      _     <- IO(println("Debug mode: exit application be pressing ENTER."))
      _     <- IO.shift(BlockingFileIO) // readLine might block for a long time so we shift to another thread
      input <- IO(StdIn.readLine())     // let it run until user presses return
      _     <- IO.shift(ExecutionContext.global) // shift back to main thread
      _     <- if(input == "b") {
                  // do some debug relevant stuff
                  IO(Unit) >> debugLoop
               } else {
                  shutDown()
               }
    } yield Unit

    // shuts down everything
    def shutDown(): IO[Unit] = ??? 
}

Теперь я хочу проверить, работает ли, например, мой метод run так, как ожидалось в моих ScalaTest s:

import org.scalatest.FlatSpec

class MainSpec extends FlatSpec{

  "Main" should "enter the debug loop if args contain 'debug'" in {
    val program: IO[ExitCode] = Main.run("debug" :: Nil)
    // is there some way I can 'search through the IO monad' and determine if my program contains the statements from the debug loop?
  }
}

Мой вопрос

Могу ли я как-то «искать / перебирать монаду ввода-вывода» и определять, содержит ли моя программа операторы из цикла отладки?Должен ли я позвонить program.unsafeRunSync(), чтобы проверить это?

Ответы [ 2 ]

0 голосов
/ 20 февраля 2019

Вы можете реализовать логику run в своем собственном методе и протестировать ее вместо этого, когда вы не ограничены типом возвращаемого значения, и перенаправить run в свою собственную реализацию.Так как run заставляет вашу руку к IO[ExitCode], вы не можете выразить многое из возвращаемого значения.В общем, нет способа «искать» значение IO, поскольку это просто значение, которое описывает вычисление, которое имеет побочный эффект.Если вы хотите проверить его базовое значение, вы можете сделать это, запустив его в конце света (ваш метод main), или для ваших тестов вы unsafeRunSync it.

Например:

sealed trait RunResult extends Product with Serializable
case object Run extends RunResult
case object Debug extends RunResult

def run(args: List[String]): IO[ExitCode] = {
  run0(args) >> IO.pure(ExitCode.Success)
}

def run0(args: List[String]): IO[RunResult] = {
  for {
    _ <- IO { println("Running with args: " + args.mkString(",")) }
    debug = args.contains("debug")
    runResult <- if (debug) debugLoop else IO.pure(Run)
  } yield runResult
}

def debugLoop: IO[Debug.type] =
  for {
    _ <- IO(println("Debug mode: exit application be pressing ENTER."))
    _ <- IO.shift(BlockingFileIO) // readLine might block for a long time so we shift to another thread
    input <- IO(StdIn.readLine()) // let it run until user presses return
    _ <- IO.shift(ExecutionContext.global) // shift back to main thread
    _ <- if (input == "b") {
      // do some debug relevant stuff
      IO(Unit) >> debugLoop
    } else {
      shutDown()
    }
  } yield Debug

  // shuts down everything
  def shutDown(): IO[Unit] = ???
}

А потом в вашем тесте:

import org.scalatest.FlatSpec

class MainSpec extends FlatSpec {

  "Main" should "enter the debug loop if args contain 'debug'" in {
    val program: IO[RunResult] = Main.run0("debug" :: Nil)
    program.unsafeRunSync() match {
      case Debug => // do stuff
      case Run => // other stuff
    }
  }
}
0 голосов
/ 19 февраля 2019

Для поиска по некоему выражению монады, это должны быть значения, а не выражения, то есть reified.Это основная идея позади (в) знаменитой Свободной монады.Если бы вам пришлось пройти через сложность выражения вашего кода в некоторой «алгебре», как они ее называют (например, DSL), и поднять ее в монадное выражение, вложенное с помощью Free, тогда да, вы могли бы найти его.Есть множество ресурсов, которые объясняют бесплатные монады лучше, чем я мог бы сказать, что Google - ваш друг здесь.

Мое общее предположение заключается в том, что общие принципы хорошего тестирования применимы повсеместно.Изолируйте побочную часть и внедрите ее в основную логику, чтобы вы могли внедрить в тест ложную реализацию, чтобы разрешить всевозможные утверждения.

...