Bash:Mock Objects/Commands
==========================
:author: Aaron Ball
:email: nullspoon@oper.io
:revdate: May 08, 2017


Bash is be a surprisingly versatile language. For example, I have been writing
tests for my Bash library code. This is quite useful and is fairly simple,
unless of course the library code makes use of system utilities such as id,
getent, getenforce, etc. that require real data and a live system to work right
(or wrong as the case may be).

It is at junctions like these where it seems Bash might let us down because we
need to do something that is seemingly far outside the capabilities of a humble
shell scripting language. But alas, Bash has a surprise for us.


Enter Bash Mock Objects
-----------------------

I understand this isn't exactly a mock object, but it seems the best
explanation for what we're doing, so we'll just proceed with it.

In this case, we need to execute something that closely mimicks a
https://en.wikipedia.org/wiki/Unit_testing[unit test]. Unit tests are useful
because they do not require any existing preconfigured system resources to run
successfully (eg: a real website to curl, a real directory to list, a real file
to stat, a real user to enumerate, etc). In unit testing, we create
https://en.wikipedia.org/wiki/Mock_object[mock objects] to mimic behavior of
the real object, including all edge cases where it might fail us. This enables
us to test every imaginable edge case without needing the real McCoy to perform
the test (also known as integration tests).

Unlike lower level programming languages (especially object oriented
languages), Bash primarily works with system resources via [sub]shells and
system binaries, and shell builtins. We don't need to mock a data object, but
we do need to mock system commands, their output, and return codes to
adequately test code responses to their different configurations.


Use Cases
---------

When writing Bash scripts, we often make calls to commands that query system
resources. A few commands that come to mind are getenforce, id, getent, grep,
etc. For simplicity (and brevity) though, let's just discuss 'id'.

The 'id' command enumerates metadata about the specified user. This is a
particularly tricky command to test because it requires that a real user exist
to return output. That output is also unpredictable as the user may have a
different uid, group membership, etc, depending on which system is running the
test. This sounds like a perfect use case for mocking commands!


Application
-----------

We can't guarantee that a system is configured with the user we want, with the
right uid, and with the right groups, so we need to override the 'id' command
and mock the return text and exit code.

Let's write a quick override function that returns a fake user, and one that
indicates no user by that name exists. We'll get to how to plug this in in the
next section.

----
id_exists_fakeuser() {
  printf "uid=9999(fakeuser) gid=9999(fakeuser) groups=10(wheel),16(audio)\n"
  return 0
}

id_not_exists() {
  local name=${1}
  printf "id: ‘%s’: no such user\n" "${name}" >&2
  return 1
}
----

If we call either of these functions, id_exists_fakeuser or id_not_exists, they
will return their respective output, which is designed to exactly mimic that of
the id command for the matching real-world scenario (user does exist, user
doesn't exist).

To better complete this use case, let's create a sample library function that
uses the 'id' command. This is a function that might exist in a bash library
for instance.

----
user_exists() {
  local username=${1}

  id ${username} 2>/dev/null 1>/dev/null
  [ $? -eq 0 ] && return 1
  return 0
}
----

This user_exists function could be very useful to determine if, well, a user
exists. Unfortunately however, we can't test this function because it requires
a real, live 'id' command along with a user database like /etc/passwd. This is
where bash command overriding comes in.


Bash Binary Override
--------------------

Since we can't change our library code to execute a test case (which wouldn't
be a valid test anyways), we need to find a way to override the call to the
'id' command and replace it with a call to our fake id override 'mock'
functions.

At first I thought I could use bash aliases to override a binary call (eg:
'alias id=id_exists_fakeuser'). However, bash doesn't allow aliases to be
passed from the parent shell to children shells. This won't work for us at all,
since calling the function in many cases spawns a subshell, thus unsetting our
alias.

After a half hour of research, I couldn't find the answer I was looking for, so
I opted for the best solution I could come up with: a bash wrapper function
(never underestimate a good wrapper function). Unlike aliases [and associative
arrays], Bash functions can be passed from the parent shell to children shells.
This enables us to override a system binary by creating a wrapper function with
the same name that calls our desired override function instead. Comme cette...

