How to Test Code that Executes Subprocesses

I was writing some stuff that programmatically executed terraform via Hashicorp’s terraform-exec library this week. This works by interacting with a terraform executable that you provide (along with a working directory) to the NewTerraform constructor.

Here’s the trick: when testing this code it does not need to interact with the real executable. It can subprocess any executable file/script/whatever as long as it can be found.

So to test my code and catch different failure conditions, I wrote a small, very fake terraform bash script that looked for the commands I needed to handle:


#!/usr/bin/env bash

case "$1" in
    init)
        echo "in init"
        exit "${TERRAFORM_INIT_EXIT:-0}"
        ;;
    apply)
        echo "in apply"
        exit "${TERRAFORM_APPLY_EXIT:-0}"
        ;;
    destroy)
        echo "in destroy"
        exit "${TERRAFORM_DESTROY_EXIT:-0}"
        ;;
    version)
        echo '{"terraform_version": "1.11.3", "provider_selections":{}}'
        exit 0
        ;;
    *)
        echo "unknown command: $@"
        exit 1
        ;;
esac

This handle the version command with a fake version, but otherwise looks for environment variables to determin exit codes for various commands. Now combine that idea with how I like to handle test setup and teardown type stuff in Go:


package example_test

import (
	"fmt"
	"path/filepath"
	"runtime"
	"testing"
)

type tfExitCodes struct {
	init    uint
	apply   uint
	destroy uint
}

const (
	tfExitCodeInit    = "TERRAFORM_INIT_EXIT"
	tfExitCodeApply   = "TERRAFORM_APPLY_EXIT"
	tfExitCodeDestroy = "TERRAFORM_DESTROY_EXIT"
)

func startTerraformTest(t *testing.T, exitCodes tfExitCodes) terraform.Terraform {
	t.Helper()

	_, filename, _, ok := runtime.Caller(0)
	if !ok {
		t.Fatal("failed to get caller information")
	}

	executable := filepath.Join(filepath.Dir(filename), "fixtures", "terraform")

	if exitCodes.init > 0 {
		t.Setenv(tfExitCodeInit, fmt.Sprint(exitCodes.init))
	}
	if exitCodes.apply > 0 {
		t.Setenv(tfExitCodeApply, fmt.Sprint(exitCodes.apply))
	}
	if exitCodes.destroy > 0 {
		t.Setenv(tfExitCodeDestroy, fmt.Sprint(exitCodes.destroy))
	}

	return &TheRealThingThatUsesTerraformExecUnderTheHood{
		Executable: executable,
	}
}

This uses runtime to look up the files directory, this points it to the fake terraform executable before using t.SetEnv to send our exit code info to the subprocess via env vars.

Now I can run tests against this code without having to know the whole world of terraform underneath, I just have to know how to make my stub executable work. I use the work stub here because this is very much just a test double but perhaps a counterintuitive one.