In an application I am currently writing I am using cats-effect's IO monad in an IOApp.
If started with a command line argument 'debug', I am delegeting my program flow into a debug loop that waits for user input and executes all kinds of debugging-relevant methods. As soon as the developer presses enter
without any input, the application will exit the debug loop and exit the main method, thus closing the application.
The main method of this application looks roughly like this:
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] = ???
}
Now, I want to test if e.g. my run
method behaves like expected in my 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?
}
}
Can I somehow 'search/iterate through the IO monad' and determine if my program contains the statements from the debug loop? Do I have to call program.unsafeRunSync()
on it to check that?
You could implement the logic of run
inside your own method, and test that instead, where you aren't restricted in the return type and forward run
to your own implementation. Since run
forces your hand to IO[ExitCode]
, there's not much you can express from the return value. In general, there's no way to "search" an IO
value as it just a value that describes a computation that has a side effect. If you want to inspect it's underlying value, you do so by running it in the end of the world (your main
method), or for your tests, you unsafeRunSync
it.
For example:
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] = ???
}
And then in your test:
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
}
}
}
To search through some monad expression, it would have to be values, not statements, aka reified. That is the core idea behind the (in)famous Free monad. If you were to go through the hassle of expressing your code in some "algebra" as they call (think DSL) it and lift it into monad expression nesting via Free
, then yes you would be able to search through it. There are plenty of resources that explain Free monads better than I could google is your friend here.
My general suggestion would be that the general principles of good testing apply everywhere. Isolate the side-effecting part and inject it into the main piece of logic, so that you can inject a fake implementation in testing to allow all sorts of assertions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With