----
mock_cmd() {
  local command="${1:-}"
  local override="${2:-}"

  # Remove target function if one is already set
  unset ${command}
  # Create a wrapper function called "${command}"
  eval "${command}() { ${override} \${@}; }"
}
----

This _mock_cmd_ function takes two arguments: the name of the command to
override and the name of the function that will replace it. Using our earlier
override function examples, let's demonstrate. Regardez cette.

----
mock_cmd 'id' 'id_exists_fakeuser'
----

This will create a wrapper function named 'id', which calls the function
'id_exists_fakeuser', passing through all arguments that were destined for the
id command, to the override function.


Tying it Together
-----------------

That was a lot of concepts to explain, so let's put it all together into the
same place...

Set up the user library (this is the application code piece)

.lib/user.sh
----
user_exists() {
  local username=${1}

  id ${username} 2>/dev/null 1>/dev/null
  [ $? -eq 0 ] && return 1
  return 0
}
----


Create the user_exists mock commands for each scenario we want to test for.

.mocks/user.sh
----
id_exists_fakeuser() {
  printf "uid=9999(fakeuser) gid=9999(fakeuser) groups=10(wheel),16(audio)\n"
  return 0
}

id_not_exists() {
  local name=${1}
  printf "id: ‘%s’: no such user\n" "${name}" >&2
  return 1
}
----

Set up the mock test library to the mock_cmd function is available to all
tests when they source it.

.tests/lib/mock.sh
----
mock_cmd() {
  local command="${1:-}"
  local override="${2:-}"

  # Remove target function if one is already set
  unset ${command}
  # Create a wrapper function called "${command}"
  eval "${command}() { ${override} \${@}; }"
}
----

Set up the user library test script. This will be the entry point for running
tests. This will bring it all together by sourcing our user library so its
functions can be called, and sourcing the mock library so we can override the
user library command calls.

.tests/user.sh
----
# Include the mock testing library
source lib/mock.sh

# Include the user library for testing it
source ../lib/user.sh

test.user_exists() {
  mock_cmd 'id' 'id_exists_fakeuser'

  # This will return that fakeuser exists, with a fake metadata string.
  user_exists 'fakeuser'
}

test.user_not_exists() {
  mock_cmd 'id' 'id_not_exists'

  # This will return that fakeuser does not exist
  user_exists 'fakeuser'
}
----


This is just a single test set. To scale this up to multiple libraries is
somewhat more complicated. To put it [somewhat] simply, I personally write a
run-tests.sh script at the top of the tests directory. This script loads each
library test set (read from a config file) in order. Each library test set
overrides some global variables at source time, such as TESTS, which contains
the names of each test function.

For each test set, iterrate over the TESTS array, executing each function mame
with an eval statement, and you've got a smoothly running test suite. For bonus
points, write a few testing libraries beyond libmock, such as libensure for
easy one-line value checking, liblog for easy standardized test logging output,
etc.


In Conclusion
-------------

This seems quite complicated at first, but once you've written the basics, it
all should fall into place fairly easily.

Some will likely argue that if you need to write 'mock' tests or even
libraries in Bash, you've exceeded the reasonable bounds for bash scripting and
should probably pick another language. I personally haven't arrived at that
conclusion yet, since no other scripting language [that isn't a shell] offers
such seamless integration with system utilities. It's just so easy to do system
task automation with bash.

On the other hand, readability and maintainability are very important. Chances
are, as you approach the limits of bash scripting, the pool of shell scripters
who can read and maintain your code will shrink the further out you go.
However, the same could be said for a niche testing framework for any other
language as well.

I don't question the usage of bash for system automation very often. When I do
though, it's usually when I need complex data scructures (like arrays of arrays
or hashes, things you can't easily pass to a subshell, etc). Though, if someone
would come up with a compiler language that closely mimicked C syntax and
functionality without losing ease of system integration, I would certainly be
tempted to jump ship a little more often.



[role="datelastedit"]
Last edited: {revdate}

// vim: set syntax=asciidoc:
