Monday, July 6, 2020

Unit testing with ZIO


ZIO has the concept of Layers. These allow you to inject requirements to your ZIO monad (the R in ZIO[R, E, A]). The nice thing about them is that they make testing easier.

Say, I have an application with an intialisation piece and a data flow piece. My code would look like this:

import zio.{Has, Task, ZIO}

  type Init = Has[Init.Service]

  object Init {
    trait Service {
      def initializeWith(configFile:  String,
                         session:     SparkSession,
                         fs:          FileSystem): ZIO[Any, Throwable, Settings]
    }
...
  type Flow = Has[Flow.Service]

  object Flow {
    trait Service {
      def flow(s:       Settings,
               paths:   Filenames,
               session: SparkSession,
               fs:      FileSystem): Task[Results[String]]
    }

Now, the data structure I want to test is something like a ZIO[Init with Flow, Throwable, A] and the code that creates it is probably some large and complicated for-comprehension that's begging for a test. 

In production, the code to "run" it would look like:


    val prodInitialization: ULayer[Init]    = ZLayer.succeed(InitProd)
    val prodDataFlow:       ULayer[Flow]    = ZLayer.succeed(FlowProd)
...
    zio.provideLayer(prodInitialization ++ prodDataFlow)

where InitProd and FlowProd are my production objects that implement Init and Flow

My test code, however, look like:

import zio.test.Assertion._
import zio.test.environment.TestEnvironment
import zio.test.junit.JUnitRunnableSpec
import zio.test.{ZSpec, assertM, suite, testM}
import zio.{Layer, Task, ZIO, ZLayer}

class InitFlowSpec extends JUnitRunnableSpec {

  def unhappyPathInitLayer(e: Exception): Layer[Nothing, Init] = ZLayer.succeed(
    new Init.Service {
      override def initializeWith(configFile: String, session: SparkSession, fs: FileSystem) = ZIO.fail(e)
    }
  )
...
  val error                         = new Exception("Boo")
  val withNoErrors: Results[String] = Map.empty

  override def spec: ZSpec[TestEnvironment, Any] = suite("Orchestration")(
    testM("returns error if initialisation fails"){
      val layers    = unhappyPathInitLayer(error) ++ flowLayer(withNoErrors)
      val result    = zio.provideLayer(layers)
      assertM(result.run)(fails(equalTo(error)))
    }
  )

Et voila, we have a mocking framework for the requirement channel of a ZIO monad. 

No comments:

Post a Comment