{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/test-subprocesses/",
  "description": "Test Go subprocesses with the re-exec pattern: spawn your test binary as a subprocess to emulate real command behavior reliably.",
  "path": "/go/test-subprocesses/",
  "publishedAt": "2025-11-16T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "Testing"
  ],
  "textContent": "When testing Go code that spawns subprocesses, you usually have three options.\n\nRun the real command. It invokes the actual binary that creates the subprocess and\nasserts against the output. However, that makes tests slow and tied to the environment. You\nhave to make sure the same binary exists and behaves the same everywhere, which is harder\nthan it sounds.\n\nFake it. Mock the subprocess to keep tests fast and isolated. The problem is that the\nfake version doesn't behave like a real process. It won't fail, write to stderr, or exit\nwith a non-zero code. That makes it hard to trust the result, and over time the mock can\ndrift away from what the real command actually does.\n\nRe-exec. I discovered this neat trick while watching Mitchel Hashimoto's [Advanced\nTesting with Go] talk. In fact, it originated in the stdlib [os/exec test suite]. With\nre-exec, your test binary spawns a new subprocess that runs itself again. Inside that\nsubprocess, the code emulates the behavior of the real command. The parent process then\ninteracts with this subprocess exactly as it would with a real command. In short:\n\n- The parent test process spawns the subprocess.\n- The subprocess emulates the behavior of the target command.\n- The parent process interacts with the emulated subprocess as if it were the real command.\n\nThis setup makes re-exec a middle ground between mocking and invoking the actual subprocess.\n\nThe first two paths are well-trodden, so let's look closer at the third one. Here's how it\nworks:\n\n- The test re-launches itself with a special flag or environment variable to signal it's\n  running in \"child\" mode.\n- In this mode, it acts as the subprocess and can print output, write to stderr, or exit\n  with any code you want. This subprocess basically emulates the real command's subprocess.\n- The main test process then runs as usual and interacts with it just like it would with a\n  real subprocess.\n\nYou still get a real subprocess, but the behavior of your original binary invocation is\nemulated inside it. So you don't invoke the original command. Observe:\n\nRunEcho invokes the system's echo binary with some argument and returns the output. Now\nlet's test it using the re-exec trick:\n\nTestRunEcho creates a command that re-runs the same test binary (os.Args[0]) as a\nsubprocess via the exec.Command. The -test.run=TestEchoHelper flag tells Go's test\nrunner to execute only the TestEchoHelper function inside that new process. The \"--\"\nmarks the end of the test runner's own flags, and everything after it (\"hello\") becomes an\nargument available to the helper process in os.Args.\n\nWhen this subprocess starts, it sees that the environment variable\nGO_WANT_HELPER_PROCESS=1 is set. That tells it to behave like a helper instead of running\nthe full test suite. The TestEchoHelper function then prints its last argument (\"hello\")\nto standard output and exits. In other words, we're emulating echo inside\nTestEchoHelper. This part is intentionally kept simple, but you can do all kinds of things\nhere to emulate the actual echo command. In real tests, this will also include different\nfailure modes.\n\nFrom the parent process's perspective, it looks just like running /bin/echo hello, except\neverything is happening within the Go test binary. The subprocess is real, but its behavior\nis entirely controlled by the test.\n\nYou might find it strange that the actual RunEcho function isn't called anywhere. That's\non purpose. The goal of this example is not to test production logic, but to show how to\nemulate and control subprocesses inside a test environment. The production function here\ndoesn't contain any logic beyond calling exec.Command, so there's nothing meaningful to\nverify yet.\n\nIn real code, typically, you'd split subprocess management into two parts: one that spawns\nthe process and another that handles its output and errors. The handler is where the bulk of\nyour logic should live. This way, the subprocess handling code can be tested in isolation\nwithout having to tie it with a real subprocess.\n\nConsider this example where the production code invokes the git switch mybranch command.\nThe RunGitSwitch command calls the git binary with the appropriate arguments and passes\nthe exec.Cmd pointer to the handleGitSwitch function. This handler function has the\nbulk of the logic that interacts with the git subprocess.\n\nAnd the corresponding test:\n\nIn this test, the subprocess behavior (git switch) is emulated by TestGitSwitchHelper.\nThe helper prints predictable output that mimics the real command, but the subprocess itself\nis still a separate process spawned by the parent test.\n\nWhat's under test here is handleGitSwitch, which manages subprocess execution, reads its\noutput, and handles errors. The subprocess is fake in behavior but real in execution, which\nmeans the I/O boundaries are still exercised.\n\nThis separation between subprocess creation and handling keeps tests focused and repeatable.\nYou can emulate different subprocess outcomes, such as errors or unexpected output, while\nkeeping the process interaction logic untouched.\n\n\n\n\n\n[advanced testing with go]:\n    https://www.youtube.com/watch?v=8hQG7QlcLBk\n\n[os/exec test suite]:\n    https://cs.opensource.google/go/go/+/refs/tags/go1.25.4:src/os/exec/exec_test.go",
  "title": "Re-exec testing Go subprocesses"
}