Skip to main content

A Python helper for unit testing shell scripts.

Project description

bashmocker

A 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 the patch and Mock features in the standard unittest.mock but 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 example

Say you have a shell script named cleanup.sh:

#!/bin/bash
echo "Cleaning the database now"
data_cleanup server1.example.com
data_cleanup server2.example.com
mail_report -t steve@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:

from bashmocker import BashMocker

with BashMocker('data_cleanup', 'mail_report') as bm:
    bm.runscript('./cleanup.sh')

    # Check that cleanup.sh called 'data_cleanup' twice.
    assert len(bm.last_calls['data_cleanup']) == 2

    # Check that 'mail_report' was run once with expected args
    assert bm.last_calls['mail_report'] == \
            [ ["-t", "steve@example.com", "All servers cleaned"] ]

    # And we can also see the message that was printed
    assert bm.last_stdout == "Cleaning the database now\n"

Adding side effects

In the above example, when the script calls data_cleanup in the BashMocker sandbox it actually calls a small script which simply logs the calling arguments and exits with status 0. If cleanup.sh expected some output from the data_cleanup program then it may get confused:

#!/bin/bash
result=$( data_cleanup server1.example.com )
[ "$result" = OK ] || mail_report -t steve@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 to add_mock(), as you can't specify side effects in the constructor. Note that the side_effect is a line to be run by Bash not a Python callback.

with BashMocker('mail_report') as bm:
    bm.add_mock('data_cleanup', side_effect="echo OK")
    bm.runscript('cleanup.sh')

You can also have a mocked command return a failure status.

with BashMocker() as bm:
    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 files

This 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?

BashMocker tries 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bashmocker-0.3.0.tar.gz (9.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bashmocker-0.3.0-py3-none-any.whl (7.8 kB view details)

Uploaded Python 3

File details

Details for the file bashmocker-0.3.0.tar.gz.

File metadata

  • Download URL: bashmocker-0.3.0.tar.gz
  • Upload date:
  • Size: 9.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/4.11.3 pkginfo/1.7.0 requests/2.27.1 requests-toolbelt/0.9.1 tqdm/4.64.0 CPython/3.7.10

File hashes

Hashes for bashmocker-0.3.0.tar.gz
Algorithm Hash digest
SHA256 334668c62b6428dd2d0efe79816e5ffc7f251caa4b3d19b3637e4744b59ce504
MD5 f651b18640a5a753c7e3b2df96842414
BLAKE2b-256 ebb115b3e6cec04b133c798a5decd13e088526de3f86b51ce4b3cfba02422421

See more details on using hashes here.

File details

Details for the file bashmocker-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: bashmocker-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 7.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/4.11.3 pkginfo/1.7.0 requests/2.27.1 requests-toolbelt/0.9.1 tqdm/4.64.0 CPython/3.7.10

File hashes

Hashes for bashmocker-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2c7768f036de87e87ebc2ae4892fac5ff1c35637e1fc0df30ecb5b7565d2546b
MD5 7bb68702a5ddc42ff3577b402257bf13
BLAKE2b-256 f3b1c105b7a9692c6b842065332967ad580dc0d554b70949c8ca5e3c29ba874e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page