{"id":35,"date":"2025-03-21T18:03:42","date_gmt":"2025-03-21T18:03:42","guid":{"rendered":"https:\/\/www.osxx.com\/?p=35"},"modified":"2025-03-21T18:05:33","modified_gmt":"2025-03-21T18:05:33","slug":"an-introduction-to-shmock","status":"publish","type":"post","link":"https:\/\/www.osxx.com\/index.php\/2025\/03\/21\/an-introduction-to-shmock\/","title":{"rendered":"An introduction  to shMock"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">shMock - A simple test framework for shell scripts such as bash<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">License<\/h2>\n\n\n\n<p>license AGPL-3.0 This code and the package of <strong>shMock<\/strong> are free software: you can redistribute it and\/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation.<\/p>\n\n\n\n<p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.<\/p>\n\n\n\n<p>You should have received a copy of the GNU Affero General Public License, version 3, along with this program. If not, see http:\/\/www.gnu.org\/licenses\/<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A short background<\/h2>\n\n\n\n<p>I use to recode times sepent for stories\/cards when I work on a project by using time tracker tools.<br>It helps to analyze jobs and improving ways of workings for next steps and future projects.<\/p>\n\n\n\n<p>During a test automation job to automate E2E tests\/ and improving a continuous testing flow I realized<br>that the time spent to fix shell scripts bugs and issues (in this case, scripts were mostly coded in<br>bash and groovy) is more than 40% of the total recoded times.<\/p>\n\n\n\n<p>Scripts usually are not so complicated. The main idea of using a script is to package a bunch of commands<br>that are run together to get a certain output.<br>In the case of scripts for automation flows such as CI flow, CD or CT flow, most challenges are<br>values used by scripts (typically output from a command used as arguments for other commands).<br>Moreover most initial values and parameters are generated by hosts (such as Jenkins, gerrit\u2026) that scripts run under. Accesses and permissions can also make it a little bit more complicated, for example when a script is run by Jenkins that the Jenkins itself is run under Tomcat.<\/p>\n\n\n\n<p>Anyways a WOW optimization was started and the first idea was to use a real test framework for scripts to reduce the number of issues after being implemented in the main flow.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What shMock is<\/h2>\n\n\n\n<p><strong>shMock<\/strong> is a practical framework to develop tests for shell scripts such as sh\/bash\/groovy.<br>The framework contains several principals and also a set of pre-coded libraries to write and running tests.<br><strong>shMock<\/strong> is developed on a private repository and <strong>its GitHub fork is only updated for major changes and hotfixes<\/strong>.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">An overview of shMock<\/h1>\n\n\n\n<p>If you are familiar with GMock and other standard test frameworks, you have already know how to use <strong>shMock<\/strong>!<br><strong>shMock<\/strong> is based on a very simple idea to mock and stubbing shell commands and user's functions used within a shell script to have control on a bunch of shell script lines that are planed to work together.<br>The libraries provided by <strong>shMock<\/strong> help to automate script development steps, especially CI\/DI scripts. Using <strong>shMock<\/strong> as the test framework helps to develop shell scripts faster and more structured.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Why test for shell scripts<\/h1>\n\n\n\n<p>Both on development processe flows and system automations, specially Linux-based embedded systems, scripts play a big role. Any small changes to the environment\/host or the script itself can lead to a failure. Tests help ensure that a script still works as expected after applying changes.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Quick start<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">An overview<\/h2>\n\n\n\n<p>Using <strong>shMock<\/strong> framework itself is not complicated.<br>A simple test usually coded on a sperated file (but it can also be part of the main file).<\/p>\n\n\n\n<p>A <strong>shMock<\/strong> test file contains three parts<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Header - Initializing global variables and importing required files and libraries<\/li>\n\n\n\n<li>Tests<\/li>\n\n\n\n<li>Test environment initializer and runner<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\n#: An shMOck test file template\n#---------------------------------------\n## Initializing global variables and importing libraries\n#---------------------------------------\nTESTORIGINALSCRIPT_PATH=$( dirname $(realpath \"$0\") )\nSCRIPT_PATH=$( dirname \"$0\")\nSCRIPT_NAME=myscript\n\n. $TESTORIGINALSCRIPT_PATH\/test.mock.shinc\n\n#---------------------------------------\n## Tests\n#---------------------------------------\n#@TEST\nfunction TEST_TEMPLATE ()\n{\n    return 0\n}\n\n#---------------------------------------\n## Initializing environment and running tests\n#---------------------------------------\nfunction testSetup()\n{\n    return 0\n}\n\nfunction testTeardown()\n{\n    return 0\n}\n\n# Main - run tests\n#---------------------------------------\ntestGroup=\"\"\n#testGroup=WORKING\nTEST_CASES=( $(grep -P -i -A1 \"^#@TEST\\s*$testGroup\" $0 | grep '^\\s*function' | cut -d' ' -f2) )\n\nexitCode=0\n$(testSetup)\nfor testCase in \"${TEST_CASES&#91;@]}\"\ndo\n    TESTWORK_DIR=$(bash -c \"mktemp -d\")\n    export TESTWORK_TEMPORARYFOLDER=$TESTWORK_DIR\n\n    echo -e \"\\n$testCase\"\n\n    echo \"&#91;RUN]\"\n    exitCode=1\n    $testCase\n    exitCode=$?\n    &#91; $exitCode -ne 0 ] &amp;&amp;\n        echo \"&#91;FAILED]\" &amp;&amp;\n        exitCode=1 &amp;&amp;\n        break\n\n    echo \"&#91;PASSED]\"\n\n    RESETMOCKS\n    unset TESTWORK_TEMPORARYFOLDER\n    bash -c \"rm -r \\\"$TESTWORK_DIR\\\"\"\ndone\n$(testTeardown)\n\n&#91; $exitCode -ne 0 ] &amp;&amp;\n    exit 1\n\nexit 0<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The above template can have more or less sections depending on how a script is coded.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Writing a simple test<\/h2>\n\n\n\n<p>Let's start with a simple script and two simple tests for the scripts.<\/p>\n\n\n\n<p>The script below, is a simple script that is called by ocserv (a vpn server) when a client is connected or discconnecte.<br>The script logs client connections to an sqlite db.<br>It uses several variables that are passed throgh env definations and can be not called from a terminal directlly. It is developed to be called by ocserv.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<ul class=\"wp-block-list\">\n<li>When a client is connected, the script addsa new row to the database with some fields such as <code>connectid<\/code>, <code>username<\/code> passed by ocserv<\/li>\n\n\n\n<li>When a client is disconnected, the script looks on <code>\/var\/log\/syslog<\/code> to collect more information about the client and then updated the row created for the connection with additional information such as <code>totalrx<\/code> and <code>totaltx<\/code><\/li>\n<\/ul>\n<\/blockquote>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ocservlogscript<\/code> - The script uses some external applications such as <code>grep<\/code>, <code>sqlite3<\/code> and <code>mail<\/code>. It has no function, starts from the first line and exits with code 0.<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\n\n# Script to call when a client connects and obtains an IP.\n# The following parameters are passed on the environment.\n# REASON, USERNAME, GROUPNAME, HOSTNAME (the hostname selected by client),\n# DEVICE, IP_REAL (the real IP of the client), IP_REAL_LOCAL (the local\n# interface IP the client connected), IP_LOCAL (the local IP\n# in the P-t-P connection), IP_REMOTE (the VPN IP of the client),\n# IPV6_LOCAL (the IPv6 local address if there are both IPv4 and IPv6\n# assigned), IPV6_REMOTE (the IPv6 remote address), IPV6_PREFIX, and\n# ID (a unique numeric ID); REASON may be \"connect\" or \"disconnect\".\n# In addition the following variables OCSERV_ROUTES (the applied routes for this\n# client), OCSERV_NO_ROUTES, OCSERV_DNS (the DNS servers for this client),\n# will contain a space separated list of routes or DNS servers. A version\n# of these variables with the 4 or 6 suffix will contain only the IPv4 or\n# IPv6 values.\n\n# The disconnect script will receive the additional values: STATS_BYTES_IN,\n# STATS_BYTES_OUT, STATS_DURATION that contain a 64-bit counter of the bytes\n# output from the tun device, and the duration of the session in seconds.\n\nUSAGELOGDB_PATH=\/opt\/ocservlog.db\n\n# email setting\nemailSendingFlag=0\nadminMail=\"admin@mailserver\"\nmyHostName=$(hostname)\nsystemLogfile=\/var\/log\/syslog\n\nif &#91; $REASON = \"disconnect\" ]; then\n    dtlsline=$(grep -i -e \"ocserv\\&#91;${ID}\\].*DTLS ciphersuite\" $systemLogfile)\n    dtlsvalue=\"${dtlsline##* }\"\n\n    regexinoutstr=\"in: &#91;0-9]*.*out: &#91;0-9]*\"\n    statsline=$(grep --text -i -e \"ocserv\\&#91;${ID}\\].*sent periodic stats.*\" \"$systemLogfile\" | grep -o --text -i -e \"$regexinoutstr\")\n    totalrx=${statsline\/\/*in: \/}\n    totalrx=${totalrx\/\/, out: *\/}\n    totaltx=${statsline\/\/*, out: \/}\nfi\n\n&#91; $REASON = 'connect' ] &amp;&amp;\n    sqlite3 $USAGELOGDB_PATH \"INSERT INTO usagelog (connectid,username,userip,userlocalip,localip,vpnip,devicename,connectat,status) VALUES (\\\"$ID\\\",\\\"$USERNAME\\\",\\\"$IP_REAL\\\",\\\"$IP_REMOTE\\\",\\\"$IP_LOCAL\\\",\\\"$IP_REAL_LOCAL\\\",\\\"$DEVICE\\\",\\\"$(date +%s)\\\",1);\"\n\n&#91; $REASON = \"disconnect\" ] &amp;&amp;\n    sqlite3 \"$USAGELOGDB_PATH\" \"UPDATE usagelog SET dtls=\\\"$dtlsvalue\\\",rx=\\\"$totalrx\\\",tx=\\\"$totaltx\\\",status=\"0\",disconnectat=\\\"$(date +%s)\\\" WHERE connectid=\\\"$ID\\\"\"\n\n&#91; \"${emailSendingFlag}\" = 1 ] &amp;&amp;\n    echo \"${USERNAME}: ${HOSTNAME}, ${REASON}, ${IP}: ${IP_REAL}, ${IP_LOCAL}: ${IP_REAL_LOCAL}, ${IP_REMOTE}, ${ID}, ${DEVICE}, ${dtlsvalue}, ${totalrx}, ${totaltx}\"  | mail -s \"$myHostName - ${USERNAME} ${REASON}\" \"${adminMail}\"\n\nexit 0<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>ocservlogscript.test<\/code> - We write two tests for the script above, one for connection and one for disconnection. Lets starts with two empty tests<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\n#: ocservlogscript tests\n#---------------------------------------\n## Initializing global variables and importing libraries\n#---------------------------------------\nTESTORIGINALSCRIPT_PATH=$( dirname $(realpath \"$0\") )\nSCRIPT_PATH=$( dirname \"$0\")\nSCRIPT_NAME=ocservlogscript\n\n. $TESTORIGINALSCRIPT_PATH\/test.mock.shinc\n\n#---------------------------------------\n## Tests\n#---------------------------------------\n#@TEST\nfunction TEST_OCSERVLOGSCRIPT_CONNECT ()\n{\n    return 0\n}\n\n#@TEST\nfunction TEST_OCSERVLOGSCRIPT_DISCONNECT ()\n{\n    return 0\n}\n\n#---------------------------------------\n## Initializing environment and running tests\n#---------------------------------------\nfunction testSetup()\n{\n    return 0\n}\n\nfunction testTeardown()\n{\n    return 0\n}\n\n# Main - run tests\n#---------------------------------------\ntestGroup=\"\"\n#testGroup=WORKING\nTEST_CASES=( $(grep -P -i -A1 \"^#@TEST\\s*$testGroup\" $0 | grep '^\\s*function' | cut -d' ' -f2) )\n\nexitCode=0\n$(testSetup)\nfor testCase in \"${TEST_CASES&#91;@]}\"\ndo\n    TESTWORK_DIR=$(bash -c \"mktemp -d\")\n    export TESTWORK_TEMPORARYFOLDER=$TESTWORK_DIR\n\n    echo -e \"\\n$testCase\"\n\n    echo \"&#91;RUN]\"\n    exitCode=1\n    $testCase\n    exitCode=$?\n    &#91; $exitCode -ne 0 ] &amp;&amp;\n        echo \"&#91;FAILED]\" &amp;&amp;\n        exitCode=1 &amp;&amp;\n        break\n\n    echo \"&#91;PASSED]\"\n\n    RESETMOCKS\n    unset TESTWORK_TEMPORARYFOLDER\n    bash -c \"rm -r \\\"$TESTWORK_DIR\\\"\"\ndone\n$(testTeardown)\n\n&#91; $exitCode -ne 0 ] &amp;&amp;\n    exit 1\n\nexit 0<\/code><\/pre>\n\n\n\n<p>As the test code above, <code>ocservlogscript.test<\/code> looks for <code>test.mock.shinc<\/code> (shMOck library).<br>It means to run tests it needs to find a copy of <code>test.mock.shinc<\/code> alongside the test file.<br>If <code>test.mock.shinc<\/code> is stored somewhere else, <code>ocservlogscript.test<\/code> should be updated with the correct path for <code>test.mock.shinc<\/code><\/p>\n\n\n\n<p>Now the test above can be run. The both empty tests <code>TEST_OCSERVLOGSCRIPT_CONNECT<\/code> and <code>TEST_OCSERVLOGSCRIPT_DISCONNECT<\/code> are passed (they are testsing nothing for now).<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>TEST_OCSERVLOGSCRIPT_CONNECT<\/code> (The first edition) - The test below mocks 3 command used by <code>ocservlogscript<\/code> and then initialize environment variables to simulate a <code>connect<\/code> call.<\/li>\n<\/ul>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<ul class=\"wp-block-list\">\n<li>The command <code>ADDMOCK grep<\/code> takes controll over <code>grep<\/code> calls and logs them (number of calls, passed argumants and outpts). Using <code>ADDMOCK grep<\/code> without extra parameters does not chagne the functionality of <code>gerp<\/code>. It works as a stub, logs the call and then run the original command at the end.<\/li>\n\n\n\n<li><code>ExpectCalls grep:0 sqlite3:1 mail:0<\/code> checks number of calls for <code>grep<\/code>, <code>sqlite3<\/code> and <code>mail<\/code> For example in the case of a <code>connect<\/code> we expect only 1 <code>sqlite3<\/code> call.<\/li>\n<\/ul>\n<\/blockquote>\n\n\n\n<pre class=\"wp-block-code\"><code>function TEST_OCSERVLOGSCRIPT_CONNECT ()\n{\n    ADDMOCK grep\n    ADDMOCK sqlite3\n    ADDMOCK mail\n\n    REASON=connect\n    ID='id'\n    USERNAME='USERNAME'\n    IP_REAL='IP_REAL'\n    IP_REMOTE='IP_REMOTE'\n    IP_LOCAL='IP_LOCAL'\n    IP_REAL_LOCAL='IP_REAL_LOCAL'\n    DEVICE='DEVICE'    \n    .\/ocservlogscript\n    exitCode=$?\n    &#91; $exitCode -ne 0 ] &amp;&amp;\n        return 1\n\n    ExpectCalls grep:0 sqlite3:1 mail:0\n    &#91; $? -ne 0 ] &amp;&amp;\n        return 1\n\n    return 0\n\n}<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>TEST_OCSERVLOGSCRIPT_CONNECT<\/code> (The second edition) - As explained for the test above using <code>ADDMOCK sqlite3<\/code> without addtional parameters create a stub. It means <strong>shMock<\/strong> logs <code>sqlite3<\/code> calls but the original command will be also run. In the other words the test above tries to write to a sqlite db which is not a good idea for a test case.<\/li>\n<\/ul>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<ul class=\"wp-block-list\">\n<li>To avoid running orginal commands it needs define return codes and outputs for mocked commands.<\/li>\n\n\n\n<li><code>ADDMOCK sqlite3 $(mockCreateParamList {0,}) $(mockCreateParamList {'-',})<\/code> defines return codes and outputs for <code>sqlite3<\/code>.<\/li>\n\n\n\n<li>The first argumant after <code>ADDMOCK sqlite3<\/code> refers to return codes (<code>$(mockCreateParamList {0,})<\/code>). It means <strong>shMock<\/strong> will set the return code to 0 for all <code>sqlite3<\/code><\/li>\n\n\n\n<li>The second argumant after <code>ADDMOCK sqlite3<\/code> refers to outputs (<code>$$(mockCreateParamList {'-',})<\/code>). It means <strong>shMock<\/strong> will print nothing for all <code>sqlite3<\/code><\/li>\n<\/ul>\n<\/blockquote>\n\n\n\n<pre class=\"wp-block-code\"><code>function TEST_OCSERVLOGSCRIPT_CONNECT ()\n{\n    ADDMOCK grep\n    ADDMOCK sqlite3 $(mockCreateParamList {0,}) $(mockCreateParamList {'-',})\n    ADDMOCK mail\n\n    REASON=connect\n    ID='id'\n    USERNAME='USERNAME'\n    IP_REAL='IP_REAL'\n    IP_REMOTE='IP_REMOTE'\n    IP_LOCAL='IP_LOCAL'\n    IP_REAL_LOCAL='IP_REAL_LOCAL'\n    DEVICE='DEVICE'    \n    .\/ocservlogscript\n    exitCode=$?\n    &#91; $exitCode -ne 0 ] &amp;&amp;\n        return 1\n\n    ExpectCalls grep:0 sqlite3:1 mail:0\n    &#91; $? -ne 0 ] &amp;&amp;\n        return 1\n\n    return 0\n\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Some real case examples<\/h3>\n\n\n\n<p>https:\/\/github.com\/tOSuser\/whMan\/blob\/main\/whmanager\/tests\/whman.block.test.sh<br>https:\/\/github.com\/tOSuser\/whMan\/blob\/main\/whmanager\/tests\/whman.unit.test.sh<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Jenkinsjob - A very simple example<\/h2>\n\n\n\n<p>This example shows how to test a script that is developed to use with in a groovy script.<br>The groovy script used by this example is only one line that calls a bash script and it is not considered by this example.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>jenkinsjob.sh<\/code> - The main script<\/li>\n\n\n\n<li><code>jenkinsjobshelper.shinc<\/code> - provide some helper functions used by <code>jenkinsjob.sh<\/code><\/li>\n\n\n\n<li><code>jenkinsjob.test.sh<\/code> - <code>jenkinsjob.sh<\/code> tests<\/li>\n<\/ul>\n\n\n\n<p>need-to-be-updated<\/p>\n","protected":false},"excerpt":{"rendered":"<p>shMock &#8211; A simple test framework for shell scripts such as bash License license AGPL-3.0 This code and the package of shMock are free software: you can redistribute it and\/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-35","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/posts\/35","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/comments?post=35"}],"version-history":[{"count":4,"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/posts\/35\/revisions"}],"predecessor-version":[{"id":41,"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/posts\/35\/revisions\/41"}],"wp:attachment":[{"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/media?parent=35"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/categories?post=35"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.osxx.com\/index.php\/wp-json\/wp\/v2\/tags?post=35"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}