ebash

enhanced bash

Home Table of Contents GitHub

Module emock

emock is a mocking framework for bash. It integrates well into the rest of ebash and etest in particular to make it very easy to mock out real system binaries and replace them with mock instances which do something else. It is very flexible in the behavior of the mocked function utilizing the many provided option flags.

If you aren’t familiar with mocking, it is by far one of the most powerful test strategies and is not not limited to just higher-level object-oriented languages. It’s actually really powerful for low-level OS testing as well where you basically want to test just your code and not the entire OS. The typical strategy here is to essentially create a mock function or script which gets called instead of the real OS level component.

In bash you could simply create a function in order to mock out external binaries. But doing this manually all over the place is tedious and error-prone and not very feature rich. emock makes this both easier and far more powerful.

The simplest invocation of emock is to simply supply the name of the binary you wish to mock, such as:

emock "dmidecode"

This will create and export a new function called dmidecode that can be invoked instead of the real dmidecode binary at /usr/bin/dmidecode. This also creates a function named dmidecode_real which can be invoked to get access to the real underlying dmidecode binary at /usr/sbin/dmidecode.

By default, this mock function will simply return 0 and produce no stdout or stderr. This behavior can be customized via --return, --stdout, and --stderr.

Mocking a real binary with a simplex name like this is the simplest, but doesn’t always work. In particular, if at the call site you call it with the fully-qualified path to the binary, as in /usr/sbin/dmidecode, then our mocked function won’t be called. In this scenario, you need to mock it with the fully qualified path just as you would invoke it at the call site. For example:

emock "/usr/sbin/dmidecode"

Just as before, this will create and export a new function named /usr/sbin/dmidecode (yes, function names in bash CAN have slashes in them!!) which will be called in place of the real dmidecode binary. It will also create a new function /usr/sbin/dmidecode_real which will call the real binary in case you need to call it instead.

