No matter how meticulous you are, there are some parts of you program you are not going to unit test just like some funny nooks and crannies in your furniture are not reachable by a Roomba.
@techAU's Roomba feels very frustrated.
“Main” is one of those parts. Note the quotes because I don’t mean just the main function but all the code at the entry point of the application that is responsible for the initial instantiation and wiring1. For a small, simple program this is just the aforementioned main function but it can represent way more code.
Almost by definition, the so called “main” is the code that glues our application components and can’t be tested independently. Integration tests aside, this means that we can go guilt-free on having no unit tests for it. Right?
I won’t be writing this entry if that was just the end of the story. There is a risk associated with knowing that “you are not supposed to test the main”: attaching to it responsibilities that should be tested independently.
The usual suspects are snippets that are born as a line or two at the main function or class but then grow organically until becoming responsibilities by themselves: argument parsing, text-based user interaction, reading and writing configuration or data and, in general, anything adjunct to the domain logic.
The key skill here is knowing when those responsibilities should be extracted from main and independently tested. This takes experience but, as a rule of thumb, I would extract anything whose inputs and outputs can’t be exhaustively checked mentally as you read the code.
Data is your friend
Sometimes we slack in emancipating some responsibility, let’s say argument parsing, because of tricky side effects that get in out way. How to unit test a function that is supposed to end the process with exit code -1?
def parse(args: Array[String]): Command = ???
A general strategy that works most of the time is using data to represent the outcome of a computation. The effects such as terminating the process with a given exit code are kept separate (and maybe as part of the application main).
import scalaz.\/
def parse(args: Array[String]): InvalidArguments \/ Command = ???
This way you can easily test that a given command line produces a valid command
or some specific InvalidArguments
value. Then you might ask the exit status to
the error value or make it executable.
parse(args).fold(
command => execute(command),
error => System.exit(error.code)
)
Missing the right abstraction
Sometimes the obstacle is of a different nature: we lack a necessary abstraction. Paraphrasing Dijkstra, the purpose of abstraction is not to be vague but enabling us to be precise about something without becoming mired in irrelevant details.
This is the case of the text-based interaction with the console in Java/Scala as it lacks a standard out-of-the-shelf abstraction for it. Therefore, the path of least resistance is to directly use the actual console making it difficult to test.
If we are aware of this the solution is immediate and straightforward. We just
roll our own Console
interface and provide pluggable implementations tied to
the actual console or to in-memory buffers with testing methods.
trait Console {
def in: BufferedReader
def out: PrintWriter
}
object StandardConsole extend Console { ??? }
class TestConsole extend Console with ShouldMatchers {
def givenInput(line: String): Unit = { ??? }
def expectOutputContaining(text: String): Unit = { ??? }
???
}
In the end, you are supposed to test almost everything
It’s a pity that the guilt-free zone of code that should not be unit tested is very small indeed when we know some tricks. And now you don’t have any excuse to neglect writing those tests.
-
This is a very “Uncle Bob” concept explained at some point in the Clean Code videos ↩