Make your program testable

Wiem Zine
Wiem Zine
Sep 10 · 10 min read

Every programmer has their approach to implement code, but all of us have the same goal at the end. We write programs to solve real world problems, after deployment we execute many test cases to make sure that the program works correctly as expected and to evaluate the conformity with its specific requirements. But when we face some issues it’s not easy to find which part of the code causes those issues, this is why we had to implement Unit tests and integration tests before we deploy to production!

To be able to implement those tests you have to make the code of your program testable, but everyone has their way and style to implement code and to think about “how to solve problems” especially if you write code using Scala, because Scala is flexible and you have the choice to write using imperative and declarative paradigms.

  • You might be from the programmers that come from imperative world, and you think that it‘s easier to continue with that style using Scala but sometimes you face some Functional Programming tricks that seem cool but they seem hard to understand.🤷
  • You might be enjoying learning about Functional programming and challenge yourself to change your way to implement code and to think functionally. 😎
  • You might be from the Functional Programmers that are hoping your team switch to FP and you try to teach them Tagless Final. 🤓

In this blog you will see how could you make your code testable in Scala using different paradigms.

To make it simple I am going to take an example of a greeting program that asks the user about their name and answers them nice to meet you.

Imperative programming

“imperative programming is a programming paradigm that uses statements that change a program’s state” — Wikipedia

So our program in this style would be like this:

def greeting(): Unit = {
println("Hello what is your name?")
val name = readLine()
println(s"Nice to meet you $name")
}

greeting executes a bench of statements and interacts directly with the console. There is no way to stop or control those statements, you will see the result of this program once you execute it, imagine if you have a more complex program using statements, this would be more tricky to figure out a way to test the possible test cases, and in case of any issue it wouldn’t be simple to know which statement in your program causes the failure.

It’s important to test the code.. But how could we test this simple program ?

The answer: We should make the code testable!

There is a way to improve this code and make it testable:

  1. Define an interface that describes the service:
trait Console {
def println(line: String): Unit
def readLine: String
}

2. The program now interacts with println and readLine via the Console interface:

