Bash:Mock Objects/Commands

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

Last edited: May 08, 2017