如何用unittest.mock捕获subprocess.call真实退出码(兼容Python2.7+)
Great question! Since you can't modify the run_hook function and need Python 2.7 compatibility, the key is to use unittest.mock (or the standalone mock library for Python 2.7) to wrap the real subprocess.call function—this lets you capture actual exit codes while letting run_hook behave as normal.
Here's a step-by-step solution:
First, let's assume your external somemodule.py looks like this (for context):
# somemodule.py import subprocess def run_hook(command): # You can't modify this function exit_code = subprocess.call(command, shell=True) if exit_code == 0: print("Hook succeeded") else: print("Hook failed with code {}".format(exit_code)) return exit_code
Test Code Implementation
For Python 2.7, install the mock library first with pip install mock. For Python 3, unittest.mock is included in the standard library.
import unittest # For Python 2.7: from mock import patch # For Python 3: from unittest.mock import patch from mock import patch import subprocess import somemodule class TestRunHook(unittest.TestCase): def setUp(self): # Store captured exit codes across test runs self.captured_exit_codes = [] # Create a wrapper function that calls real subprocess.call and captures exit codes def capture_real_exit_code(*args, **kwargs): real_exit_code = subprocess.call(*args, **kwargs) self.captured_exit_codes.append(real_exit_code) return real_exit_code # Patch the subprocess.call instance that run_hook uses self.patch = patch('somemodule.subprocess.call', side_effect=capture_real_exit_code) self.patch.start() def tearDown(self): # Clean up the patch after each test self.patch.stop() def test_run_hook_success(self): # Run a cross-platform command that exits with code 0 result = somemodule.run_hook('python -c "exit(0)"') # Assert the real exit code was captured self.assertEqual(self.captured_exit_codes[-1], 0) # Assert run_hook returns the correct code self.assertEqual(result, 0) def test_run_hook_failure(self): # Run a cross-platform command that exits with code 1 result = somemodule.run_hook('python -c "exit(1)"') self.assertEqual(self.captured_exit_codes[-1], 1) self.assertEqual(result, 1) if __name__ == '__main__': unittest.main()
Key Details:
- Patch the Right Instance: We patch
somemodule.subprocess.callinstead ofsubprocess.calldirectly. This is critical becauserun_hookimports subprocess into its own namespace—we need to mock the version it actually uses. - Capture Real Exit Codes: The
capture_real_exit_codewrapper invokes the realsubprocess.call, stores the exit code, and returns it. This ensuresrun_hookbehaves exactly as it would in production, while letting you inspect the exit code for assertions. - Cross-Platform Commands: Using
python -c "exit(n)"works on both Windows and Unix-like systems, so your tests are portable. - Cleanup: The
setUpandtearDownmethods ensure the patch is applied only for the duration of each test, preventing side effects between test cases.
Alternative: Mocking Exit Codes (No Real Commands)
If you don't want to execute real system commands (for speed or safety), you can mock subprocess.call to return specific exit codes and test how run_hook handles them:
def test_run_hook_handles_failure_mocked(self): with patch('somemodule.subprocess.call', return_value=2): result = somemodule.run_hook('any_command') self.assertEqual(result, 2) # Add assertions for how run_hook responds to failure (e.g., logs, error handling)
This approach gives you full control over the exit codes without running real processes, which is useful for edge cases.
内容的提问来源于stack exchange,提问作者Andre Pastore

