Bach
Unit testing framework for developing cross-platform Bash unit tests
Install / Use
/learn @bach-sh/BachREADME
Bach Unit Testing Framework
Bach
Bach is a Bash testing framework, can be used to test scripts that contain dangerous commands like rm -rf /. No surprises, no pain.
- Website: https://bach.sh
- Repo: https://github.com/bach-sh/bach
- 查看本文档的中文版
Getting Started
Bach Unit Testing Framework is a real unit testing framework. All commands in the PATH environment variable become external dependencies of bash scripts being tested. No commands can be actually executed. In other words, all commands in Bach test cases are dry run. Because that unit tests should verify the behavior of bash scripts, not test commands. Bach Testing Framework also provides APIs to mock commands.
Prerequisites
Installing
Installing Bach Testing Framework is very simple. Download bach.sh to your project, use the source command to import bach.sh.
For example:
source path/to/bach.sh
A complete example
#!/usr/bin/env bash
source bach.sh
test-rm-rf() {
# Write your test case
project_log_path=/tmp/project/logs
rm -rf "$project_log_ptah/" # Typo here!
}
test-rm-rf-assert() {
# Verify your test case
rm -rf / # This is the actual command to run on your host!
# DO NOT PANIC! By using Bach Testing Framework it won't actually run.
}
test-rm-your-dot-git() {
# Mock `find` command with certain parameters, will output two directories
@mock find ~ -type d -name .git === @stdout ~/src/your-awesome-project/.git \
~/src/code/.git
# Do it, remove all .git directories
find ~ -type d -name .git | xargs -- rm -rf
}
test-rm-your-dot-git-assert() {
# Verify the actual command
rm -rf ~/src/your-awesome-project/.git ~/src/code/.git
}
See tests/bach-testing-framework.test.sh for more examples.
How to run tests?
It's recommended to use a shebang at the top of your test file:
#!/usr/bin/env bash
Using #!/usr/bin/env bash is preferable to #!/bin/bash because it looks for the bash executable in the user's PATH. This is particularly important on operating systems like macOS, where the default /bin/bash is an outdated version 3.x, while Bach requires Bash v4.3 or newer. A newer version of Bash installed via Homebrew (brew install bash) is typically located in a path like /usr/local/bin/bash, and #!/usr/bin/env bash will ensure this correct, newer version is used.
After setting the shebang, make the test file executable and run it directly (recommended):
chmod +x your-test-file.test.sh
./your-test-file.test.sh
Alternatively, you can run the test script directly with the bash command:
bash your-test-file.test.sh
Important: Do not use the source command to run your test files (e.g., source your-test-file.test.sh). Sourcing the script runs it within your current shell session, which can pollute your environment and lead to unexpected side effects. Test files should be executed in a separate subshell process to ensure a clean, isolated testing environment.
On Windows
Make sure to use for shebang
#!/bin/bash
and not
#!/bin/sh
If on Cygwin (as opposed to Git Bash), the end of line sequence of bach.sh should be LF.
Write test cases
Unlike the other testing frameworks, A standard test case of Bach is composed of two Bash functions. One is for running tests, the other is for asserting. Bach will run the two functions separately and then compare whether the same sequence of commands will be executed in both functions. The name of a testing function must start with test-, the name of the corresponding asserting function ends with -assert.
For example:
source bach.sh
test-rm-rf() {
project_log_path=/tmp/project/logs
sudo rm -rf "$project_log_ptah/" # Typo!
# An undefined bash variable is an empty string, which can be a serious problem!
}
test-rm-rf-assert() {
sudo rm -rf /
}
Bach will run the two functions separately, test-rm-rf and test-rm-rf-assert. In the testing function, test-rm-rf, the final actual command to be executed is sudo rm -rf "/". It's the same as the asserting function test-rm-rf-assert. So this test case passes.
If Bach does not find the asserting function for a testing function. It will try to use a traditional test method. In this case, the testing function must have a call to assert the APIs. Otherwise, the test case will fail.
For example:
test-single-function-style() {
declare i=2
@assert-equals 4 "$((i*2))"
}
If Bach does not find the corresponding asserting function and there is no assertion API call in the testing function, the test case must fail.
If the name of a test case starts with test-ASSERT-FAIL, it means that the asserting result of this test case is reversed. That is, if the asserting result is successful, the test case fails, if the asserting result fails, the test case is successful.
The assertion APIs of Bach Testing Framework:
@assert-equals@assert-fail@assert-success
Mock commands
There are mock APIs in the Bach test framework that can be used to mock commands and scripts.
The Mock APIs:
@mock@ignore@mockall@mocktrue@mockfalse@@mock
But it doesn't allow to mock the following built-in commands in Bach Testing Framework:
builtindeclareevalsetunsettruefalseread
Test cases will fail if you attempt to mock these built-in commands. If they are needed in the script under test, we can extract a new function which contains the built-in commands in our scripts, and then use Bach to mock this new function.
Run the actual commands in Bach
In order to make test cases fast, stable, repetitive, and run in random order. We should write unit-testing cases and avoid calling real commands. But Bach also provides a set of APIs for executing real commands.
Bach mocks all commands by default. If it is unavoidable to execute a real command in a test case, Bach provides an API called @real to execute the real command, just put @real at the beginning of commands.
Bach also provides APIs for commonly used commands. The real commands for these APIs are obtained from the system's PATH environment variable before Bach starts.
These common used APIs are:
@cd@command@echo@exec@false@popd@pushd@pwd@set@trap@true@type@unset@eval@source@cat@chmod@cut@diff@find@env@grep@ls@shasum@mkdir@mktemp@rm@rmdir@sed@sort@tee@touch@which@xargs
command and xargs are a bit special. Bach mocks both commands by default to make the similar behavior of themselves.
In Bach Testing Framework the xargs is a mock function. It's behavior is similar to the real xargs command if you put -- between xargs and the command. But the commands to be executed by xargs are dry run.
For examples:
test-xargs-no-dash-dash() {
@mock ls === @stdout foo bar
ls | xargs -n1 rm -v
}
test-xargs-no-dash-dash-assert() {
xargs -n1 rm -v
}
test-xargs() {
@mock ls === @stdout foo bar
ls | xargs -n1 -- rm -v
}
test-xargs-assert() {
rm -v foo
rm -v bar
}
test-xargs-0() {
@mock ls === @stdout foo bar
ls | xargs -- rm -v
}
test-xargs-0-assert() {
rm -v foo bar
}
We can also mock the test command [ ... ]. But it will keep the original behavior if we don't mock it.
For examples:
test-if-string-is-empty() {
if [ -n "original behavior" ] # We did not mock it, so this test keeps the original behavior
then
It keeps the original behavior by default # We should see this
else
It should not be empty
fi
@mockfalse [ -n "Non-empty string" ] # We can reverse the test result by mocking it
if [ -n "Non-empty string" ]
then
Non-empty string is not empty # No, we cannot see this
else
Non-empty string should not be empty but we reverse its result
fi
}
test-if-string-is-empty-assert() {
It keeps the original behavior by default
Non-empty string should not be empty but we reverse its result
}
# Mocking the test command `[ ... ]` is useful
# when we want to check whether a file with absolute path exists or not
test-a-file-exists() {
@mocktrue [ -f /etc/an-awesome-config.conf ]
if [ -f /etc/an-awesome-config.conf ]; then
Found this awesome config file
else
Even though this config file does not exist
fi
}
test-a-file-exists-assert() {
Found this awesome config file
}
Configure Bach
There are some environment variables starting with BACH_ for configuring Ba
Related Skills
gh-issues
347.0kFetch GitHub issues, spawn sub-agents to implement fixes and open PRs, then monitor and address PR review comments. Usage: /gh-issues [owner/repo] [--label bug] [--limit 5] [--milestone v1.0] [--assignee @me] [--fork user/repo] [--watch] [--interval 5] [--reviews-only] [--cron] [--dry-run] [--model glm-5] [--notify-channel -1002381931352]
node-connect
347.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
107.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
Writing Hookify Rules
107.8kThis skill should be used when the user asks to "create a hookify rule", "write a hook rule", "configure hookify", "add a hookify rule", or needs guidance on hookify rule syntax and patterns.
