r/golang•
MadPhoenix

How to unit test os/exec's cmd.Run() without actually invoking the program?

I'm writing a CLI and one of my desired features is to have a command called "feedback" that opens a browser window to the project's issue page using the appropriate helper program for the platform (xdg-open on Linux, open on Mac OS, etc.).

I've been able to isolate the building of the command to a func (getFeedback) that takes GOOS and my own command interface, allowing me to mock out the run() command for that particular unit test. But the parent func Feedback() that calls getFeedback() obviously has to call the real os/exec command, and os/exec doesn't seem to offer any helpful interfaces for mocking this.

I'm having issues figuring out how to write a unit test for the execution of cmd.Run that won't actually cause the process to run, since I obviously don't want a browser window to open when I run my tests.

I've found some suggestions on how to mock the real Run() (such as here), but they seem to be focused on running the external program and making sure some output is returned. In this case I don't care what is returned from the process, only whether there's an error running it.

Beyond that, it'd be nice if I could trap the exec() of the external process in some platform-agnostic way, make sure the command and args are what I expect, and prevent the external process from actually being run. That may be possible with this library, but I'm not sure that's the best design either. While my code is written for Linux and Mac OS today, I'd like to be able to extend it to Windows in the future.

Gist here: https://gist.github.com/brandentimm/0fd788e1a527eb7b74d090a01c72ae3d

Feedback() is the parent function that will be invoked by the CLI interface which I can't currently figure out how to validate or prevent from opening a browser window.

Any suggestions or wholesale critiques of my design welcome. Cheers!

EDIT: I should also add that the proposed solution from my first link introduces package-global state (var execCommand = exec.Command) which I'd like to avoid if possible.

Posted Oct 6, 2019, 10:42 PM UTC · edited

010
mattgen88•

I'd make an interface for an invokable process. Write your implementation to that interface, use a mock implementation in your test. Seems you're mostly doing this already.

Mocks are for your architecture boundaries. You don't want to test the OS invoking some process. You want to test that you're calling that process correctly. Assert the correct args and what not are being sent to your public interface and set up different return values and most importantly errors.

3
MadPhoenixOP•

Yea that's what I'm trying to achieve, you and I seem to agree on what a unit test should be. But I'm struggling with how best to implement it and not finding many examples available. I'm a bit surprised this isn't a more common pattern.

2
survivalmachine•

Wouldn’t you want to test to make sure the process fires effectively?

You could always just have the test case capture the pid of the process with Start, and upon successfully running it just kill the process.

2
MadPhoenixOP•

Thanks for the reply.

For a unit test, all I really want to care about is whether the appropriate Run call happens with the right command and arguments. Otherwise my tests have an external dependency on the program being called (xdg-open, open, and the browsers they'd be opening).

Plus it wouldn't work in a CI system. At least not without somehow calling chrome-headless or similar (still an external dependency), which would require platform-specific environment setup. I could do something like you suggest in a platform-specific integration test.

edit: several edits, sorry I had to think more thoroughly through your response.

1
Lewisham•

If you need to call an external process to prove a behavior, then you need to call the process to prove the behavior. If you stub out all the interactions, you’re just verifying your stub.

Create a CI environment that contains the dependencies you need.

1
MadPhoenixOP•

For integration testing I agree with you. Unit tests should not have external dependencies or be platform-specific if it can at all be avoided.

1
Lewisham•

I’ve been doing this for a long time and my thinking has aligned with yours for the majority. Given how much software now relies on microservices, networking, external processes etc, my thinking changed. I think the distinction between unit and integration is now very arbitrary and should only be thought of by:

“Will I modify external state?” “Will it take long to run?” “Is it impossible for me to ensure this process is available on all environments for the test to run?”

If you answer yes to any, then it’s an integration test. Otherwise it’s a unit test.

This is what I do at Big Unnamed Silicon Valley Tech Company.

2
survivalmachine•

Then your test doesn’t conform to a unit test, and should probably be included in integration testing with platform specific cases.

1
[deleted]•

[removed]

1
tv64738•

Exec something simpler in your tests, that just records whether the right thing (args, env, current dir, ...) happened or not. Do NOT use global state for that.

1