Testing Side Effects

Controlling the objects that a class depends on can allow us to verify the correct interactions occur from unit tests.

Testing Side Effects
Photo by WrongTog / Unsplash

I want to introduce a technique that can be utilised to test side effects that would otherwise be difficult to verify. We'll use a basic form of dependency injection but using just the Kotlin standard library rather than utilising a dependency injection framework. Let's keep it simple.

Side effects are things that the program does. It could be to make an image appear on screen, a file to be saved to disk or a job sent to a printer. Without side effects, computer programs would be difficult to interact with and would not be of much use.

By “difficult to verify”, I mean that there are some side effects from our code that we cannot check by simply accessing the value of a property or variable. There might be no change to the state of the application and no value is returned. Examples of this are an entry saved to a database or a file saved to disk.

In theory we could start a database to receive the transaction, and then query it after our test is run, but there is a lot of overhead in doing so and our test will be slower. Also checking to see that the file is created is possible, but we need to make sure that the file is cleaned up afterwards or subsequent tests might pass just because a previous run created the file.

The code that we want to test may interact with another service that may or may not give a response to indicate success or might not be queryable afterwards. Even if we could verify our interaction with it, setting up the other service to call is too much setup for a unit test.

Example Outline: Hello World!

Consider the process of writing to the console with a println() statement (or in Java System.out.println). How can you verify that the output that goes to the console is what was intended? This simple application will print the ubiquitous introductory phrase: “Hello World!” to the screen if no arguments are passed to the program at runtime. If any arguments are passed, then the “World” will be replaced with the first argument.

Now we need to write a test for this. But how can we check what has been printed on the console? There is no easy way to read back from the console using JVM languages. There are some libraries that provide some functionality to help, but we are not going to introduce any fresh dependencies. The trick is that during the test the program will not be interacting with the actual system console.

I don't recommend using the console as a means of interacting with users in general. It may be appropriate if the application is intended as a CLI tool accepting commands via the terminal. Use a logging framework rather than printing to System.out as a general rule.

Main Function

The program is pretty simple. It has a couple of lines in a main function in a file Main.kt, as follows:

fun main(args: Array<String>) {
	val name = if (args.size > 0) args[0] else "World"
	println("Hello $name!")
}

The is not testable at present. The function has access to the object to send text to the console via println and uses it to print the message. The change I want to introduce is to remove access to that object and replace it with something else, in this case a variable of type PrintStream. Let's replace this line with a different object of the same type.

printStream.println("Hello $name!")

Now the code is no longer directly interacting with something we cannot access to verify. More work is needed to setup this object, however, as we need it to equate to System.out when the program runs as normal and as our special testing version when running a test.

Test Driven

Time to start thinking of how to write the test for the simple program. How can we check what is printed with printStream.println()? One way is to use a ByteArrayOutputStream which can be interrogated to return the bytes that were output to the stream and can then be converted to a String. This could be constructed with:

val byteArrayOutputStream = ByteArrayOutputStream()
val printStream = PrintStream(byteArrayOutputStream)

Next we need to get our version of the PrintStream into the code instead of System.out. To call the code as it is now from test code, we'd need a line such as MainKt.main(args); we cannot change the signature for this to pass in the new object as this is the entry point to the program.

We need to move the work from the main function call to a new function in a class that we can control. Why not make a HelloWorld class that we can instantiate with our own PrintStream object? Then we can move the functionality that prints to this object from the static function main to a new one that we will call helloWorld(). Here's some skeleton code of what the test might look like:

val helloWorld = HelloWorld(printStream)
val args = emptyArray<String>()
helloWorld.helloWorld(args)

Finally, we need to verify what was sent to the stream. With access to the byteArrayOutputStream variable, we can construct a String from what was printed to it.

val result = String(byteArrayOutputStream.toByteArray())
assertEquals(expected, result)

We now have set up the print stream, figured out a way to initialise it and interrogate what was printed. Now it's time to code the HelloWorld class.

HelloWorld Class

The HelloWorld class has a single constructor parameter to setup up the printStream variale. The work to print to the screen is now in a class function, helloWorld() and using the member object printStream:

class HelloWorld(private val printStream: PrintStream) {
    fun helloWorld(args: Array<String>) {
        val name = if (args.size > 0) args[0] else "World"
        printStream.println("Hello $name!")
    }
}
This takes a single constructor argument to set up the printStream object. With dependency injection frameworks, we might not need to define this in the constructor. Most frameworks require an empty constructor and use reflection or annotations to identify the member variables that need to be injected.

The code should allow our test to pass at this stage, but what about when run as an application. We need to make some changes to the main function so that it instantiates the HelloWorld class, this time using System.out, and calls the helloWorld function with whatever arguments were passed in.

fun main(args: Array<String>) {
    val helloWorld = HelloWorld(System.out)
    helloWorld.helloWorld(args)
}

More Tests

Let's return to the test code and tidy it up somewhat. There are two test cases I want to use: one where no arguments are passed and one where a name is passed. I'll let you add another to see what happens if more than one argument are sent to the program. In the code below, byteArrayOutputStream, printStream and main variables are created at class level.

    @Test
    fun `should print Hello World when there are no arguments`() {
        val args = emptyArray<String>()
        val expected = "Hello World!\n"
        helloWorld.helloWorld(args)
        val result = String(byteArrayOutputStream.toByteArray())
        assertEquals(expected, result)
    }

    @Test
    fun `should print Hello Dave when the name Dave is passed`() {
        val expected = "Hello Dave!\n"
        val args = arrayOf("Dave")
        helloWorld.helloWorld(args)
        val result = String(byteArrayOutputStream.toByteArray())
        assertEquals(expected, result)
    }

Conclusion

In this post we used a simple technique, borrowed from dependency injection, to make the code testable. It allows a member variable to be passed as a constructor argument when the class being tested. This allowed the class to be constructed differently depending on whether it is being used by the main or test code. It allows us to capture interactions with that object, side effects to running the code, so that we can verify that things will work in the unit test.

It was beyond the scope of this article to talk about a dependency injection framework, which could have helped to wire up the class without having to pass in the printStream object. Another way to capture the interaction with the printStream would have been to use a “mock” object, but again that felt a little too heavy handed for this example. Both would have required importing other dependencies into the project.

The approach used fits well with both mocking and dependency injection; they can be added easily for more complicated scenarios, but the basic principle is still the same: classes need to be designed in such a way that objects they depend on can be substituted out. That allows us to focus a test on just the functions in the class under test.