summaryrefslogtreecommitdiff
path: root/src/bash_mock_objects_commands.adoc
blob: 814f5691b13945683878e7ed8af899128136a0ca (plain)
    1 Bash:Mock Objects/Commands
    2 ==========================
    3 :author: Aaron Ball
    4 :email: nullspoon@oper.io
    5 :revdate: May 08, 2017
    6 
    7 == {doctitle}
    8 
    9 Bash is be a surprisingly versatile language. For example, I have been writing
   10 tests for my Bash library code. This is quite useful and is fairly simple,
   11 unless of course the library code makes use of system utilities such as id,
   12 getent, getenforce, etc. that require real data and a live system to work right
   13 (or wrong as the case may be).
   14 
   15 It is at junctions like these where it seems Bash might let us down because we
   16 need to do something that is seemingly far outside the capabilities of a humble
   17 shell scripting language. But alas, Bash has a surprise for us.
   18 
   19 
   20 Enter Bash Mock Objects
   21 -----------------------
   22 
   23 I understand this isn't exactly a mock object, but it seems the best
   24 explanation for what we're doing, so we'll just proceed with it.
   25 
   26 In this case, we need to execute something that closely mimicks a
   27 https://en.wikipedia.org/wiki/Unit_testing[unit test]. Unit tests are useful
   28 because they do not require any existing preconfigured system resources to run
   29 successfully (eg: a real website to curl, a real directory to list, a real file
   30 to stat, a real user to enumerate, etc). In unit testing, we create
   31 https://en.wikipedia.org/wiki/Mock_object[mock objects] to mimic behavior of
   32 the real object, including all edge cases where it might fail us. This enables
   33 us to test every imaginable edge case without needing the real McCoy to perform
   34 the test (also known as integration tests).
   35 
   36 Unlike lower level programming languages (especially object oriented
   37 languages), Bash primarily works with system resources via [sub]shells and
   38 system binaries, and shell builtins. We don't need to mock a data object, but
   39 we do need to mock system commands, their output, and return codes to
   40 adequately test code responses to their different configurations.
   41 
   42 
   43 Use Cases
   44 ---------
   45 
   46 When writing Bash scripts, we often make calls to commands that query system
   47 resources. A few commands that come to mind are getenforce, id, getent, grep,
   48 etc. For simplicity (and brevity) though, let's just discuss 'id'.
   49 
   50 The 'id' command enumerates metadata about the specified user. This is a
   51 particularly tricky command to test because it requires that a real user exist
   52 to return output. That output is also unpredictable as the user may have a
   53 different uid, group membership, etc, depending on which system is running the
   54 test. This sounds like a perfect use case for mocking commands!
   55 
   56 
   57 Application
   58 -----------
   59 
   60 We can't guarantee that a system is configured with the user we want, with the
   61 right uid, and with the right groups, so we need to override the 'id' command
   62 and mock the return text and exit code.
   63 
   64 Let's write a quick override function that returns a fake user, and one that
   65 indicates no user by that name exists. We'll get to how to plug this in in the
   66 next section.
   67 
   68 ----
   69 id_exists_fakeuser() {
   70   printf "uid=9999(fakeuser) gid=9999(fakeuser) groups=10(wheel),16(audio)\n"
   71   return 0
   72 }
   73 
   74 id_not_exists() {
   75   local name=${1}
   76   printf "id: ‘%s’: no such user\n" "${name}" >&2
   77   return 1
   78 }
   79 ----
   80 
   81 If we call either of these functions, id_exists_fakeuser or id_not_exists, they
   82 will return their respective output, which is designed to exactly mimic that of
   83 the id command for the matching real-world scenario (user does exist, user
   84 doesn't exist).
   85 
   86 To better complete this use case, let's create a sample library function that
   87 uses the 'id' command. This is a function that might exist in a bash library
   88 for instance.
   89 
   90 ----
   91 user_exists() {
   92   local username=${1}
   93 
   94   id ${username} 2>/dev/null 1>/dev/null
   95   [ $? -eq 0 ] && return 1
   96   return 0
   97 }
   98 ----
   99 
  100 This user_exists function could be very useful to determine if, well, a user
  101 exists. Unfortunately however, we can't test this function because it requires
  102 a real, live 'id' command along with a user database like /etc/passwd. This is
  103 where bash command overriding comes in.
  104 
  105 
  106 Bash Binary Override
  107 --------------------
  108 
  109 Since we can't change our library code to execute a test case (which wouldn't
  110 be a valid test anyways), we need to find a way to override the call to the
  111 'id' command and replace it with a call to our fake id override 'mock'
  112 functions.
  113 
  114 At first I thought I could use bash aliases to override a binary call (eg:
  115 'alias id=id_exists_fakeuser'). However, bash doesn't allow aliases to be
  116 passed from the parent shell to children shells. This won't work for us at all,
  117 since calling the function in many cases spawns a subshell, thus unsetting our
  118 alias.
  119 
  120 After a half hour of research, I couldn't find the answer I was looking for, so
  121 I opted for the best solution I could come up with: a bash wrapper function
  122 (never underestimate a good wrapper function). Unlike aliases [and associative
  123 arrays], Bash functions can be passed from the parent shell to children shells.
  124 This enables us to override a system binary by creating a wrapper function with
  125 the same name that calls our desired override function instead. Comme cette...
  126 
  127 ----
  128 mock_cmd() {
  129   local command="${1:-}"
  130   local override="${2:-}"
  131 
  132   # Remove target function if one is already set
  133   unset ${command}
  134   # Create a wrapper function called "${command}"
  135   eval "${command}() { ${override} \${@}; }"
  136 }
  137 ----
  138 
  139 This _mock_cmd_ function takes two arguments: the name of the command to
  140 override and the name of the function that will replace it. Using our earlier
  141 override function examples, let's demonstrate. Regardez cette.
  142 
  143 ----
  144 mock_cmd 'id' 'id_exists_fakeuser'
  145 ----
  146 
  147 This will create a wrapper function named 'id', which calls the function
  148 'id_exists_fakeuser', passing through all arguments that were destined for the
  149 id command, to the override function.
  150 
  151 
  152 Tying it Together
  153 -----------------
  154 
  155 That was a lot of concepts to explain, so let's put it all together into the
  156 same place...
  157 
  158 Set up the user library (this is the application code piece)
  159 
  160 .lib/user.sh
  161 ----
  162 user_exists() {
  163   local username=${1}
  164 
  165   id ${username} 2>/dev/null 1>/dev/null
  166   [ $? -eq 0 ] && return 1
  167   return 0
  168 }
  169 ----
  170 
  171 
  172 Create the user_exists mock commands for each scenario we want to test for.
  173 
  174 .mocks/user.sh
  175 ----
  176 id_exists_fakeuser() {
  177   printf "uid=9999(fakeuser) gid=9999(fakeuser) groups=10(wheel),16(audio)\n"
  178   return 0
  179 }
  180 
  181 id_not_exists() {
  182   local name=${1}
  183   printf "id: ‘%s’: no such user\n" "${name}" >&2
  184   return 1
  185 }
  186 ----
  187 
  188 Set up the mock test library to the mock_cmd function is available to all
  189 tests when they source it.
  190 
  191 .tests/lib/mock.sh
  192 ----
  193 mock_cmd() {
  194   local command="${1:-}"
  195   local override="${2:-}"
  196 
  197   # Remove target function if one is already set
  198   unset ${command}
  199   # Create a wrapper function called "${command}"
  200   eval "${command}() { ${override} \${@}; }"
  201 }
  202 ----
  203 
  204 Set up the user library test script. This will be the entry point for running
  205 tests. This will bring it all together by sourcing our user library so its
  206 functions can be called, and sourcing the mock library so we can override the
  207 user library command calls.
  208 
  209 .tests/user.sh
  210 ----
  211 # Include the mock testing library
  212 source lib/mock.sh
  213 
  214 # Include the user library for testing it
  215 source ../lib/user.sh
  216 
  217 test.user_exists() {
  218   mock_cmd 'id' 'id_exists_fakeuser'
  219 
  220   # This will return that fakeuser exists, with a fake metadata string.
  221   user_exists 'fakeuser'
  222 }
  223 
  224 test.user_not_exists() {
  225   mock_cmd 'id' 'id_not_exists'
  226 
  227   # This will return that fakeuser does not exist
  228   user_exists 'fakeuser'
  229 }
  230 ----
  231 
  232 
  233 This is just a single test set. To scale this up to multiple libraries is
  234 somewhat more complicated. To put it [somewhat] simply, I personally write a
  235 run-tests.sh script at the top of the tests directory. This script loads each
  236 library test set (read from a config file) in order. Each library test set
  237 overrides some global variables at source time, such as TESTS, which contains
  238 the names of each test function.
  239 
  240 For each test set, iterrate over the TESTS array, executing each function mame
  241 with an eval statement, and you've got a smoothly running test suite. For bonus
  242 points, write a few testing libraries beyond libmock, such as libensure for
  243 easy one-line value checking, liblog for easy standardized test logging output,
  244 etc.
  245 
  246 
  247 In Conclusion
  248 -------------
  249 
  250 This seems quite complicated at first, but once you've written the basics, it
  251 all should fall into place fairly easily.
  252 
  253 Some will likely argue that if you need to write 'mock' tests or even
  254 libraries in Bash, you've exceeded the reasonable bounds for bash scripting and
  255 should probably pick another language. I personally haven't arrived at that
  256 conclusion yet, since no other scripting language [that isn't a shell] offers
  257 such seamless integration with system utilities. It's just so easy to do system
  258 task automation with bash.
  259 
  260 On the other hand, readability and maintainability are very important. Chances
  261 are, as you approach the limits of bash scripting, the pool of shell scripters
  262 who can read and maintain your code will shrink the further out you go.
  263 However, the same could be said for a niche testing framework for any other
  264 language as well.
  265 
  266 I don't question the usage of bash for system automation very often. When I do
  267 though, it's usually when I need complex data scructures (like arrays of arrays
  268 or hashes, things you can't easily pass to a subshell, etc). Though, if someone
  269 would come up with a compiler language that closely mimicked C syntax and
  270 functionality without losing ease of system integration, I would certainly be
  271 tempted to jump ship a little more often.
  272 
  273 
  274 
  275 [role="datelastedit"]
  276 Last edited: {revdate}
  277 
  278 // vim: set syntax=asciidoc:

Generated by cgit