emock tracks various metadata about mocked binaries for easier testability. This includes the number of times a mock is called, as well as the arguments, return code, stdout, and stderr for each invocation. By default this is created in a local hidden directory named ‘.emock-\(' (where\) is the current process PID) and there will be a directory beneath that for each mock:

${PWD}/.emock-$$/dmidecode/called
${PWD}/.emock-$$/dmidecode/mode
${PWD}/.emock-$$/dmidecode/0/{args,return_code,stdin,stdout,stderr,timestamp}
${PWD}/.emock-$$/dmidecode/1/{args,return_code,stdin,stdout,stderr,timestamp}

Finally, you can pass in the --filesystem option and emock will write out the mock to the filesystem itself rather than only creating an in-memory mock. This facilitates more complex testing where the mock is called by a 3rd party script and we want to still be able to mock things out properly.

func assert_emock_called

assert_emock_called is used to assert that a mock is called the expected number of times.

For example:

assert_emock_called "func" 25
OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   times
         Number of times we expect the mock to have been called.

func assert_emock_called_with

assert_emock_called_with is used to assert that a particular invocation of a mock was called with the expected arguments. All arguments are fully quoted to ensure whitepace is properly perserved.

For example:

assert_emock_called_with "func" 0 "1" "2" "3" "docks and cats" "Anarchy"
expected=( "1" "2" "3" "dogs and cats" "Anarchy" )
assert_emock_called_with "func" 1 "${expected[@]}"
OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to look at the arguments for.

   expect
         Argument array we expect the mock function to have been called with.

func assert_emock_return_code

assert_emock_return_code is used to assert that a particular invocation of a mock produced the expected return code.

For example:

assert_emock_return_code "func" 0 0
assert_emock_return_code "func" 0 1
OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to look at the arguments for.

   return_code
         The expected return code.

func assert_emock_stderr

assert_emock_stderr is used to assert that a particular invocation of a mock produced the expected standard error.

For example:

assert_emock_stderr "func" 0 "This is the expected standard error for call #0"
assert_emock_stderr "func" 1 "This is the expected standard error for call #1"
OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to look at the arguments for.

   stderr
         The expected stdandard error.

func assert_emock_stdin

assert_emock_stdin is used to assert that a particular invocation of a mock was provided the expected standard input.

For example:

assert_emock_stdin "func" 0 "This is the expected standard input for call #0"
assert_emock_stdin "func" 1 "This is the expected standard input for call #1"
OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to look at the arguments for.

   stdin
         The expected stdandard input.

func assert_emock_stdout

assert_emock_stdout is used to assert that a particular invocation of a mock produced the expected standard output.

For example:

assert_emock_stdout "func" 0 "This is the expected standard output for call #0"
assert_emock_stdout "func" 1 "This is the expected standard output for call #1"
OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to look at the arguments for.

   stdout
         The expected stdandard output.

func emock

emock is used to mock out a real function or external binary with a fake instance which we control the return code, stdandard output, and standard error.

The simplest invocation of emock is to simply supply the name of the binary you wish to mock, such as:

emock "dmidecode"

This will create and export a new function called dmidecode that can be invoked instead of the real dmidecode binary at /usr/bin/dmidecode. This also creates a function named dmidecode_real which can be invoked to get access to the real underlying dmidecode binary at /usr/sbin/dmidecode.

By default, this mock function will simply return 0 and produce no stdout or stderr. This behavior can be customized via --return, --stdout, and --stderr.

You can also mock out binaries that are invoked using fully-qualified paths, as in:

emock "/usr/sbin/dmidecode"

Just as before, this will create and export a new function named /usr/sbin/dmidecode (yes, function names in bash CAN have slashes in them!!) which will be called in place of the real dmidecode binary. It will also create a new function /usr/sbin/dmidecode_real which will call the real binary in case you need to call it instead.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --filesystem, -f
         Write out the mock to the filesystem.

   --reset, -r
         Reset existing mock state inside statedir from prior mock invocations.

   --return-code, --return, --rc, -r <value>
         What return code should the mock script use. By default this is 0.

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the return code,
         stdout, and stderr for each invocation.

   --stderr, -e <value>
         What standard error should be returned by the mock.

   --stdin, -i
         Mock should read from standard input and store it into a file.

   --stdout, -o <value>
         What standard output should be returned by the mock.

   --textfile, -t
         Treat the body as a simple text file rather than a shell script. In this mode there
         will be no generated stdin, stdout or stderr and the mock will not be executable. The
         additional tracking emock does around how many times a mock is called is also disabled
         when in this mode. This is suitable for mocking out simple text files which your code
         will read or write to.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode). This must match
         the calling convention at the call site.

   body
         This allows fine-grained control over the body of the mocked function that is
         created. Instead of using return_code, stdout, and stderr, you can directly provide
         the entire body of the script in this string. The syntax of this is single quotes with
         enclosing curly braces. This is identical to what you would use with override_function.

func emock_args

emock_args is a utility function to make it easier to get the argument array from a particular invocation of a mocked function. This is stored on-disk and is easy to manually retrieve, but this function should always be used to provide a clean abstraction. If the call number is not provided, this will default to the most recent invocation’s argument array.

Inside the statedir, the argument array is stored as a newline separated file so that whitespace is preserved. To convert this back into an array, the best thing to do is to use array_init_nl:

array_init_nl args "$(emock_args func)"

Alternatively, the helper assert_emock_called_with is an extremely useful way to validate the arguments passed into a particular invocation.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to get the standard error for.

func emock_called

emock_called makes it easier to check how many times a mock has been called. This is tracked on-disk in the statedir in the file named called. While it’s easy to manually retrieve from this file, this function should always be used to provide a clean abstraction.

Just like a typical array in any language, the size, or count of the number of times that the mock has been called is 1-based but the actual index values we use to store the state files for each invocation is zero-based (again, just like an array).

