Sysadmin Hell

Bats testing and mocks

In my last post, I made a script and tested it with bats. The tests, however, didn’t simulate side effects… they caused them. Files were written, commands executed - it was more of a live-fire exercise than a controlled test. This time, I’m doing things properly. Instead of unleashing real-world chaos, I’ll be mocking the side effects and testing the logic without actually pulling any wires.

Project

This time around, let's organise our project. Below is the directory structure

├── check_service.sh
└── test
    ├── check_service_test.bats
    └── mocks
        ├── systemctl.error
        ├── systemctl.fail
        └── systemctl.success

We'll have our script in the root directory. Inside the test/ directory, we keep our test files and a subdirectory for mocks.

Script

This time, it's the script that checks systemctl on whether the service is running. I use the same template as before:

#!/bin/bash
set -euo pipefail

check_service() {
  local svc=$1
  if systemctl is-active --quiet "$svc"; then
    echo "$svc is running"
  else
    echo "$svc is down"
  fi
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  "$@"
fi

and can be ran as such:

$ ./check_service.sh check_service cron
cron is running

Next, we’ll test three scenarios: service running, service down, and systemctl throwing an error.

systemctl.success

#!/bin/bash

exit 0

systemctl.fail

#!/bin/bash
exit 3

systemctl.error

#!/bin/bash
echo "systemctl: internal error" >&2
exit 1

Tests

In this test I test fictional 'iptables-roulette' service.

#!/usr/local/bin/bats

setup() {
  export PATH="$BATS_TEST_DIRNAME/mocks:$PATH"
}

@test "check_service reports running service" {
  cp "$BATS_TEST_DIRNAME/mocks/systemctl.success" "$BATS_TEST_DIRNAME/mocks/systemctl"
  chmod +x "$BATS_TEST_DIRNAME/mocks/systemctl"

  run "$BATS_TEST_DIRNAME/../check_service.sh" check_service iptables-roulette
  [ "$status" -eq 0 ]
  [ "$output" = "iptables-roulette is running" ]
}

@test "check_service reports down service" {
  cp "$BATS_TEST_DIRNAME/mocks/systemctl.fail" "$BATS_TEST_DIRNAME/mocks/systemctl"
  chmod +x "$BATS_TEST_DIRNAME/mocks/systemctl"

  run "$BATS_TEST_DIRNAME/../check_service.sh" check_service iptables-roulette
  [ "$status" -eq 0 ]
  [ "$output" = "iptables-roulette is down" ]
}

@test "check_service handles systemctl error" {
  cp "$BATS_TEST_DIRNAME/mocks/systemctl.error" "$BATS_TEST_DIRNAME/mocks/systemctl"
  chmod +x "$BATS_TEST_DIRNAME/mocks/systemctl"

  run "$BATS_TEST_DIRNAME/../check_service.sh" check_service iptables-roulette
  [ "$status" -eq 0 ]
  [[ "$output" == *"is down"* ]]
}

As you can see from this part:

cp "$BATS_TEST_DIRNAME/mocks/systemctl.error" "$BATS_TEST_DIRNAME/mocks/systemctl"
chmod +x "$BATS_TEST_DIRNAME/mocks/systemctl"

Test will assume systemctl from mocks directory if it's present. First line just copies the right mock to simulate or scenario.

Running the tests

Now to run it, just like before, we just need to run the bats file.