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. Also since the args are stored base64 encoded this function makes it simpler to unpack them.
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).