Open-source project
bats-core/bats-core avatar
bats-core/bats-core

bats-core: a TAP-compliant test runner for Bash scripts

Bash Automated Testing System

6,289 stars495 forksShellNOASSERTION

At a glance

What is it?
bats-core wraps ordinary shell commands in named test cases and reports results as TAP. It is the maintained fork of the original Bats, and it is aimed at people who write and ship shell code.
Who is it for?
Adopt bats-core if your deliverables are shell scripts and you want tests that run in the same interpreter as the code, with TAP output that CI systems already understand. Skip it if you need coverage measurement, mocking of external services, or structured assertions out of the box; those come from companion libraries rather than the core.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What bats-core is for, and who ends up using it

Shell scripts fail in ways that ordinary unit-test frameworks handle badly. A script that parses arguments, moves files and calls curl is not a function you can import, and the bugs live in exit codes, quoting and environment state. bats-core addresses that gap by treating a test file as a Bash script with a special syntax for test cases. The README puts it plainly: a test file is "a Bash script with special syntax for defining test cases", and under the hood each case is a function with a description.

The intended audience is anyone shipping UNIX programs, with Bash being the primary target. The README notes that Bats is most useful for testing software written in Bash but that you can use it to test any UNIX program. In practice that means maintainers of installers, CI helper scripts, dotfile managers and CLI wrappers. It also means projects that already use shell for deployment and want those steps covered by the same test run as the rest of the pipeline.

The project is a fork. The README explains that a call for maintainers on the original Bats repository went unanswered because write access could not be obtained, and that the fork was created on 2017-09-19 from commit 0360811. The original repository was archived on 2021-04-29 and is read-only. That history matters when you search for documentation: older tutorials describe the pre-fork tool, and some of their advice no longer applies.

How a test file becomes a TAP stream

The execution model is deliberately thin. bats-core runs each test case with Bash's errexit option enabled, so a test case passes only if every command inside it exits with status 0. The README describes the consequence directly: each line is an assertion of truth. There is no separate assertion API to learn before you can write your first test, which is why the example in the README compares the output of bc and dc with a plain bracket test.

Results are emitted as TAP, the Test Anything Protocol. That is the architectural decision with the longest reach: any CI system, reporting tool or TAP consumer can read the output without knowing anything about Bash. The repository also carries junit and xunit topics, so converting results into those formats is part of the expected workflow rather than an afterthought.

The repository layout separates concerns in a way that is worth knowing before you debug something. The bin directory holds the entry point, lib and libexec hold the implementation, man holds manual pages, and test holds the project's own suite, which package.json runs with bin/bats test. A shellcheck.sh script and a .pre-commit-config.yaml sit at the top level, so the project lints itself as part of its own workflow. If you plan to patch bats-core rather than just use it, those two files tell you what your patch will be checked against.

Installing bats-core and running a first test

There are several install paths, and they are not equivalent. The npm package is named bats, and the published package version tracks the release, with 1.14.0 as the current one. The repository also ships install.sh and uninstall.sh at the top level, and package.json includes both in the files list it publishes, which is a signal that script-based installation is a supported route rather than a leftover.

If you prefer containers, the repository contains a Dockerfile and a compose.yaml. The compose file defines a single service named bats, builds from the Dockerfile with the repository root as context, sets user to root, and mounts the working directory at /opt/bats. The image is built from the official bash image, installs tini as the entrypoint wrapper, and installs four companion libraries at pinned versions: support 0.3.0, file 0.4.0, assert 2.1.0 and detik 1.3.2. That pinning is the useful detail. A container run gives you a known-good set of libraries, while an npm or Homebrew install gives you the runner and leaves the libraries to you.

Once the binary is on your PATH, a test file looks like this. Save it as addition.bats, with the shebang pointing at bats, and define cases with the @test keyword followed by a description in quotes.

bash
#!/usr/bin/env bats

@test "addition using bc" {
  result="$(echo 2+2 | bc)"
  [ "$result" -eq 4 ]
}

Run it with the TAP flag and a path. The README's own testing section uses exactly this form against the project's test directory.

bash
bin/bats --tap test

You should see one TAP line per test case, with ok or not ok followed by the description. A failing case does not stop the run; the remaining cases still execute, which is what makes the output useful in CI. If you are running from a checkout rather than an installed binary, bin/bats is the path to use, since that is the entry point package.json invokes for the project's own suite.

Where bats-core stops and companion libraries begin

The core is small on purpose, and that is the main limitation to plan around. There is no assertion library, no mocking facility and no coverage measurement in the runner itself. The Dockerfile tells you where those live: it installs bats-support, bats-file, bats-assert and bats-detik as separate artifacts at their own versions. bats-assert is the one people expect to be built in, and it is not.

The practical consequence is a version-matching problem. Your test files will call helpers from those libraries, and the runner does not vendor them. On a container run the versions are fixed by the image build arguments, so tests behave the same everywhere. On a developer machine, the libraries come from wherever that developer installed them, and a mismatch between the local copy and the CI copy is a real source of confusing failures. Pin them explicitly in whatever install path you choose.

The second limitation is Bash itself. The README states the supported floor is Bash 3.2 or above. That floor exists to cover macOS, which ships an old Bash, but it also means the runner cannot assume newer shell features. If your tests rely on associative arrays or other constructs added after 3.2, you are working against the framework's stated compatibility target rather than with it.

A third case where bats-core is the wrong tool: coverage. The related searches include bats core coverage, and the repository material does not describe a coverage mechanism. If line coverage of your shell code is a requirement, bats-core will run your tests but will not tell you which lines they touched.

bats-core compared with writing tests as plain shell scripts

The obvious alternative is not another framework but no framework: a directory of shell scripts that each set up state, run the thing under test, and exit non-zero on failure. That approach has real advantages. There is nothing to install, and the scripts run under whatever shell the developer already has.

The difference shows up in reporting and in isolation. A plain script exits at the first failure, so one broken assertion hides everything after it. bats-core runs each case independently and reports per-case results as TAP, which is why the output can be fed to a CI system without a custom parser. The @test syntax also gives each case a name that appears in the report, whereas a plain script gives you a filename and a line number.

The cost of that structure is a dependency and a syntax. Your test files are no longer portable shell; they need bats to run. For a project with a handful of one-off scripts, a plain script plus set -e may be the better trade. For a project where shell is a shipped deliverable and failures need to be attributable to a named case in CI, the TAP output is worth the dependency.

Maintenance, releases and the licence question

The last push to the default branch was on 2026-09-21, and the most recent release is v1.14.0 from 2026-07-21, preceded by v1.13.0 in 2025-11-07 and v1.12.0 in 2025-05-18. The cadence over that window is roughly two releases a year, which is slow but not stalled. The repository is not archived. For a test runner whose job is to keep working, a slow cadence is not automatically a problem, but it does mean you should not expect new features on a schedule.

Upgrade cost is mostly about the companion libraries rather than the runner. The Dockerfile pins support, file, assert and detik at fixed versions, so a container-based workflow upgrades them when the image is rebuilt. A source or npm install upgrades only the runner, and the libraries move independently. Read docs/CHANGELOG.md, which the README points to for version history, before bumping, and check whether your tests depend on behavior that changed.

The licence situation needs a caveat. The README says Bats is released under an MIT-style license and points to LICENSE.md, and the npm package declares MIT in its license field. The repository metadata reports the licence as NOASSERTION, meaning the automated classifier could not confirm a standard identifier. Those two statements are not in conflict, but the discrepancy is worth resolving by reading LICENSE.md directly if your organisation has strict licence review. Nothing here is legal advice; treat the file itself as the source.

Editorial conclusion

Adopt bats-core if your deliverables are shell scripts and you want tests that run in the same interpreter as the code, with TAP output that CI systems already understand. Skip it if you need coverage measurement, mocking of external services, or structured assertions out of the box; those come from companion libraries rather than the core. Before committing, verify which install path your environment supports, since npm, Homebrew, Docker and install.sh each place the binary differently, and confirm that the bats-support and bats-assert versions your tests assume are the ones your runner actually loads.

Frequently asked questions

What is bats-core used for?

It is a TAP-compliant testing framework for Bash 3.2 and above, used to verify that UNIX programs behave as expected. A test file is a Bash script with @test blocks, and each block passes when every command inside it exits with status 0.

What is bats-core?

It is the community-maintained fork of the original Bats testing framework. The README states the fork was created on 2017-09-19 because write access to the original repository could not be obtained, and the original was archived on 2021-04-29.

How do I install bats-core?

The repository ships install.sh and uninstall.sh, publishes an npm package named bats, and provides a Dockerfile with a compose.yaml that builds a service named bats. package.json lists install.sh in its published files, so the script route is a supported one.

Does bats-core include an assertion library?

No. The runner itself has no assertion library. The Dockerfile installs bats-support, bats-file, bats-assert and bats-detik as separate artifacts at pinned versions, so they are companions rather than part of the core.

Which Bash versions does bats-core support?

The README states that Bats is a TAP-compliant testing framework for Bash 3.2 or above. That floor keeps it usable on systems shipping an older Bash, but it also limits which shell features a test can rely on.

Official sources

  1. bats-core/bats-core on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/bats-core-bats-core.svg)](https://hysenlabs.com/projects/bats-core-bats-core)