This gist illustrates the basic concept of how to unit test code that executes external scripts and utilities by mocking.
It combines several blog posts to create the necessary insights:
- http://cs-guy.com/blog/2015/01/test-main/ for a modern (golang 1.4+) approach to test chicanery.
- https://npf.io/2015/06/testing-exec-command/ for more explanation of the technique involved.
- http://cs-guy.com/blog/2015/01/test-main/ for broader context on how to use TestMain
- https://medium.com/@povilasve/go-advanced-tips-tricks-a872503ac859 for deeper insight into testing mindset.
- https://golang.org/pkg/testing for details of the core testing API.
In reading this content on Day 1 of my Golang Testing journey, I struggled to understand my goal: how to effectively test functionality related to remotely executed functionality without actually leveraging those utilities.
The connection between a basic understanding of Golang testing and some of the techniques recommended is really unclear, so this write-up seeks to connect the dots a bit more slowly while illustrating what's involved in my interpretation.
The key insights you will be wrapping your head around:
- You can store a function, even a function attached to another package, in a variable in your local package, then use that variable as a function.
- Inside a test function, you can swap in a different function for that variable, monkey patching it to use some test-only code.
- When you run
go test
it compiles and generates a binary which is immediately executed, we are able to reference and execute this binary with os.Args[0] and the rest of the arguments passed to it. - Using TestMain we can tailor what the generated test binary does, using environment variables to provide context.
- We can call our own test binary with the exec library, and using our trick from #2, we can manipulate our external executions to instead target our handy, immediately managed test binary.
Review the code and the inline comments for more insights into how this works.
But first, an example of the code under test for context:
// execCommand is a simple variable reference to the function exec.Command. We will use this in our test code to swap out the value of execCommand,
// creating an opportunity to manipulate the exec.Command struct and therefore the details of what will be executed.
var execCommand = exec.Command
// This is a function from github.com/phase2/rig/util to parse out the version number from a docker --version command.
func GetRawCurrentDockerVersion() string {
output, _ := execCommand("docker", "--version").Output()
re := regexp.MustCompile("Docker version (.*),")
return re.FindAllStringSubmatch(string(output), -1)[0][1]
}