def greeting(console: Console): Unit = {
console.println("Good morning, "what is your name?")
val name = console.readLine()
console.println(s"Good to meet you, $name!")
}

a. Test module

This enables us to test this program, and to check what are the outputs that would be displayed at the console by implementing a TestConsole:

var outputs: Vector[String] = Vector.empty
object TestConsole extends Console {
def println(line: String): Unit = outputs = outputs :+ line
def readLine: String = "test"
}

Now we’re able to test our program and check the outputs are showing the expected results:

outputs shouldBe
Vector("Good morning, what is your name?", "nice to meet you user-test”)

b. Live module

The production module interacts with the real console.

With this technique you can have different implementations of Console for test and production and the greeting program is generalized because it could be called using different environment.

Now let’s move to the functional programming way!

Declarative programming

Declarative programming means programming using expressions or declarations instead of statements. Functional programming is a declarative paradigm in which the program is a combination of small programs that describe every interaction. And each interaction is considered as an effect.

In this paradigm it’s possible to combine programs and to pass them through functions. Unlike imperative programming that uses statements that we cannot pass them through functions and return them as result.

In order to write purely functional code in Scala we would have to build an immutable data structure that takes the description of our effectful program.

Basically this data structure should have an interpreter to interact with the outside world at the end of your program. (to see more details take a look at the first part in my previous blog post)

This data structure represent something like this:

class IO[A](val unsafeRun: () => A) { s =>
def map[B](f: A => B) =
flatMap(f.andThen(IO.effect(_)))

def flatMap[B](f: A => IO[B]): IO[B] =
IO.effect(f(s.unsafeRun()).unsafeRun())
}

object IO {
def effect[A](eff: => A) = new IO(() => eff)
}

io.unsafeRun() //interact with the outside world

Now let’s describe our program greeting in functional programming fashion:

val greeting: IO[Unit] =
for {
_ <- IO.effect(println("Hello what is your name?"))
name <- IO.effect(readLine())
_ <- IO.effect(println(s"Nice to meet you $name"))
} yield ()
greeting.unsafeRun() // interact with the console

Nice! But how could we test a functional effect?

If you have an advanced level in FP there is a popular technique in Scala called Tagless Final that enables us to test our functional effects.

Tagless Final

We will use the similar interface that we saw earlier in the imperative approach.

  1. Console interface using functional effects:
trait Console[F[_]] {
def println(line: String): F[Unit]
val readLine: F[String]
}

object Console {
def apply[F[_]](implicit F: Console[F]) = F
}

But the difference:

  • Using F[_] to generalize the return type for each function to be a functional effect that has the same kind of IO. And IO has one parameter type. So F could be an IO , Future , List , Set
  • Turn Console into a type class that abstracts over different data type that have similar structures. This will help us to define different implementations for every functional effect that we want to use (in production and test). apply requires that F to be an instance of Console and returns the concrete type of our functional effect.

The idea of Tagless Final is to use type classes to model effects instead of interacting directly with the interface.

2. Implement the greeting program:

def greeting[F[_]: Monad: Console): F[Unit] =
for {
_ <- Console[F].println("Hello what is your name?")
name <- Console[F].readLine
_ <- Console[F].println(s"Nice to meet you $name")
} yield ()

greeting takes a parameter type F and requires that F is an instance of:

  • Console to interact with println and readLine .
  • Monad to be able to use flatMap and map → for-comprehension syntax sugar. ( Monad is defined in scalaz and cats or you can also write your own data structure that contains flatMap and map in order to be able to use them in a generic way.)

a. Test module

This enables us to test this program, and to check what are the outputs that would be displayed at the console by implementing a type class instance for test:

type Test[A] = State[Vector[String], A]

implicit val TestConsole = new Console[Test] {
def println(line: String): Test[Unit] =
State.modify(oldState => TestIO(oldState.outputs :+ line))

val readLine: Test[String] = State.get.map(_ => "test")
}

We would have to save the values that are shown in the console, this is why we’re using State which is defined in scalaz / cats

val (test, _) = greeting[Test].run(TestIO(Vector.empty))
test shouldBe Vector("Good morning, what is your name?", "nice to meet you test”)

b. Live module

For our production environment we can use IO , so we need a type class instance for IO :

implicit val LiveConsole = new Console[IO] {
def println(line: String): IO[Unit] = IO.effect(scala.Console.println(line))

val readLine: IO[String] = IO.effect(scala.io.StdIn.readLine)
}
greeting[IO].unsafeRun() //interact with the real world

Nice, this is how we gain the ability to test using Tagless Final.

But the problem is that the interface accepts any functional effect that could be not functional. So you would have to have knowledge about Functional effects.

if you have the knowledge about functional effects and you could use different types and you might use a data structure with more than one parameter type that doesn’t have the same kind of F[_] so you need to have a knowledge about Partial Type application and how to make lambda types like using ? in kind projector,

You also would have to know the Monad hierarchy as we saw F requires to be a Monad but did you wonder how it worked with Test using State in our example without specifying a type class Monad[Test]? because State is a Monad, so it’s important to use the State type of the same library that you defined your Monad .

To make sure that you’re using a type safe for effectful programs you could use the functional Scala libraries ZIO, Cats-effect, Monix that provide different features that you can use for your concurrent and asynchronous programs.

We could use the functional effects provided by those FP libraries using tagless final technique.

In February, 25th: John De Goes the maintainer of ZIO library reveals the ZIO-Environment that enables us to test our program using ZIO. And I am using the same example in this blog that John presented in his presentation.

ZIO

ZIO is a zero dependency Scala library for asynchronous and concurrent programming based on pure functional programming.

It has the functional effect type like the IO that we covered earlier but with many other features than controlling effects, it provides resource safety, and you could handle errors caused by the effectful programs and you can use many data types build on ZIO to solve complex concurrency problems or also to preserve the state (like we saw the data type State. we wouldn’t need to import an external library you could use Ref in ZIO) more than that ZIO provides a way to test your programs.

The data structure for effectful program in ZIO is called ZIO:

ZIO[R, E, A]

  • R is for the environment that our effectful program requires
  • E is the error type of the effectful program
  • A is the result of the effectful program

Type aliases in ZIO:

In case if your effectful program doesn’t require any environment: ZIO[Any, E, A] you can use the typeIO[E, A]

If your program doesn’t require any environment and might fail with an Exception or any subtype of Throwable : ZIO[Any, Throwable, A] you can use the type: Task[A] but if it requires an environment: ZIO[R, Throwable, A] you can use RIO[A] .

If your program is total that will never fail and doesn’t require any environment: ZIO[Any, Nothing, A] you can use UIO[A]

So if you see IO , Task , RIO , UIO they are type alias of ZIO

ZIO environment

In our greeting example, how could we define the environment R which is a part of ZIO[R, E, A] ? the R is the Console in our example but how could we interact with their methods via the functional effects of ZIO?

  1. Console interface:
trait Console {
val console: Console.Service
}

object Console {
trait Service {
def println(line: String): UIO[Unit]
val readLine: UIO[String]
}
}

println and readLine are accessible via the service console

The next step is to define a helper: that accesses the methods of the service console and we make println and readLine accessible via this helper and return an effectful type that requires an environment of type Console

knowing that: ZIO[R, E, A]R => IO[E, A]

ZIO has a method called accessM that we can use to access the environment:

package console {
def println(line: String): ZIO[Console, Nothing, Unit] =
ZIO.accessM(_.console println line)

val readLine: ZIO[Console, Nothing, String] =
ZIO.accessM(_.console.readLine)
}

2. greeting program:

val greeting: ZIO[Console, Nothing, Unit] =
for {
_ <- console.println("Hello what is your name?")
name <- console.readLine
_ <- console.println(s"Nice to meet you $name")
} yield ()

To test this program, and to check what are the outputs that would be displayed at the console we need to implement the test environment that uses a Ref built on ZIO to preserve the outputs:

a. Test Module

case class Test(ref: Ref[Vector[String]]) extends Console {
val console: Service[Any] = new Service[Any] {
def println(line: String): UIO[Unit] =
ref.update(_ :+ line).unit
val readLine: UIO[String] = UIO("test")
}
}

To test greeting we have to provide the environment using greeting.provide(Test(state)) and we initialize our state then we get it at the end and test our outputs that are supposed to be displayed in the greeting program.

for {
state <- Ref.make(Vector.empty[String])
_ <- greeting.provide(Test(state))
result <- state.get
} yield result shouldBe Vector(
"Good morning, what is your name?",
"nice to meet you test”)

b. Live Module

trait Live extends Console {
val console: Service[Any] = new Service[Any] {
def println(line: String): UIO[Unit] =
UIO(scala.Console.println(line))
val readLine: UIO[String] = UIO(scala.io.StdIn.readLine)
}
}
object Live extends Live

At the end we provide this environment to our program:

greeting.provide(Live)

Awesome, there are different ways in Scala to write your code and to make it testable and generic for different environment, I hope that you enjoyed reading this blog post and you learned something. 👋

Note: I turned this presentation (that I used on my talk at Berlin Scala Meetup) into this blog post using the same steps that John De Goes explained in his talk.

I want to thank John De Goes for giving me the opportunity to learn from him and to work together on the ZIO-Environment feature and for giving me the opportunity to participate with him on his talk in Amsterdam Scala meetup about this work.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade