Bats Error Command Not Found and Load Problems Fully
Fix Bats errors: bats command not found, load: command not found, helper libraries not found, bats-core install methods, PATH, submodules and CI setup.

Two different errors share the word Bats. One is at the start, bats: command not found, and the other appears inside tests, load: command not found or bats: Could not find library 'bats-assert'. Both are installation and path problems, and both are fixed once you decide how the project gets its test tools. This is part of the setup story in Bats tests in CI. The commands assume bats-core 1.11.
Error 1: bats: command not found
command -v bats || echo "not installed"
sudo apt install bats # Debian/Ubuntu (version may be old)
brew install bats-core # macOS
npm install --save-dev bats # project-local
git submodule add https://github.com/bats-core/bats-core.git test/bats
bats --versionnot installed
Bats 1.11.0
The distribution package is the easiest and often one or two releases old, which matters if you want newer features such as --jobs improvements or bats_load_library. The npm and submodule routes pin a version in the repository so everyone, including CI, runs the same one. After npm install, the binary is at ./node_modules/.bin/bats and npx bats finds it; with a submodule, call test/bats/bin/bats. If you installed with a Git checkout, run its install.sh /usr/local.
Make it findable
BATS := $(shell command -v bats || echo test/bats/bin/bats)
test:
$(BATS) tests/
export PATH="$PWD/node_modules/.bin:$PATH"
# or a tiny wrapper
cat > run-tests.sh <<'EOF'
#!/usr/bin/env bash
exec "$(dirname "$0")/test/bats/bin/bats" "$@"
EOF
chmod +x run-tests.shA wrapper or a Makefile target means nobody has to remember the path. In a cron job or CI step, remember the limited PATH described in command not found in cron: use an absolute path or set the PATH explicitly. The Makefile fragment above is shown as text.
Error 2: load: command not found
tests/foo.bats: line 3: load: command not found
load is provided by Bats while it runs a .bats file. Seeing "command not found" means the file is being run by Bash directly (bash tests/foo.bats or ./tests/foo.bats with a Bash shebang) instead of by bats. Run bats tests/foo.bats. Likewise a helper file loaded with load that itself calls load works only inside Bats.
Error 3: cannot find the helper library
git submodule add https://github.com/bats-core/bats-support.git test/test_helper/bats-support
git submodule add https://github.com/bats-core/bats-assert.git test/test_helper/bats-assert
git submodule update --init --recursivesetup() {
load 'test_helper/bats-support/load'
load 'test_helper/bats-assert/load'
}bats: ... 'test_helper/bats-support/load' does not exist
load paths are relative to the test file's directory, not to the project root or the current directory, so a test in tests/unit/ needs ../test_helper/.... A fresh clone without --recurse-submodules leaves the helper directories empty, a common CI failure; check out with submodules enabled, as in git submodule. The helpers are separate projects, and nothing comes with bats-core itself except the core.
Library search path
export BATS_LIB_PATH=/usr/lib/bats:$PWD/test/test_helper
bats tests/setup() {
bats_load_library bats-support
bats_load_library bats-assert
}bats_load_library (bats-core 1.7 and later) searches the directories in BATS_LIB_PATH for the library name, like a library path, so tests do not hardcode relative paths. Package managers install helper libraries under /usr/lib/bats or Homebrew's prefix, so the variable lets one test file work on a laptop and in CI. Put the export in the Makefile target or in a .batsrc-style wrapper.
CI setup
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- run: sudo apt-get install -y bats
- run: bats --version && bats tests/Print the version in CI to catch drift. A common pitfall is a distribution Bats that predates a feature your tests use. Pin by npm or submodule when that matters.
Why test tooling needs to be pinned
Test frameworks change, and so do their helper libraries. A test that relies on assertion helpers from one release may use a function that a distribution's older package lacks, and the result is a pass on one machine and a confusing failure on another. The cause is rarely the tests. It is that the project never said which versions it needs. Writing that down, by pinning in a submodule, a package manifest or a container image, is the same discipline you apply to the application's own dependencies, and it removes this class of failure completely.
Decide where the tools live and apply that choice to every environment. A developer's laptop, the CI runner and a colleague's machine should all get Bats the same way, preferably through a single command in the repository such as make test-deps. If tests need a manual installation step documented in a wiki, the step will be skipped by the next new contributor, and the first sign will be this error message.
Path problems that look like install problems
Sometimes the tool is installed and the shell cannot find it. A package installed under a user prefix such as ~/.local/bin or a language manager's directory is invisible to a shell that did not load the profile. This is the usual story in CI scripts and cron entries, where a non-interactive shell skips the files that extend the path. Print echo "$PATH" and command -v bats at the start of the failing step; the answer is nearly always visible in those two lines.
Containers repeat the lesson. The test image may install Bats as one user and run as another, or install it in a build stage that is not copied into the final stage. A RUN command -v bats line at the end of the Dockerfile fails the build when the tool is missing, which is a better time to find out than during a pipeline run.
Library and helper loading, understood properly
Bats loads helpers by evaluating files in the test's shell, so a helper behaves as if its text were pasted at the point of the call. Relative paths resolve against the directory of the test file that calls load. A nested helper that loads another helper also resolves relative to the original test file, not to its own location, which surprises people who organise helpers into subdirectories. Keep one flat helper directory, or load by library name through the search path, and the surprise disappears.
Name collisions are the second hazard. Two helpers that define a function with the same name will silently override each other depending on load order. Prefix project helper functions with the project name, and avoid names that collide with the standard assertion library, such as assert_equal. When a test behaves strangely after adding a helper, run declare -F inside a test and look for duplicates.
Keeping the setup healthy
Add a smoke test that only checks the harness: it loads the libraries and asserts that one trivial condition holds. When it fails, the cause is the environment, not the code, and the failure message says so in the first line of the report. Run it first in CI. This separates "the tooling is broken" from "the code is broken", which saves a surprising amount of debugging time, because people otherwise start reading application code for a problem that sits in the install.
Where this does not apply
If tests start but fail with assertion errors, the problem is the tests, not the install. Windows needs Git Bash or WSL to run Bats. See the Bats installation guide and the bats_load_library documentation.