So if this has never been called, then emock_called will echo 0, and there will be no on-disk state directory. The first time you call it, emock_called will echo 1, and there will be a ${statedir}/0 directory storing the state files for that invocation.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

func emock_dump_all_state

emock_dump_all_state is used to dump the state values of all existing mock.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.

func emock_dump_state

emock_dump_state is used to dump the state values of an existing mock.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

func emock_indexes

emock_indexes makes it easier to get the call indexes for the mock invocations. Think of this as the array indexes where the first call will have a call index of 0, the second call will have a call index of 1, etc. This is different than emock_called which a 1-based count. This is a 0-based list of call invocation numbers.

So if this has never been called, then emock_indexes will echo an empty string. If it has been called 3 times then this will echo 0 1 2. These corresponding to the directories: ${statedir}/0 ${statedir}/1 ${statedir}/2.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --last
         Display the LAST index only rather than all indexes.

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

func emock_mode

emock_mode is a utility function to make it easier to get the mocking mode that a mock was created with. This is stored on-disk and is easy to manually retrieve, but this function should always be used to provide a clean abstraction. The mocking mode will be one of either function or filesystem.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

func emock_return_code

emock_return_code is a utility function to make it easier to get the return code from a particular invocation of a mocked function. This is stored on-disk and is easy to manually retrieve, but this function should always be used to provide a clean abstraction. If the call number is not provided, this will default to the most recent invocation’s return code.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to get the standard error for.

func emock_stderr

emock_stderr is a utility function to make it easier to get the standard error from a particular invocation of a mocked function. This is stored on-disk and is easy to manually retrieve, but this function should always be used to provide a clean abstraction. If the call number is not provided, this will default to the most recent invocation’s standard error.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to get the standard error for.

func emock_stdin

emock_stdin is a utility function to make it easier to get the standard input that was provided to a particular invocation of a mocked function. This is stored on-disk and is easy to manually retrieve, but this function should always be used to provide a clean abstraction. If the call number is not provided, this will default to the most recent invocation’s standard output.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to get the standard output for.

func emock_stdout

emock_stdout is a utility function to make it easier to get the standard output from a particular invocation of a mocked function. This is stored on-disk and is easy to manually retrieve, but this function should always be used to provide a clean abstraction. If the call number is not provided, this will default to the most recent invocation’s standard output.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).

   num
         The call number to get the standard output for.

func eunmock

eunmock is used in tandem with emock to remove a mock that has previosly been created. This essentially removes the wrapper functions we create and also cleans up the on-disk statedir for the mock. Typically this is done in one of two ways. For test suite usage, the mock is often created inside setup and the mock is removed in teardown. Recall that setup is run before each test and teardown is run after each test.

Example:

setup()
{
    emock --filesystem "/usr/bin/logger"
}

teardown()
{
    eunmock "/usr/bin/logger"
}

Another alternative to this for more isolated usage where you don’t want the mock used in every test would be to create the mock and immediately register a trap to remove the mock. The reason we use a trap instead of just explicitly removing the mock at the end of the test is that a trap gets executed however you leave the function. This way if we leave the test early due to an assertion failure the mock is still removed. For example:

ETEST_foo()
{
    emock --filesystem "/usr/bin/logger"
    trap_add "eunmock /usr/bin/logger"
    ... rest of my test ...
}

If you are using emock without --filesystem flag then there is no reason to explicitly call eunmock as the mock is a function on your local stack. Since each test executes it a clean bash environment, when the test completes that local function goes away with your local test execution environment.

OPTIONS
(*) Denotes required options
(&) Denotes options which can be given multiple times

   --statedir <value>
         This directory is used to track state about mocked binaries. This will hold metadata
         information such as the number of times the mock was called as well as the exit code,
         stdout, and stderr for each invocation.


ARGUMENTS

   name
         Name of the binary to mock (e.g. dmidecode or /usr/sbin/dmidecode).