Skip to content
Back to skills

Bats Testing

BSecurity

Configures Bats-core (Bash Automated Testing System) for testing CLI tools, shell scripts, and Unix programs - `.bats` test files with `@test` blocks, `run` to capture command exit + output, `[ \"$status\" -eq 0 ]` and `[ \"$output\" = ... ]` assertions, `setup`/`teardown` hooks, `load` for shared helpers, parallel execution via `--jobs N`, TAP-compliant output for CI integration. Use whenever the unit-under-test is a shell script, CLI binary, or anything invokable from Bash.

  • 10 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 12, 2026
testingshellbashdockertestinggitdatabase

Works with

  • cli

Security analysis

B89/100
  • highPerforms destructive filesystem operations
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 3 files and shows the line behind each finding

Scanned October 12, 2026

npx -y skills add testland/qa --skill bats-testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Bats Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Bats Testing
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/testland-bats-testing/badge)](https://www.skillsdirectory.com/skills/testland-bats-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: bats-testing
description: "Configures Bats-core (Bash Automated Testing System) for testing CLI tools, shell scripts, and Unix programs - `.bats` test files with `@test` blocks, `run` to capture command exit + output, `[ \"$status\" -eq 0 ]` and `[ \"$output\" = ... ]` assertions, `setup`/`teardown` hooks, `load` for shared helpers, parallel execution via `--jobs N`, TAP-compliant output for CI integration. Use whenever the unit-under-test is a shell script, CLI binary, or anything invokable from Bash."
---

# bats-testing

## Overview

Per [bats-readthedocs][bats]:

[bats]: https://bats-core.readthedocs.io/en/stable/

> "Bats (Bash Automated Testing System) is a TAP-compliant testing
> framework for Bash 3.2 or above."

Per [bats-github][bgh]:

[bgh]: https://github.com/bats-core/bats-core

> "Bats is a TAP-compliant testing framework for Bash 3.2 or above.
> It provides a simple way to verify that the UNIX programs you
> write behave as expected."

Bats works "with any Unix program," not just Bash - it captures
stdout / stderr / exit code of any executable.

## When to use

- Testing a shell script (deployment scripts, CI scripts,
  installers).
- Testing a CLI binary's interface (commands, flags, exit codes,
  output) at the integration layer.
- Validating that wrapper scripts (e.g., `kubectl` plugins, `git`
  aliases) behave as expected.
- Cross-platform smoke tests for a CLI's install scripts.

## How to use

1. Install `bats` (npm / Homebrew / git submodule); pin via submodule for CI determinism (Step 1).
2. Create `test/<feature>.bats` starting with `#!/usr/bin/env bats` and one `@test` block per behavior (Step 2).
3. Wrap each command under test with `run`, then assert on `$status`, `$output`, and `$lines[]` (Step 3).
4. Add `setup` / `teardown` with `mktemp -d` so each test is isolated and parallel-safe (Step 4).
5. `load` shared helpers plus bats-support / bats-assert for diff-style assertions (Steps 5-6).
6. Guard environment-specific tests with `skip`; run the suite via `bats test/`, adding `--jobs N` for parallelism (Steps 7-8).
7. Wire it into CI with TAP or JUnit output ([references/ci-integration.md](references/ci-integration.md)).

## Step 1 - Install

Per [bgh][bgh]:

```bash
# npm
npm install -g bats

# Homebrew
brew install bats-core

# Git submodule (vendored, deterministic for CI)
git submodule add https://github.com/bats-core/bats-core.git test/bats
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
```

The git-submodule approach pins the Bats version per repo - 
recommended for CI determinism.

## Step 2 - First test file

Per [bgh][bgh] (verbatim example):

```bash
#!/usr/bin/env bats

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

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

Save as `test/math.bats`; run `bats test/math.bats`.

## Step 3 - `run` for exit-code + output capture

The `run` helper invokes a command and captures `$status`,
`$output`, and `$lines[]`:

```bash
@test "myscript --help exits 0 and mentions usage" {
  run ./myscript --help
  [ "$status" -eq 0 ]
  [[ "$output" == *"Usage:"* ]]
}

@test "myscript with no args exits 1" {
  run ./myscript
  [ "$status" -eq 1 ]
  [ "$output" = "error: missing required argument" ]
}

@test "myscript output line count" {
  run ./myscript list
  [ "$status" -eq 0 ]
  [ "${#lines[@]}" -eq 3 ]
  [ "${lines[0]}" = "alice" ]
}
```

Without `run`, a non-zero exit code aborts the test before
assertions execute.

## Step 4 - `setup` / `teardown`

Per [bats][bats]: "setup and teardown: Pre- and post-test hooks."

```bash
setup() {
  TEST_DIR="$(mktemp -d)"
  cp fixtures/sample.txt "$TEST_DIR/"
}

teardown() {
  rm -rf "$TEST_DIR"
}

@test "myscript processes the sample file" {
  run ./myscript "$TEST_DIR/sample.txt"
  [ "$status" -eq 0 ]
}
```

`setup_file` / `teardown_file` run once per file (vs once per
test). Use for expensive setup (Docker container start,
database init).

## Step 5 - `load` shared helpers

Per [bats][bats]: "load: Share common code."

```bash
# test/test_helper.bash
make_temp_repo() {
  local d
  d="$(mktemp -d)"
  (cd "$d" && git init -q)
  echo "$d"
}
```

```bash
# test/integration.bats
load test_helper

