Package: pytest-subprocess
pytest-subprocessPytest plugin to fake subprocess.UsageThe plugin adds thefake_processfixture (andfpas an alias).
It can be used it to register subprocess results so you won’t need to rely on
the real processes. The plugin hooks on thesubprocess.Popen(), which is
the base for other subprocess functions. That makes thesubprocess.run(),subprocess.call(),subprocess.check_call()andsubprocess.check_output()methods also functional.InstallationYou can installpytest-subprocessviapipfromPyPI:$ pip install pytest-subprocessBasic usageThe most important method isfp.register()(orregister_subprocessif you prefer to be more verbose), which
allows defining the fake processes behavior.deftest_echo_null_byte(fp):fp.register(["echo","-ne","\x00"],stdout=bytes.fromhex("00"))process=subprocess.Popen(["echo","-ne","\x00"],stdout=subprocess.PIPE,)out,_=process.communicate()assertprocess.returncode==0assertout==b"\x00"Optionally, thestdoutandstderrparameters can be a list (or tuple)
of lines to be joined together with a trailingos.linesepon each line.deftest_git(fp):fp.register(["git","branch"],stdout=["* fake_branch","  master"])process=subprocess.Popen(["git","branch"],stdout=subprocess.PIPE,universal_newlines=True,)out,_=process.communicate()assertprocess.returncode==0assertout=="* fake_branch\nmaster\n"Passing inputBy default, if you useinputargument to thePopen.communicate()method, it won’t crash, but also won’t do anything useful. By passing
a function asstdin_callableargument for thefp.register()method you can specify the behavior
based on the input. The function shall accept one argument, which will be
the input data. If the function will return a dictionary withstdoutorstderrkeys, its value will be appended to according stream.deftest_pass_input(fp):defstdin_function(input):return{"stdout":"This input was added:{data}".format(data=input.decode())}fp.register(["command"],stdout=[b"Just stdout"],stdin_callable=stdin_function,)process=subprocess.Popen(["command"],stdin=subprocess.PIPE,stdout=subprocess.PIPE,)out,_=process.communicate(input=b"sample input\n")assertout.splitlines()==[b"Just stdout",b"This input was added: sample input",]Unregistered commandsBy default, when thefpfixture is being used, any attempt to
run subprocess that has not been registered will raise
theProcessNotRegisteredErrorexception. To allow it, usefp.allow_unregistered(True), which will execute all unregistered
processes with realsubprocess, or usefp.pass_command("command")to allow just a single command.deftest_real_process(fp):withpytest.raises(fp.exceptions.ProcessNotRegisteredError):# this will fail, as "ls" command is not registeredsubprocess.call("ls")fp.pass_command("ls")# now it should be fineassertsubprocess.call("ls")==0# allow all commands to be called by real subprocessfp.allow_unregistered(True)assertsubprocess.call(["ls","-l"])==0Differing resultsEachregister()orpass_command()method call will register
only one command execution. You can call those methods multiple times, to
change the faked output on each subprocess run. When you call subprocess more
will be raised. To prevent that, callfp.keep_last_process(True),
which will keep the last registered process forever.deftest_different_output(fp):# register process with output changing each executionfp.register("test",stdout="first execution")# the second execution will return non-zero exit codefp.register("test",stdout="second execution",returncode=1)assertsubprocess.check_output("test")==b"first execution"second_process=subprocess.run("test",stdout=subprocess.PIPE)assertsecond_process.stdout==b"second execution"assertsecond_process.returncode==1# 3rd time shall raise an exceptionwithpytest.raises(fp.exceptions.ProcessNotRegisteredError):subprocess.check_call("test")# now, register two processes once again,# but the last one will be kept foreverfp.register("test",stdout="first execution")fp.register("test",stdout="second execution")fp.keep_last_process(True)# now the processes can be called foreverassertsubprocess.check_output("test")==b"first execution"assertsubprocess.check_output("test")==b"second execution"assertsubprocess.check_output("test")==b"second execution"assertsubprocess.check_output("test")==b"second execution"Using callbacksYou can pass a function ascallbackargument to theregister()method which will be executed instead of the real subprocess. The callback function
can raise exceptions which will be interpreted in tests as an exception raised
by the subprocess. The fixture will passFakePopenclass instance into the
callback function, that can be used to change the return code or modify output
streams.defcallback_function(process):process.returncode=1raisePermissionError("exception raised by subprocess")deftest_raise_exception(fp):fp.register(["test"],callback=callback_function)withpytest.raises(PermissionError,match="exception raised by subprocess"):process=subprocess.Popen(["test"])process.wait()assertprocess.returncode==1It is possible to pass additional keyword arguments intocallbackby using
thecallback_kwargsargument:defcallback_function_with_kwargs(process,return_code):process.returncode=return_codedeftest_callback_with_arguments(fp):return_code=127fp.register(["test"],callback=callback_function_with_kwargs,callback_kwargs={"return_code":return_code},)process=subprocess.Popen(["test"])process.wait()assertprocess.returncode==return_codeAs a context managerThefpfixture providescontext()method that allows us to
use it as a context manager. It can be used to limit the scope when a certain
command is allowed, e.g. to make sure that the code doesn’t want to execute
it somewhere else.deftest_context_manager(fp):withpytest.raises(fp.exceptions.ProcessNotRegisteredError):# command not registered, so will raise an exceptionsubprocess.check_call("test")withfp.context()asnested_process:nested_process.register("test",occurrences=3)# now, we can call the command 3 times without errorassertsubprocess.check_call("test")==0assertsubprocess.check_call("test")==0# the command was called 2 times, so one occurrence left, but since the# context manager has been left, it is not registered anymorewithpytest.raises(fp.exceptions.ProcessNotRegisteredError):subprocess.check_call("test")Non-exact command matchingIf you need to catch a command with some non-predictable elements, like a path
to a randomly-generated file name, you can usefake_subprocess.any()for
that purpose. The number of arguments that should be matched can be controlled
byminandmaxarguments. To usefake_subprocess.any()you need
to define the command as atupleorlist. The matching will work even
if the subprocess command will be called with a string argument.deftest_non_exact_matching(fp):# define a command that will take any number of argumentsfp.register(["ls",fp.any()])assertsubprocess.check_call("ls -lah")==0# `fake_subprocess.any()` is OK even with no argumentsfp.register(["ls",fp.any()])assertsubprocess.check_call("ls")==0# but it can force a minimum amount of argumentsfp.register(["cp",fp.any(min=2)])withpytest.raises(fp.exceptions.ProcessNotRegisteredError):# only one argument is used, so registered command won't matchsubprocess.check_call("cp /source/dir")# but two arguments will be fineassertsubprocess.check_call("cp /source/dir /tmp/random-dir")==0# the `max` argument can be used to limit maximum amount of argumentsfp.register(["cd",fp.any(max=1)])withpytest.raises(fp.exceptions.ProcessNotRegisteredError):# cd with two arguments won't match with max=1subprocess.check_call("cd ~/ /tmp")# but any single argument is fineassertsubprocess.check_call("cd ~/")==0# `min` and `max` can be used togetherfp.register(["my_app",fp.any(min=1,max=2)])assertsubprocess.check_call(["my_app","--help"])==0You can also specify just the command name, and have it match any command with
the same name, regardless of the location. This is accomplished withfake_subprocess.program("name").deftest_any_matching_program(fp):# define a command that can come from anywherefp.register([fp.program("ls")])assertsubprocess.check_call("/bin/ls")==0Check if process was calledYou may want to simply check if a certain command was called, you can do this
by accessingfp.calls, where all commands are stored as-called.
You can also use a utility functionfp.call_count()to see
how many a command has been called. The latter supportsfp.any().deftest_check_if_called(fp):fp.keep_last_process(True)# any command can be calledfp.register([fp.any()])subprocess.check_call(["cp","/tmp/source","/source"])subprocess.check_call(["cp","/source","/destination"])subprocess.check_call(["cp","/source","/other/destination"])# you can check if command is in ``fp.calls``assert["cp","/tmp/source","/source"]infp.callsassert["cp","/source","/destination"]infp.callsassert["cp","/source","/other/destination"]infp.calls# or check how many it was called, possibly with wildcard argumentsassertfp.call_count(["cp","/source","/destination"])==1# with ``call_count()`` you don't need to use the same type as# the subprocess was calledassertfp.call_count("cp /tmp/source /source")==1# can be used with ``fp.any()`` to match more callsassertfp.call_count(["cp",fp.any()])==3Handling signalsYou can use standardkill(),terminate()orsend_signal()methods
inPopeninstances. There is an additionalreceived_signals()method
to get a tuple of all signals received by the process. It is also possible to
set up an optional callback function for signals.importsignaldeftest_signal_callback(fp):"""Test that signal callbacks work."""defcallback(process,sig):ifsig==signal.SIGTERM:process.returncode=-1# the `register()` method returns a ProgressRecorder object, where# all future matching `Popen()` instances will be appendedprocess_recorder=fp.register("test",signal_callback=callback)process=subprocess.Popen("test")process.send_signal(signal.SIGTERM)process.wait()assertprocess.returncode==-1assertprocess.received_signals()==(signal.SIGTERM,)# the instance appended to `register()` output is the `Popen` instance# created laterassertprocess_recorder.first_callisprocessAsyncio supportThe plugin now supports asyncio and works forasyncio.create_subprocess_shellandasyncio.create_subprocess_exec:@pytest.mark.asyncioasyncdeftest_basic_usage(fp,):fp.register(["some-command-that-is-definitely-unavailable"],returncode=500)process=awaitasyncio.create_subprocess_shell("some-command-that-is-definitely-unavailable")returncode=awaitprocess.wait()assertprocess.returncode==returncodeassertprocess.returncode==500DocumentationFor full documentation, including API reference, please seehttps://pytest-subprocess.readthedocs.io/en/latest/.ContributingContributions are very welcome. Tests can be run withtox, please ensure
the coverage at least stays the same before you submit a pull request.LicenseDistributed under the terms of theMITlicense, “pytest-subprocess” is free and open source softwareIssuesIf you encounter any problems, pleasefile an issuealong with a detailed description.Thispytestplugin was generated withCookiecutteralong with@hackebrot’scookiecutter-pytest-plugintemplate.History1.5.0 (2023-01-28)Features#109: Match alsoos.PathLike.#105: Add program matcher.Other changes#110: Produce TypeError on Win Py<3.8 for Path args.1.4.2 (2022-10-02)Features#87: Add support for Python 3.11.#80,#86: Theregister()method returns an auxiliary object that will contain all matchingFakePopeninstances.Bug fixes#93: Raise callback exceptions oncommunicate()calls.Other changes#97: Fixed warnings in tests, treat warnings as errors.#91: Usesys.executableinstead just“python”in tests while invoking python subprocess.#90: Fix documentation build, add CI check for it.1.4.1 (2022-02-09)Other changes#74: Addfpalias for the fixture, andregisterfor theregisiter_subprocess.1.4.0 (2022-01-23)Features#71: Add support for stdin with asyncio.Bug fixes#68: Makestdoutandstderranasyncio.StreamReaderinstance when using asyncio functions.#63,#67: Add missing items toasyncio.subprocess.Other changes#69: Extracted code into separate files to improve navigation.1.3.2 (2021-11-07)Bug fixes#61: Fixed behavior ofasyncio.create_subproess_exec().1.3.1 (2021-11-01)Bug fixes#58: Correctly handle file stream output.1.3.0 (2021-10-24)Features#55: Add support forterminate(),kill(),send_signal().1.2.0 (2021-10-09)Features#49,#52: Add support forasyncio.Other changes#50: Change docs theme.1.1.2 (2021-07-17)Bug fixes#47: Preventallow_unregistered()andkeep_last_process()from affecting other tests.1.1.1 (2021-06-18)Bug fixes#43: Wait for callback thread to finish when callingcommunicate().Other changes#42: Fix type annotations forregister_subprocess().1.1.0 (2021-04-18)Bug fixes#37: Preserve original command inproc.argsto prevent leaking the internalCommandtype.Other changes#38: Switched CI from Azure Pipelines to GitHub Actions.#35: Drop support for python 3.4 and 3.5. Move type annotations from.pyifiles into sources.1.0.1 (2021-03-20)Bug fixes#34: Prevent appending newlines to outputs unless defined as list/tuple.Other changes#32: Make theCommandclass iterable.1.0.0 (2020-08-22)Features#29: Remember subprocess calls to check if expected commands were executed.#28: Allow to match a command with variable arguments (non-exact matching).0.1.5 (2020-06-19)Bug fixes#26:encodinganderrorsarguments will properly triggertextmode.0.1.4 (2020-04-28)Bug fixes#22: Thereturncodewill not be ignored whencallbackis used.#21: The exception raised from callback will take precedence over those from subprocess.#20: Registering process will be now consistent regardless of the command type.#19: Fixed crash for stderr redirect with an empty stream definition.0.1.3 (2020-03-04)Features#13: Allow passing keyword arguments into callbacks.Bug fixes#12: Properly raise exceptions from callback functions.Documentation changes#15: Add documentation chapter about the callback functions.0.1.2 (2020-01-17)Features#3: Add basic support for process input.Bug fixes#5: Makewait()method to raiseTimeoutErrorafter the desired time will elapse.Documentation changes#7,#8,#9: Create Sphinx documentation.Other changes#10:  Switch fromtoxtonoxfor running tests and tasks.#4: Add classifier for Python 3.9. Update CI config to test also on that interpreter version.0.1.1 (2019-11-24)Other changes#1,#2: Enable support for Python 3.4, add CI tests for that version.0.1.0 (2019-11-23)Initial release