Hero Image
- Philipp Ludewig

Let's assert and stub for bash scripts

Aloha people,

In my last post about bats-core, I described how you can start testing bash scripts. Today I want to share some of my learnings regarding stubbing with Bats-Mock. I won't lie, when I saw the two repositories for mocking. I went straight with Jason Karns repository. How to stub a command like date looked so much easier than from grayhemp. The problem with that: it doesn't work. The last change that was made to Jason Karns repository was eight years ago. In the meantime, bats-core must have changed in a way that made this repository unusable. I wasn't able to stub or unstub properly, and spent hours figuring out if I could make it work. I reached a point where I looked for help in the only place a developer would go: Stack Overflow. The side did not disappoint, as I found what I needed. The user dragon788 wrote a comprehensive explanation about both repositories and helped me to understand grayhemps bats-mock. I whole heartily can say, ditch Jason Karns repository and just use grayhemps.

The README of grayhemps bats-mock contains an example in which the scripts expect an environment variable to be set that contains a path to the executable. I haven't seen a script that does that to be honest. Therefore, I was a little confused about how I should add the mocks to the PATH. I found two different solutions to this and one from the user dragon788 and another from the user endreymarcell. I prefer the solution from endreymarcell as it's simpler and more readable.

setup() {
    load /test_frameworks/test_helper/bats-support/load
    load /test_frameworks/test_helper/bats-assert/load
    load /test_frameworks/test_helper/bats-mock/load

    export mock_date_path
    mock_date_path="$(mock_create)"

    export -f date
}

teardown() {
    export -n date
}

date() {
    bash "${mock_date_path}" "${@}"
}

@test 'Stub date so it returns a specific date' {
    mock_set_output "${mock_date_path}" "1460967598.184561556"

    run date 

    assert_output "1460967598.184561556"
}

In this example I have stubbed date through exporting a function with the same name that points to the mock_date_path. I have done this directly in the test file but we could also create a setup script that does this for us as we likely reruse the date function in other scripts. Here is an example from the spoon repository.

Let's talk about stubbing. Bats-mock has three methods to stub the behaviour of a mock mock_set_status , mock_set_output, and mock_set_side_effect . These functions can also be used to stub the behaviour of several calls to the same mock by adding a counter. The counter starts at 1. The function mock_set_output lets you set the output that the mock would produce, and mock_set_side_effect enables you to do something when the mock was called. An example for this could be to copy a test file to a specific folder. The example can be found here. I observed that the mocks act more like a spy and don't overwrite the shell commands, although I had added them at the start of the PATH. I had the case where the shell command unzip would still expect a file and broke the test. Furthermore, I had expected that the mock would be chosen over the actual executable. After I supplied a test file the shell command could unzip, I was able to verify the args to the unzip mock successfully..

@test 'Stub date so it behaves differently on three calls' {
    # Just wanna show we can inject values into the side effect string
    temp_dir=$(mktemp -d)
    mock_set_output "${mock_date_path}" "111" 1
    mock_set_side_effect "${mock_date_path}" "touch $temp_dir/someFile" 2
    mock_set_output "${mock_date_path}" "222" 3

    run date 
    assert_output "111"

    run date 
    assert_file_exists "$temp_dir/someFile"

    run date 
    assert_output "222"

    rm "$temp_dir/someFile"
}

Now that we have stubbed some mocks, it's time to verify the calls to those mocks. For this bats-mock has four functions mock_get_call_num, mock_get_call_user, mock_get_call_args and mock_get_call_env. In order to use the functions, you will need to combine them with assert_equal from bats-core. Similar to the stubbing, it is also possible to verify specific calls to a mock with a counter. Here is the example file:

@test 'Verify that stub date is called with the argument' {
    mock_set_output "${mock_date_path}" "1460967598.184561556"

    date --utc 

    assert_equal "$(mock_get_call_args ${mock_date_path} 1)" "--utc"
}

I hope this will help people have an easier start with bats-mock. Check out the repository about my bats-mock learnings here.