@test "git status in fresh repo is clean" {
  local d
  d="$(make_temp_repo)"
  cd "$d"
  run git status --porcelain
  [ "$status" -eq 0 ]
  [ -z "$output" ]
}
```

## Step 6 - bats-assert / bats-support

Better assertions than raw `[ ]`:

```bash
load test_helper/bats-support/load
load test_helper/bats-assert/load

@test "myscript produces expected JSON" {
  run ./myscript --json
  assert_success
  assert_output --partial '"version":'
  refute_output --partial 'ERROR'
}

@test "myscript fails on bad input" {
  run ./myscript --bad-flag
  assert_failure 2
  assert_line --index 0 "Unknown flag: --bad-flag"
}
```

`assert_success` / `assert_failure` / `assert_output` /
`assert_line` give clear diff-style failure messages (vs `[ ]`'s
silent fail).

## Step 7 - `skip`

Per [bats][bats]: "skip: Easily skip tests."

```bash
@test "macOS-only behavior" {
  if [[ "$(uname -s)" != "Darwin" ]]; then
    skip "macOS-only test"
  fi
  run ./myscript --use-keychain
  [ "$status" -eq 0 ]
}

@test "needs docker daemon" {
  if ! command -v docker >/dev/null; then
    skip "docker not installed"
  fi
  if ! docker info >/dev/null 2>&1; then
    skip "docker daemon not running"
  fi
  run ./deploy-script --dry-run
  [ "$status" -eq 0 ]
}
```

`skip` reports the reason in test output (vs commenting out, which
hides the fact that coverage dropped).

## Step 8 - Parallel execution

Per [bats][bats]: "Parallel Execution."

```bash
# Install GNU parallel first (apt install parallel / brew install parallel)
bats --jobs 4 test/
```

Tests must be independent - share no global filesystem state, no
shared ports. Use `setup`/`teardown` with `mktemp -d` to isolate.

## Step 9 - Output formats and CI

`bats` emits pretty output by default, TAP or JUnit XML for CI, and runs in
the official `bats/bats` Docker image. The output-format flags, a GitHub
Actions job, and the Docker one-liner are in
[references/ci-integration.md](references/ci-integration.md).

## Worked example

You maintain `release.sh`, a shell script that prints usage on `--help` and
exits non-zero when called with no arguments. Cover both paths with Bats:

1. Vendor Bats as a submodule (Step 1), then create `test/release.bats`:

   ```bash
   #!/usr/bin/env bats

   load test_helper/bats-support/load
   load test_helper/bats-assert/load

   @test "release.sh --help exits 0 and prints usage" {
     run ./release.sh --help
     assert_success
     assert_output --partial "Usage:"
   }

   @test "release.sh with no args exits non-zero" {
     run ./release.sh
     assert_failure
     assert_output --partial "missing required argument"
   }
   ```

2. Run it: `bats test/release.bats`. Bats reports:

   ```
   release.bats
    ✓ release.sh --help exits 0 and prints usage
    ✓ release.sh with no args exits non-zero

   2 tests, 0 failures
   ```

3. In CI, swap to `bats --formatter junit test/ > junit.xml` and upload the
   report (see [references/ci-integration.md](references/ci-integration.md)).

## Anti-patterns

| Anti-pattern                                                          | Why it fails                                                              | Fix |
|-----------------------------------------------------------------------|---------------------------------------------------------------------------|-----|
| Calling commands directly without `run`                               | Non-zero exit aborts the test before any assertion runs.                  | Wrap with `run`; check `$status`. |
| Tests sharing `/tmp` / fixed ports                                    | Parallel execution breaks intermittently.                                | Per-test `mktemp -d` in `setup` (Step 4). |
| Using `[ ]` for everything                                            | Silent failures; no diff message.                                       | bats-assert (Step 6). |
| Commenting out skipped tests                                          | Hides coverage loss; no signal.                                         | `skip` with reason (Step 7). |
| Asserting full multiline output                                       | Brittle - any whitespace change breaks tests.                          | Use `assert_output --partial` or `assert_line --index N`. |
| One mega `.bats` file                                                 | Slow; hard to grep failures.                                          | Per-feature files in `test/`. |

## Limitations

- **Bash-only.** Tests are written in Bash; PowerShell scripts
  need Pester instead.
- **Slow vs in-language tests.** Each `run` forks a process - 
  expensive vs library-level tests in the language.
- **Output capture quirks.** `run` strips trailing newlines; some
  binary output is not faithfully captured.
- **Windows requires WSL or Git Bash.** Per [bats][bats]:
  "Installing Bats from source via Git Bash."

## References

- [bats][bats] - overview, install methods, test structure,
  `@test` / `run` / `setup` / `teardown` / `load` / `skip`,
  parallel execution, Docker.
- [bgh][bgh] - example test file, install one-liners.
- [references/ci-integration.md](references/ci-integration.md) - output formats, GitHub Actions job, Docker image.
- `tui-snapshot-tester` - 
  TUI snapshot tests (visual layer; bats covers exit code +
  text output).
- `cli-output-conventions` - what to assert on (stable formats, exit codes, stderr).

Files in this skill

  • .tessl-plugin/plugin.json608 B
  • SKILL.md9.6 KB
  • references/ci-integration.md1.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.