Package: bashmocker
bashmockerA Python helper for unit testing shell scripts. It's intended for use with Bash but
in fact any program that runs other programs and respects $PATH is testable this way.
I made it originally for a mostly-Python project which had some embedded shell scripts,
and I wanted to be able to test everything in one test framework.The idea is based on thepatchandMockfeatures in the standardunittest.mockbut rather than replacing Python functions it replaces executables. It works by making
a directory of decoy scripts, then adding this directory to the front of the PATH when
running whatever you want to test. You can then query which of the decoy scripts were
run, and with what parameters.This way you can use Python to write unit tests that invoke shell scripts and probe
aspects of their internal behaviour. You can probably do other things with it as well -
it's not tied to any particular testing framework.A quick exampleSay you have a shell script namedcleanup.sh:#!/bin/bashecho"Cleaning the database now"data_cleanupserver1.example.com
data_cleanupserver2.example.com
mail_report-tsteve@example.com"All servers cleaned"To test the behaviour of the script without actually cleaning any databases or spamming
Steve, we can run the script with these commands mocked out:frombashmockerimportBashMockerwithBashMocker('data_cleanup','mail_report')asbm:bm.runscript('./cleanup.sh')# Check that cleanup.sh called 'data_cleanup' twice.assertlen(bm.last_calls['data_cleanup'])==2# Check that 'mail_report' was run once with expected argsassertbm.last_calls['mail_report']==\[["-t","steve@example.com","All servers cleaned"]]# And we can also see the message that was printedassertbm.last_stdout=="Cleaning the database now\n"Adding side effectsIn the above example, when the script callsdata_cleanupin the BashMocker sandbox it
actually calls a small script which simply logs the calling arguments and exits with
status 0. Ifcleanup.shexpected some output from thedata_cleanupprogram then it
may get confused:#!/bin/bashresult=$(data_cleanupserver1.example.com)["$result"=OK]||mail_report-tsteve@example.com"All is not well!!!"To provide the output you can add a side effect. This needs to be added with an explicit
call toadd_mock(), as you can't specify side effects in the constructor. Note that theside_effectis a line to be run by Bash not a Python callback.withBashMocker('mail_report')asbm:bm.add_mock('data_cleanup',side_effect="echo OK")bm.runscript('cleanup.sh')You can also have a mocked command return a failure status.withBashMocker()asbm:bm.add_mock('data_cleanup',side_effect="echo OK")bm.add_mock('mail_report',fail=True)bm.runscript('cleanup.sh')Scripts that read and write filesThis module does not aim to help you with constructing a sandbox for your script to run in,
or inspecting the results of files written (apart from STDIN and STDOUT). You'll need to set
that up yourself.What if my script invokes programs /by/explicit/path?BashMockertries to patch these by defining functions, which are then fed to Bash via the
BASH_ENV setting. This is very hacky but can work sometimes. If you're only using this module
for writing regression tests then often a hacky solution is fine.What cannot be mocked out?Shell builtins, because Bash does not look for these in the PATH. I've not found a good reason
to mock them out in any case.
They could be done with the hack described above I guess - at present you'll have to call the
internal_add_mockfunc()directly to make that work.Does it only work with Bash?The main mocking mechanism simply pokes dummy programs into a directory added to the PATH, so
CSH scripts or Makefiles or compiled applications or even Python scripts (please don't!) can
be tested this way.Internally the module creates these dummy programs as little Bash Scripts, but you can also force
a different interpreter to be set using eg.BashMocker(shell="/bin/dash"). The mechanism will
work with Dash or with compatibility-mode Bash, but seriously who has Python3 and not Bash on
their system??The hack that allows mocking of programs with '/' in the name, ie. programs that are invoked
directly without searching the PATH, only works for Bash. POSIX shell doesn't let you make
functions with '/' in the name. I thought I could achieve it with aliases, but I hit a dead end.