Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,8 +91,8 @@ function test_should_add() {

```bash
assert_match_snapshot "$output"
# Re-record: delete the snapshot file and re-run; the assertion writes it when missing
# (so a deleted snapshot never fails — read the diff before deleting)
assert_match_named_snapshot "stderr" "$stderr" # multiple snapshots in one test
# Re-record deliberately: ./bashunit --snapshot-update --filter "test name" tests/
```

## Test Isolation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 71 assertions plus spies, mocks, data providers, snapshots and more.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ _bashunit() {
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches
assert_not_contains assert_not_empty assert_not_equals assert_not_matches
assert_not_same assert_same assert_string_ends_with
Expand Down
1 change: 1 addition & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches \
assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
Expand Down
30 changes: 29 additions & 1 deletion docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,7 @@ to narrow it (`bashunit doc json`).
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |
Expand DownExpand Up@@ -1633,6 +1633,34 @@ function test_failure() {
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Expand Down
6 changes: 3 additions & 3 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,10 +575,10 @@ steps:

> `bashunit test --snapshot-update`

Re-record snapshots: every `assert_match_snapshot` /
`assert_match_snapshot_ignore_colors` whose snapshot already exists is
Re-record snapshots: every snapshot assertion whose file already exists is
overwritten with the value this run produced, and reported as a recorded
snapshot instead of a pass. A missing snapshot is written as usual.
snapshot instead of a pass. A missing snapshot is written as usual. This
includes default, named and ignore-colors snapshots.

Use it when an output change is deliberate. It replaces deleting snapshot files
by hand — the path is derived from the test file and function name, so a wrong
Expand Down
5 changes: 3 additions & 2 deletions docs/public/bashunit-skill.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (71 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All@@ -132,7 +132,8 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
- JSON: `assert_json_equals`, `assert_json_contains`, `assert_json_key_exists`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`,
`assert_match_named_snapshot`, `assert_match_named_snapshot_ignore_colors`
- Spies: `assert_have_been_called`, `assert_not_called`, `assert_have_been_called_with`,
`assert_have_been_called_with_any`, `assert_have_been_called_with_args`,
`assert_have_been_called_times`, `assert_have_been_called_nth_with`
Expand Down
24 changes: 23 additions & 1 deletion docs/snapshots.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ Pass `snapshot_file` to point at a specific snapshot — useful to share one sna
By default each test gets its own, named after the test function.

::: tip
You can update the snapshot by deleting it and running its test again.
Update snapshots deliberately with `--snapshot-update`; combine it with `--filter` to
re-record a single test.
:::

::: code-group
Expand DownExpand Up@@ -77,6 +78,27 @@ function test_failure() {
```
:::

## Named snapshots

Use named snapshots when one test needs to capture several independent values:

> `assert_match_named_snapshot "name" "actual"`

```bash
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```

The name becomes a normalized filename suffix, so these resolve beside the default
snapshot as `test_render_modes.compact.snapshot` and
`test_render_modes.verbose.snapshot`. Spaces and punctuation are safe and cannot escape
the test's `snapshots/` directory.

Use `assert_match_named_snapshot_ignore_colors "name" "actual"` for the same behavior
with ANSI escape sequences removed.

## Placeholders

Snapshot files can contain placeholder tokens to ignore variable parts of the output.
Expand Down
41 changes: 39 additions & 2 deletions src/assert/snapshot.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,19 @@ function assert_match_snapshot() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
local snapshot_name=$1
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$2"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
# Only fork sed when the input actually carries an escape sequence; plain,
# colorless output takes a pure-bash fast path. The sed pattern is kept
Expand All@@ -44,6 +57,25 @@ function assert_match_snapshot_ignore_colors() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
local snapshot_name=$1
# Keep this byte-compatible with assert_match_snapshot_ignore_colors: only
# ANSI sequences ending in m/K are stripped, and only when ESC is present.
local stripped=$2
case "$stripped" in
*$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
esac
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$stripped"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

# Strips the `./` resolve_file prepends and squeezes doubled slashes, so a path
# recorded by an assertion and the same path as `find` prints it compare equal.
function bashunit::snapshot::normalize_path() {
Expand DownExpand Up@@ -214,6 +246,7 @@ _BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
local file_hint="$1"
local func_name="$2"
local snapshot_name="${3:-}"

if [ -n "$file_hint" ]; then
_BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
Expand All@@ -233,9 +266,13 @@ function bashunit::snapshot::resolve_file() {
bashunit::helper::normalize_variable_name_to_slot "$base_part"
local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
bashunit::helper::normalize_variable_name_to_slot "$func_name"
local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"
local name=$_BASHUNIT_HELPER_VARNAME_OUT
if [ -n "$snapshot_name" ]; then
bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
fi

_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot"
}

function bashunit::snapshot::initialize() {
Expand Down
12 changes: 8 additions & 4 deletions src/console/test_line.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,16 +162,21 @@ function bashunit::console_results::print_failed_snapshot_test() {

local line
line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
"$function_name" "$snapshot_file")"

if bashunit::dependencies::has_git; then
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
line="$line
$(bashunit::console_results::snapshot_line_diff \
"$(cat "$snapshot_file")" "$actual_content")"
fi

Expand DownExpand Up@@ -282,4 +287,3 @@ function bashunit::console_results::print_worker_stderr() {
"$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
sed 's/^/|/' "$stderr_file"
}

16 changes: 12 additions & 4 deletions tests/acceptance/bashunit_fail_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_a_test_fail_verbose_output_option() {
}

function test_different_verbose_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file" --detailed)"
}

function test_bashunit_when_a_test_fail_simple_output_env() {
Expand DownExpand Up@@ -54,6 +58,10 @@ function test_bashunit_with_a_test_fail_and_exit_immediately() {
}

function test_different_simple_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --simple)"
}
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_snapshot_named_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
FIXTURE="$(pwd)/tests/acceptance/fixtures/snapshot_named/named.sh"
}

function set_up() {
WORKDIR="$(mktemp -d)"
cp "$FIXTURE" "$WORKDIR/named.sh"
SNAPSHOTS="$WORKDIR/snapshots"
DEFAULT="$SNAPSHOTS/named_sh.test_named_snapshots.snapshot"
FIRST="$SNAPSHOTS/named_sh.test_named_snapshots.first_value.snapshot"
SAFE="$SNAPSHOTS/named_sh.test_named_snapshots._______second_.snapshot"
COLORED="$SNAPSHOTS/named_sh.test_named_snapshots.colored.snapshot"
}

function tear_down() {
rm -rf "$WORKDIR"
}

function test_named_snapshots_create_distinct_safe_files() {
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file named.sh) \
>/dev/null 2>&1

assert_same "default value" "$(<"$DEFAULT")"
assert_same "named value" "$(<"$FIRST")"
assert_same "safe value" "$(<"$SAFE")"
assert_same "colored value" "$(<"$COLORED")"
}

function test_named_snapshot_mismatch_names_the_path_and_update_flag() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

local output status=0
output=$(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
named.sh 2>&1) || status=$?

assert_same 1 "$status"
assert_contains "snapshots/named_sh.test_named_snapshots.first_value.snapshot" "$output"
assert_contains "--snapshot-update" "$output"
}

function test_snapshot_update_rewrites_a_named_snapshot() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
--snapshot-update named.sh) >/dev/null 2>&1

assert_same "named value" "$(<"$FIRST")"
}
8 changes: 6 additions & 2 deletions tests/acceptance/bashunit_stop_on_failure_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_stop_on_failure_env() {
}

function test_different_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_stop_on_failure.sh

assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --stop-on-failure "$test_file")"
assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_STOP_ON_FAILURE" "$test_file")"
}

function test_bashunit_when_stop_on_failure_env_simple_output() {
Expand Down
3 changes: 2 additions & 1 deletion tests/acceptance/bashunit_summary_output_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ function test_incomplete_tests_displayed_with_show_incomplete_flag() {
function test_both_flags_can_be_used_together() {
local output
output=$(./bashunit --env "$TEST_ENV_FILE" --no-parallel --simple --show-skipped --show-incomplete \
"tests/acceptance/bashunit_fail_test.sh" "tests/acceptance/bashunit_init_test.sh" 2>&1) || true
"tests/acceptance/bashunit_execution_error_test.sh" \
"tests/acceptance/bashunit_init_test.sh" 2>&1) || true

assert_contains "incomplete test" "$output"
assert_contains "skipped test" "$output"
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/fixtures/snapshot_named/named.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

function test_named_snapshots() {
assert_match_snapshot "default value"
assert_match_named_snapshot "first value" "named value"
assert_match_named_snapshot "../../ second!" "safe value"
assert_match_named_snapshot_ignore_colors "colored" $'\e[31mcolored value\e[0m'
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
..F..

There was 1 failure:

|1) ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
|✗ Failed: Assert failing
| Expected '1'
| but got  '0'
| at ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
| Source:
| 13: assert_same 1 0



Tests:  4 passed, 1 failed, 5 total
Assertions: 6 passed, 1 failed, 7 total

 Some tests failed 
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(snapshot): add named snapshots by Chemaclass · Pull Request #988 · TypedDevs/bashunit · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,8 +91,8 @@ function test_should_add() {

```bash
assert_match_snapshot "$output"
# Re-record: delete the snapshot file and re-run; the assertion writes it when missing
# (so a deleted snapshot never fails — read the diff before deleting)
assert_match_named_snapshot "stderr" "$stderr" # multiple snapshots in one test
# Re-record deliberately: ./bashunit --snapshot-update --filter "test name" tests/
```

## Test Isolation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 71 assertions plus spies, mocks, data providers, snapshots and more.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ _bashunit() {
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches
assert_not_contains assert_not_empty assert_not_equals assert_not_matches
assert_not_same assert_same assert_string_ends_with
Expand Down
1 change: 1 addition & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches \
assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
Expand Down
30 changes: 29 additions & 1 deletion docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,7 @@ to narrow it (`bashunit doc json`).
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |
Expand DownExpand Up@@ -1633,6 +1633,34 @@ function test_failure() {
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Expand Down
6 changes: 3 additions & 3 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,10 +575,10 @@ steps:

> `bashunit test --snapshot-update`

Re-record snapshots: every `assert_match_snapshot` /
`assert_match_snapshot_ignore_colors` whose snapshot already exists is
Re-record snapshots: every snapshot assertion whose file already exists is
overwritten with the value this run produced, and reported as a recorded
snapshot instead of a pass. A missing snapshot is written as usual.
snapshot instead of a pass. A missing snapshot is written as usual. This
includes default, named and ignore-colors snapshots.

Use it when an output change is deliberate. It replaces deleting snapshot files
by hand — the path is derived from the test file and function name, so a wrong
Expand Down
5 changes: 3 additions & 2 deletions docs/public/bashunit-skill.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (71 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All@@ -132,7 +132,8 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
- JSON: `assert_json_equals`, `assert_json_contains`, `assert_json_key_exists`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`,
`assert_match_named_snapshot`, `assert_match_named_snapshot_ignore_colors`
- Spies: `assert_have_been_called`, `assert_not_called`, `assert_have_been_called_with`,
`assert_have_been_called_with_any`, `assert_have_been_called_with_args`,
`assert_have_been_called_times`, `assert_have_been_called_nth_with`
Expand Down
24 changes: 23 additions & 1 deletion docs/snapshots.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ Pass `snapshot_file` to point at a specific snapshot — useful to share one sna
By default each test gets its own, named after the test function.

::: tip
You can update the snapshot by deleting it and running its test again.
Update snapshots deliberately with `--snapshot-update`; combine it with `--filter` to
re-record a single test.
:::

::: code-group
Expand DownExpand Up@@ -77,6 +78,27 @@ function test_failure() {
```
:::

## Named snapshots

Use named snapshots when one test needs to capture several independent values:

> `assert_match_named_snapshot "name" "actual"`

```bash
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```

The name becomes a normalized filename suffix, so these resolve beside the default
snapshot as `test_render_modes.compact.snapshot` and
`test_render_modes.verbose.snapshot`. Spaces and punctuation are safe and cannot escape
the test's `snapshots/` directory.

Use `assert_match_named_snapshot_ignore_colors "name" "actual"` for the same behavior
with ANSI escape sequences removed.

## Placeholders

Snapshot files can contain placeholder tokens to ignore variable parts of the output.
Expand Down
41 changes: 39 additions & 2 deletions src/assert/snapshot.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,19 @@ function assert_match_snapshot() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
local snapshot_name=$1
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$2"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
# Only fork sed when the input actually carries an escape sequence; plain,
# colorless output takes a pure-bash fast path. The sed pattern is kept
Expand All@@ -44,6 +57,25 @@ function assert_match_snapshot_ignore_colors() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
local snapshot_name=$1
# Keep this byte-compatible with assert_match_snapshot_ignore_colors: only
# ANSI sequences ending in m/K are stripped, and only when ESC is present.
local stripped=$2
case "$stripped" in
*$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
esac
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$stripped"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

# Strips the `./` resolve_file prepends and squeezes doubled slashes, so a path
# recorded by an assertion and the same path as `find` prints it compare equal.
function bashunit::snapshot::normalize_path() {
Expand DownExpand Up@@ -214,6 +246,7 @@ _BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
local file_hint="$1"
local func_name="$2"
local snapshot_name="${3:-}"

if [ -n "$file_hint" ]; then
_BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
Expand All@@ -233,9 +266,13 @@ function bashunit::snapshot::resolve_file() {
bashunit::helper::normalize_variable_name_to_slot "$base_part"
local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
bashunit::helper::normalize_variable_name_to_slot "$func_name"
local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"
local name=$_BASHUNIT_HELPER_VARNAME_OUT
if [ -n "$snapshot_name" ]; then
bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
fi

_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot"
}

function bashunit::snapshot::initialize() {
Expand Down
12 changes: 8 additions & 4 deletions src/console/test_line.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,16 +162,21 @@ function bashunit::console_results::print_failed_snapshot_test() {

local line
line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
"$function_name" "$snapshot_file")"

if bashunit::dependencies::has_git; then
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
line="$line
$(bashunit::console_results::snapshot_line_diff \
"$(cat "$snapshot_file")" "$actual_content")"
fi

Expand DownExpand Up@@ -282,4 +287,3 @@ function bashunit::console_results::print_worker_stderr() {
"$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
sed 's/^/|/' "$stderr_file"
}

16 changes: 12 additions & 4 deletions tests/acceptance/bashunit_fail_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_a_test_fail_verbose_output_option() {
}

function test_different_verbose_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file" --detailed)"
}

function test_bashunit_when_a_test_fail_simple_output_env() {
Expand DownExpand Up@@ -54,6 +58,10 @@ function test_bashunit_with_a_test_fail_and_exit_immediately() {
}

function test_different_simple_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --simple)"
}
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_snapshot_named_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
FIXTURE="$(pwd)/tests/acceptance/fixtures/snapshot_named/named.sh"
}

function set_up() {
WORKDIR="$(mktemp -d)"
cp "$FIXTURE" "$WORKDIR/named.sh"
SNAPSHOTS="$WORKDIR/snapshots"
DEFAULT="$SNAPSHOTS/named_sh.test_named_snapshots.snapshot"
FIRST="$SNAPSHOTS/named_sh.test_named_snapshots.first_value.snapshot"
SAFE="$SNAPSHOTS/named_sh.test_named_snapshots._______second_.snapshot"
COLORED="$SNAPSHOTS/named_sh.test_named_snapshots.colored.snapshot"
}

function tear_down() {
rm -rf "$WORKDIR"
}

function test_named_snapshots_create_distinct_safe_files() {
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file named.sh) \
>/dev/null 2>&1

assert_same "default value" "$(<"$DEFAULT")"
assert_same "named value" "$(<"$FIRST")"
assert_same "safe value" "$(<"$SAFE")"
assert_same "colored value" "$(<"$COLORED")"
}

function test_named_snapshot_mismatch_names_the_path_and_update_flag() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

local output status=0
output=$(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
named.sh 2>&1) || status=$?

assert_same 1 "$status"
assert_contains "snapshots/named_sh.test_named_snapshots.first_value.snapshot" "$output"
assert_contains "--snapshot-update" "$output"
}

function test_snapshot_update_rewrites_a_named_snapshot() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
--snapshot-update named.sh) >/dev/null 2>&1

assert_same "named value" "$(<"$FIRST")"
}
8 changes: 6 additions & 2 deletions tests/acceptance/bashunit_stop_on_failure_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_stop_on_failure_env() {
}

function test_different_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_stop_on_failure.sh

assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --stop-on-failure "$test_file")"
assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_STOP_ON_FAILURE" "$test_file")"
}

function test_bashunit_when_stop_on_failure_env_simple_output() {
Expand Down
3 changes: 2 additions & 1 deletion tests/acceptance/bashunit_summary_output_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ function test_incomplete_tests_displayed_with_show_incomplete_flag() {
function test_both_flags_can_be_used_together() {
local output
output=$(./bashunit --env "$TEST_ENV_FILE" --no-parallel --simple --show-skipped --show-incomplete \
"tests/acceptance/bashunit_fail_test.sh" "tests/acceptance/bashunit_init_test.sh" 2>&1) || true
"tests/acceptance/bashunit_execution_error_test.sh" \
"tests/acceptance/bashunit_init_test.sh" 2>&1) || true

assert_contains "incomplete test" "$output"
assert_contains "skipped test" "$output"
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/fixtures/snapshot_named/named.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

function test_named_snapshots() {
assert_match_snapshot "default value"
assert_match_named_snapshot "first value" "named value"
assert_match_named_snapshot "../../ second!" "safe value"
assert_match_named_snapshot_ignore_colors "colored" $'\e[31mcolored value\e[0m'
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
..F..

There was 1 failure:

|1) ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
|✗ Failed: Assert failing
| Expected '1'
| but got  '0'
| at ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
| Source:
| 13: assert_same 1 0



Tests:  4 passed, 1 failed, 5 total
Assertions: 6 passed, 1 failed, 7 total

 Some tests failed 
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(snapshot): add named snapshots by Chemaclass · Pull Request #988 · TypedDevs/bashunit · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,8 +91,8 @@ function test_should_add() {

```bash
assert_match_snapshot "$output"
# Re-record: delete the snapshot file and re-run; the assertion writes it when missing
# (so a deleted snapshot never fails — read the diff before deleting)
assert_match_named_snapshot "stderr" "$stderr" # multiple snapshots in one test
# Re-record deliberately: ./bashunit --snapshot-update --filter "test name" tests/
```

## Test Isolation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 71 assertions plus spies, mocks, data providers, snapshots and more.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ _bashunit() {
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches
assert_not_contains assert_not_empty assert_not_equals assert_not_matches
assert_not_same assert_same assert_string_ends_with
Expand Down
1 change: 1 addition & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches \
assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
Expand Down
30 changes: 29 additions & 1 deletion docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,7 @@ to narrow it (`bashunit doc json`).
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |
Expand DownExpand Up@@ -1633,6 +1633,34 @@ function test_failure() {
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Expand Down
6 changes: 3 additions & 3 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,10 +575,10 @@ steps:

> `bashunit test --snapshot-update`

Re-record snapshots: every `assert_match_snapshot` /
`assert_match_snapshot_ignore_colors` whose snapshot already exists is
Re-record snapshots: every snapshot assertion whose file already exists is
overwritten with the value this run produced, and reported as a recorded
snapshot instead of a pass. A missing snapshot is written as usual.
snapshot instead of a pass. A missing snapshot is written as usual. This
includes default, named and ignore-colors snapshots.

Use it when an output change is deliberate. It replaces deleting snapshot files
by hand — the path is derived from the test file and function name, so a wrong
Expand Down
5 changes: 3 additions & 2 deletions docs/public/bashunit-skill.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (71 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All@@ -132,7 +132,8 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
- JSON: `assert_json_equals`, `assert_json_contains`, `assert_json_key_exists`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`,
`assert_match_named_snapshot`, `assert_match_named_snapshot_ignore_colors`
- Spies: `assert_have_been_called`, `assert_not_called`, `assert_have_been_called_with`,
`assert_have_been_called_with_any`, `assert_have_been_called_with_args`,
`assert_have_been_called_times`, `assert_have_been_called_nth_with`
Expand Down
24 changes: 23 additions & 1 deletion docs/snapshots.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ Pass `snapshot_file` to point at a specific snapshot — useful to share one sna
By default each test gets its own, named after the test function.

::: tip
You can update the snapshot by deleting it and running its test again.
Update snapshots deliberately with `--snapshot-update`; combine it with `--filter` to
re-record a single test.
:::

::: code-group
Expand DownExpand Up@@ -77,6 +78,27 @@ function test_failure() {
```
:::

## Named snapshots

Use named snapshots when one test needs to capture several independent values:

> `assert_match_named_snapshot "name" "actual"`

```bash
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```

The name becomes a normalized filename suffix, so these resolve beside the default
snapshot as `test_render_modes.compact.snapshot` and
`test_render_modes.verbose.snapshot`. Spaces and punctuation are safe and cannot escape
the test's `snapshots/` directory.

Use `assert_match_named_snapshot_ignore_colors "name" "actual"` for the same behavior
with ANSI escape sequences removed.

## Placeholders

Snapshot files can contain placeholder tokens to ignore variable parts of the output.
Expand Down
41 changes: 39 additions & 2 deletions src/assert/snapshot.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,19 @@ function assert_match_snapshot() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
local snapshot_name=$1
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$2"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
# Only fork sed when the input actually carries an escape sequence; plain,
# colorless output takes a pure-bash fast path. The sed pattern is kept
Expand All@@ -44,6 +57,25 @@ function assert_match_snapshot_ignore_colors() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
local snapshot_name=$1
# Keep this byte-compatible with assert_match_snapshot_ignore_colors: only
# ANSI sequences ending in m/K are stripped, and only when ESC is present.
local stripped=$2
case "$stripped" in
*$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
esac
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$stripped"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

# Strips the `./` resolve_file prepends and squeezes doubled slashes, so a path
# recorded by an assertion and the same path as `find` prints it compare equal.
function bashunit::snapshot::normalize_path() {
Expand DownExpand Up@@ -214,6 +246,7 @@ _BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
local file_hint="$1"
local func_name="$2"
local snapshot_name="${3:-}"

if [ -n "$file_hint" ]; then
_BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
Expand All@@ -233,9 +266,13 @@ function bashunit::snapshot::resolve_file() {
bashunit::helper::normalize_variable_name_to_slot "$base_part"
local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
bashunit::helper::normalize_variable_name_to_slot "$func_name"
local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"
local name=$_BASHUNIT_HELPER_VARNAME_OUT
if [ -n "$snapshot_name" ]; then
bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
fi

_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot"
}

function bashunit::snapshot::initialize() {
Expand Down
12 changes: 8 additions & 4 deletions src/console/test_line.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,16 +162,21 @@ function bashunit::console_results::print_failed_snapshot_test() {

local line
line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
"$function_name" "$snapshot_file")"

if bashunit::dependencies::has_git; then
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
line="$line
$(bashunit::console_results::snapshot_line_diff \
"$(cat "$snapshot_file")" "$actual_content")"
fi

Expand DownExpand Up@@ -282,4 +287,3 @@ function bashunit::console_results::print_worker_stderr() {
"$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
sed 's/^/|/' "$stderr_file"
}

16 changes: 12 additions & 4 deletions tests/acceptance/bashunit_fail_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_a_test_fail_verbose_output_option() {
}

function test_different_verbose_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file" --detailed)"
}

function test_bashunit_when_a_test_fail_simple_output_env() {
Expand DownExpand Up@@ -54,6 +58,10 @@ function test_bashunit_with_a_test_fail_and_exit_immediately() {
}

function test_different_simple_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --simple)"
}
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_snapshot_named_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
FIXTURE="$(pwd)/tests/acceptance/fixtures/snapshot_named/named.sh"
}

function set_up() {
WORKDIR="$(mktemp -d)"
cp "$FIXTURE" "$WORKDIR/named.sh"
SNAPSHOTS="$WORKDIR/snapshots"
DEFAULT="$SNAPSHOTS/named_sh.test_named_snapshots.snapshot"
FIRST="$SNAPSHOTS/named_sh.test_named_snapshots.first_value.snapshot"
SAFE="$SNAPSHOTS/named_sh.test_named_snapshots._______second_.snapshot"
COLORED="$SNAPSHOTS/named_sh.test_named_snapshots.colored.snapshot"
}

function tear_down() {
rm -rf "$WORKDIR"
}

function test_named_snapshots_create_distinct_safe_files() {
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file named.sh) \
>/dev/null 2>&1

assert_same "default value" "$(<"$DEFAULT")"
assert_same "named value" "$(<"$FIRST")"
assert_same "safe value" "$(<"$SAFE")"
assert_same "colored value" "$(<"$COLORED")"
}

function test_named_snapshot_mismatch_names_the_path_and_update_flag() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

local output status=0
output=$(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
named.sh 2>&1) || status=$?

assert_same 1 "$status"
assert_contains "snapshots/named_sh.test_named_snapshots.first_value.snapshot" "$output"
assert_contains "--snapshot-update" "$output"
}

function test_snapshot_update_rewrites_a_named_snapshot() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
--snapshot-update named.sh) >/dev/null 2>&1

assert_same "named value" "$(<"$FIRST")"
}
8 changes: 6 additions & 2 deletions tests/acceptance/bashunit_stop_on_failure_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_stop_on_failure_env() {
}

function test_different_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_stop_on_failure.sh

assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --stop-on-failure "$test_file")"
assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_STOP_ON_FAILURE" "$test_file")"
}

function test_bashunit_when_stop_on_failure_env_simple_output() {
Expand Down
3 changes: 2 additions & 1 deletion tests/acceptance/bashunit_summary_output_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ function test_incomplete_tests_displayed_with_show_incomplete_flag() {
function test_both_flags_can_be_used_together() {
local output
output=$(./bashunit --env "$TEST_ENV_FILE" --no-parallel --simple --show-skipped --show-incomplete \
"tests/acceptance/bashunit_fail_test.sh" "tests/acceptance/bashunit_init_test.sh" 2>&1) || true
"tests/acceptance/bashunit_execution_error_test.sh" \
"tests/acceptance/bashunit_init_test.sh" 2>&1) || true

assert_contains "incomplete test" "$output"
assert_contains "skipped test" "$output"
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/fixtures/snapshot_named/named.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

function test_named_snapshots() {
assert_match_snapshot "default value"
assert_match_named_snapshot "first value" "named value"
assert_match_named_snapshot "../../ second!" "safe value"
assert_match_named_snapshot_ignore_colors "colored" $'\e[31mcolored value\e[0m'
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
..F..

There was 1 failure:

|1) ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
|✗ Failed: Assert failing
| Expected '1'
| but got  '0'
| at ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
| Source:
| 13: assert_same 1 0



Tests:  4 passed, 1 failed, 5 total
Assertions: 6 passed, 1 failed, 7 total

 Some tests failed 
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(snapshot): add named snapshots by Chemaclass · Pull Request #988 · TypedDevs/bashunit · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,8 +91,8 @@ function test_should_add() {

```bash
assert_match_snapshot "$output"
# Re-record: delete the snapshot file and re-run; the assertion writes it when missing
# (so a deleted snapshot never fails — read the diff before deleting)
assert_match_named_snapshot "stderr" "$stderr" # multiple snapshots in one test
# Re-record deliberately: ./bashunit --snapshot-update --filter "test name" tests/
```

## Test Isolation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 71 assertions plus spies, mocks, data providers, snapshots and more.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ _bashunit() {
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches
assert_not_contains assert_not_empty assert_not_equals assert_not_matches
assert_not_same assert_same assert_string_ends_with
Expand Down
1 change: 1 addition & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches \
assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
Expand Down
30 changes: 29 additions & 1 deletion docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,7 @@ to narrow it (`bashunit doc json`).
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |
Expand DownExpand Up@@ -1633,6 +1633,34 @@ function test_failure() {
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Expand Down
6 changes: 3 additions & 3 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,10 +575,10 @@ steps:

> `bashunit test --snapshot-update`

Re-record snapshots: every `assert_match_snapshot` /
`assert_match_snapshot_ignore_colors` whose snapshot already exists is
Re-record snapshots: every snapshot assertion whose file already exists is
overwritten with the value this run produced, and reported as a recorded
snapshot instead of a pass. A missing snapshot is written as usual.
snapshot instead of a pass. A missing snapshot is written as usual. This
includes default, named and ignore-colors snapshots.

Use it when an output change is deliberate. It replaces deleting snapshot files
by hand — the path is derived from the test file and function name, so a wrong
Expand Down
5 changes: 3 additions & 2 deletions docs/public/bashunit-skill.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (71 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All@@ -132,7 +132,8 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
- JSON: `assert_json_equals`, `assert_json_contains`, `assert_json_key_exists`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`,
`assert_match_named_snapshot`, `assert_match_named_snapshot_ignore_colors`
- Spies: `assert_have_been_called`, `assert_not_called`, `assert_have_been_called_with`,
`assert_have_been_called_with_any`, `assert_have_been_called_with_args`,
`assert_have_been_called_times`, `assert_have_been_called_nth_with`
Expand Down
24 changes: 23 additions & 1 deletion docs/snapshots.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ Pass `snapshot_file` to point at a specific snapshot — useful to share one sna
By default each test gets its own, named after the test function.

::: tip
You can update the snapshot by deleting it and running its test again.
Update snapshots deliberately with `--snapshot-update`; combine it with `--filter` to
re-record a single test.
:::

::: code-group
Expand DownExpand Up@@ -77,6 +78,27 @@ function test_failure() {
```
:::

## Named snapshots

Use named snapshots when one test needs to capture several independent values:

> `assert_match_named_snapshot "name" "actual"`

```bash
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```

The name becomes a normalized filename suffix, so these resolve beside the default
snapshot as `test_render_modes.compact.snapshot` and
`test_render_modes.verbose.snapshot`. Spaces and punctuation are safe and cannot escape
the test's `snapshots/` directory.

Use `assert_match_named_snapshot_ignore_colors "name" "actual"` for the same behavior
with ANSI escape sequences removed.

## Placeholders

Snapshot files can contain placeholder tokens to ignore variable parts of the output.
Expand Down
41 changes: 39 additions & 2 deletions src/assert/snapshot.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,19 @@ function assert_match_snapshot() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
local snapshot_name=$1
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$2"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
# Only fork sed when the input actually carries an escape sequence; plain,
# colorless output takes a pure-bash fast path. The sed pattern is kept
Expand All@@ -44,6 +57,25 @@ function assert_match_snapshot_ignore_colors() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
local snapshot_name=$1
# Keep this byte-compatible with assert_match_snapshot_ignore_colors: only
# ANSI sequences ending in m/K are stripped, and only when ESC is present.
local stripped=$2
case "$stripped" in
*$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
esac
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$stripped"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

# Strips the `./` resolve_file prepends and squeezes doubled slashes, so a path
# recorded by an assertion and the same path as `find` prints it compare equal.
function bashunit::snapshot::normalize_path() {
Expand DownExpand Up@@ -214,6 +246,7 @@ _BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
local file_hint="$1"
local func_name="$2"
local snapshot_name="${3:-}"

if [ -n "$file_hint" ]; then
_BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
Expand All@@ -233,9 +266,13 @@ function bashunit::snapshot::resolve_file() {
bashunit::helper::normalize_variable_name_to_slot "$base_part"
local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
bashunit::helper::normalize_variable_name_to_slot "$func_name"
local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"
local name=$_BASHUNIT_HELPER_VARNAME_OUT
if [ -n "$snapshot_name" ]; then
bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
fi

_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot"
}

function bashunit::snapshot::initialize() {
Expand Down
12 changes: 8 additions & 4 deletions src/console/test_line.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,16 +162,21 @@ function bashunit::console_results::print_failed_snapshot_test() {

local line
line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
"$function_name" "$snapshot_file")"

if bashunit::dependencies::has_git; then
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
line="$line
$(bashunit::console_results::snapshot_line_diff \
"$(cat "$snapshot_file")" "$actual_content")"
fi

Expand DownExpand Up@@ -282,4 +287,3 @@ function bashunit::console_results::print_worker_stderr() {
"$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
sed 's/^/|/' "$stderr_file"
}

16 changes: 12 additions & 4 deletions tests/acceptance/bashunit_fail_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_a_test_fail_verbose_output_option() {
}

function test_different_verbose_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file" --detailed)"
}

function test_bashunit_when_a_test_fail_simple_output_env() {
Expand DownExpand Up@@ -54,6 +58,10 @@ function test_bashunit_with_a_test_fail_and_exit_immediately() {
}

function test_different_simple_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --simple)"
}
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_snapshot_named_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
FIXTURE="$(pwd)/tests/acceptance/fixtures/snapshot_named/named.sh"
}

function set_up() {
WORKDIR="$(mktemp -d)"
cp "$FIXTURE" "$WORKDIR/named.sh"
SNAPSHOTS="$WORKDIR/snapshots"
DEFAULT="$SNAPSHOTS/named_sh.test_named_snapshots.snapshot"
FIRST="$SNAPSHOTS/named_sh.test_named_snapshots.first_value.snapshot"
SAFE="$SNAPSHOTS/named_sh.test_named_snapshots._______second_.snapshot"
COLORED="$SNAPSHOTS/named_sh.test_named_snapshots.colored.snapshot"
}

function tear_down() {
rm -rf "$WORKDIR"
}

function test_named_snapshots_create_distinct_safe_files() {
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file named.sh) \
>/dev/null 2>&1

assert_same "default value" "$(<"$DEFAULT")"
assert_same "named value" "$(<"$FIRST")"
assert_same "safe value" "$(<"$SAFE")"
assert_same "colored value" "$(<"$COLORED")"
}

function test_named_snapshot_mismatch_names_the_path_and_update_flag() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

local output status=0
output=$(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
named.sh 2>&1) || status=$?

assert_same 1 "$status"
assert_contains "snapshots/named_sh.test_named_snapshots.first_value.snapshot" "$output"
assert_contains "--snapshot-update" "$output"
}

function test_snapshot_update_rewrites_a_named_snapshot() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
--snapshot-update named.sh) >/dev/null 2>&1

assert_same "named value" "$(<"$FIRST")"
}
8 changes: 6 additions & 2 deletions tests/acceptance/bashunit_stop_on_failure_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_stop_on_failure_env() {
}

function test_different_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_stop_on_failure.sh

assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --stop-on-failure "$test_file")"
assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_STOP_ON_FAILURE" "$test_file")"
}

function test_bashunit_when_stop_on_failure_env_simple_output() {
Expand Down
3 changes: 2 additions & 1 deletion tests/acceptance/bashunit_summary_output_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ function test_incomplete_tests_displayed_with_show_incomplete_flag() {
function test_both_flags_can_be_used_together() {
local output
output=$(./bashunit --env "$TEST_ENV_FILE" --no-parallel --simple --show-skipped --show-incomplete \
"tests/acceptance/bashunit_fail_test.sh" "tests/acceptance/bashunit_init_test.sh" 2>&1) || true
"tests/acceptance/bashunit_execution_error_test.sh" \
"tests/acceptance/bashunit_init_test.sh" 2>&1) || true

assert_contains "incomplete test" "$output"
assert_contains "skipped test" "$output"
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/fixtures/snapshot_named/named.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

function test_named_snapshots() {
assert_match_snapshot "default value"
assert_match_named_snapshot "first value" "named value"
assert_match_named_snapshot "../../ second!" "safe value"
assert_match_named_snapshot_ignore_colors "colored" $'\e[31mcolored value\e[0m'
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
..F..

There was 1 failure:

|1) ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
|✗ Failed: Assert failing
| Expected '1'
| but got  '0'
| at ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
| Source:
| 13: assert_same 1 0



Tests:  4 passed, 1 failed, 5 total
Assertions: 6 passed, 1 failed, 7 total

 Some tests failed 
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(snapshot): add named snapshots by Chemaclass · Pull Request #988 · TypedDevs/bashunit · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,8 +91,8 @@ function test_should_add() {

```bash
assert_match_snapshot "$output"
# Re-record: delete the snapshot file and re-run; the assertion writes it when missing
# (so a deleted snapshot never fails — read the diff before deleting)
assert_match_named_snapshot "stderr" "$stderr" # multiple snapshots in one test
# Re-record deliberately: ./bashunit --snapshot-update --filter "test name" tests/
```

## Test Isolation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 71 assertions plus spies, mocks, data providers, snapshots and more.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ _bashunit() {
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches
assert_not_contains assert_not_empty assert_not_equals assert_not_matches
assert_not_same assert_same assert_string_ends_with
Expand Down
1 change: 1 addition & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches \
assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
Expand Down
30 changes: 29 additions & 1 deletion docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,7 @@ to narrow it (`bashunit doc json`).
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |
Expand DownExpand Up@@ -1633,6 +1633,34 @@ function test_failure() {
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Expand Down
6 changes: 3 additions & 3 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,10 +575,10 @@ steps:

> `bashunit test --snapshot-update`

Re-record snapshots: every `assert_match_snapshot` /
`assert_match_snapshot_ignore_colors` whose snapshot already exists is
Re-record snapshots: every snapshot assertion whose file already exists is
overwritten with the value this run produced, and reported as a recorded
snapshot instead of a pass. A missing snapshot is written as usual.
snapshot instead of a pass. A missing snapshot is written as usual. This
includes default, named and ignore-colors snapshots.

Use it when an output change is deliberate. It replaces deleting snapshot files
by hand — the path is derived from the test file and function name, so a wrong
Expand Down
5 changes: 3 additions & 2 deletions docs/public/bashunit-skill.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (71 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All@@ -132,7 +132,8 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
- JSON: `assert_json_equals`, `assert_json_contains`, `assert_json_key_exists`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`,
`assert_match_named_snapshot`, `assert_match_named_snapshot_ignore_colors`
- Spies: `assert_have_been_called`, `assert_not_called`, `assert_have_been_called_with`,
`assert_have_been_called_with_any`, `assert_have_been_called_with_args`,
`assert_have_been_called_times`, `assert_have_been_called_nth_with`
Expand Down
24 changes: 23 additions & 1 deletion docs/snapshots.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ Pass `snapshot_file` to point at a specific snapshot — useful to share one sna
By default each test gets its own, named after the test function.

::: tip
You can update the snapshot by deleting it and running its test again.
Update snapshots deliberately with `--snapshot-update`; combine it with `--filter` to
re-record a single test.
:::

::: code-group
Expand DownExpand Up@@ -77,6 +78,27 @@ function test_failure() {
```
:::

## Named snapshots

Use named snapshots when one test needs to capture several independent values:

> `assert_match_named_snapshot "name" "actual"`

```bash
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```

The name becomes a normalized filename suffix, so these resolve beside the default
snapshot as `test_render_modes.compact.snapshot` and
`test_render_modes.verbose.snapshot`. Spaces and punctuation are safe and cannot escape
the test's `snapshots/` directory.

Use `assert_match_named_snapshot_ignore_colors "name" "actual"` for the same behavior
with ANSI escape sequences removed.

## Placeholders

Snapshot files can contain placeholder tokens to ignore variable parts of the output.
Expand Down
41 changes: 39 additions & 2 deletions src/assert/snapshot.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,19 @@ function assert_match_snapshot() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
local snapshot_name=$1
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$2"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
# Only fork sed when the input actually carries an escape sequence; plain,
# colorless output takes a pure-bash fast path. The sed pattern is kept
Expand All@@ -44,6 +57,25 @@ function assert_match_snapshot_ignore_colors() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
local snapshot_name=$1
# Keep this byte-compatible with assert_match_snapshot_ignore_colors: only
# ANSI sequences ending in m/K are stripped, and only when ESC is present.
local stripped=$2
case "$stripped" in
*$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
esac
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$stripped"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

# Strips the `./` resolve_file prepends and squeezes doubled slashes, so a path
# recorded by an assertion and the same path as `find` prints it compare equal.
function bashunit::snapshot::normalize_path() {
Expand DownExpand Up@@ -214,6 +246,7 @@ _BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
local file_hint="$1"
local func_name="$2"
local snapshot_name="${3:-}"

if [ -n "$file_hint" ]; then
_BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
Expand All@@ -233,9 +266,13 @@ function bashunit::snapshot::resolve_file() {
bashunit::helper::normalize_variable_name_to_slot "$base_part"
local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
bashunit::helper::normalize_variable_name_to_slot "$func_name"
local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"
local name=$_BASHUNIT_HELPER_VARNAME_OUT
if [ -n "$snapshot_name" ]; then
bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
fi

_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot"
}

function bashunit::snapshot::initialize() {
Expand Down
12 changes: 8 additions & 4 deletions src/console/test_line.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,16 +162,21 @@ function bashunit::console_results::print_failed_snapshot_test() {

local line
line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
"$function_name" "$snapshot_file")"

if bashunit::dependencies::has_git; then
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
line="$line
$(bashunit::console_results::snapshot_line_diff \
"$(cat "$snapshot_file")" "$actual_content")"
fi

Expand DownExpand Up@@ -282,4 +287,3 @@ function bashunit::console_results::print_worker_stderr() {
"$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
sed 's/^/|/' "$stderr_file"
}

16 changes: 12 additions & 4 deletions tests/acceptance/bashunit_fail_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_a_test_fail_verbose_output_option() {
}

function test_different_verbose_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file" --detailed)"
}

function test_bashunit_when_a_test_fail_simple_output_env() {
Expand DownExpand Up@@ -54,6 +58,10 @@ function test_bashunit_with_a_test_fail_and_exit_immediately() {
}

function test_different_simple_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --simple)"
}
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_snapshot_named_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
FIXTURE="$(pwd)/tests/acceptance/fixtures/snapshot_named/named.sh"
}

function set_up() {
WORKDIR="$(mktemp -d)"
cp "$FIXTURE" "$WORKDIR/named.sh"
SNAPSHOTS="$WORKDIR/snapshots"
DEFAULT="$SNAPSHOTS/named_sh.test_named_snapshots.snapshot"
FIRST="$SNAPSHOTS/named_sh.test_named_snapshots.first_value.snapshot"
SAFE="$SNAPSHOTS/named_sh.test_named_snapshots._______second_.snapshot"
COLORED="$SNAPSHOTS/named_sh.test_named_snapshots.colored.snapshot"
}

function tear_down() {
rm -rf "$WORKDIR"
}

function test_named_snapshots_create_distinct_safe_files() {
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file named.sh) \
>/dev/null 2>&1

assert_same "default value" "$(<"$DEFAULT")"
assert_same "named value" "$(<"$FIRST")"
assert_same "safe value" "$(<"$SAFE")"
assert_same "colored value" "$(<"$COLORED")"
}

function test_named_snapshot_mismatch_names_the_path_and_update_flag() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

local output status=0
output=$(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
named.sh 2>&1) || status=$?

assert_same 1 "$status"
assert_contains "snapshots/named_sh.test_named_snapshots.first_value.snapshot" "$output"
assert_contains "--snapshot-update" "$output"
}

function test_snapshot_update_rewrites_a_named_snapshot() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
--snapshot-update named.sh) >/dev/null 2>&1

assert_same "named value" "$(<"$FIRST")"
}
8 changes: 6 additions & 2 deletions tests/acceptance/bashunit_stop_on_failure_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_stop_on_failure_env() {
}

function test_different_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_stop_on_failure.sh

assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --stop-on-failure "$test_file")"
assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_STOP_ON_FAILURE" "$test_file")"
}

function test_bashunit_when_stop_on_failure_env_simple_output() {
Expand Down
3 changes: 2 additions & 1 deletion tests/acceptance/bashunit_summary_output_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ function test_incomplete_tests_displayed_with_show_incomplete_flag() {
function test_both_flags_can_be_used_together() {
local output
output=$(./bashunit --env "$TEST_ENV_FILE" --no-parallel --simple --show-skipped --show-incomplete \
"tests/acceptance/bashunit_fail_test.sh" "tests/acceptance/bashunit_init_test.sh" 2>&1) || true
"tests/acceptance/bashunit_execution_error_test.sh" \
"tests/acceptance/bashunit_init_test.sh" 2>&1) || true

assert_contains "incomplete test" "$output"
assert_contains "skipped test" "$output"
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/fixtures/snapshot_named/named.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

function test_named_snapshots() {
assert_match_snapshot "default value"
assert_match_named_snapshot "first value" "named value"
assert_match_named_snapshot "../../ second!" "safe value"
assert_match_named_snapshot_ignore_colors "colored" $'\e[31mcolored value\e[0m'
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
..F..

There was 1 failure:

|1) ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
|✗ Failed: Assert failing
| Expected '1'
| but got  '0'
| at ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
| Source:
| 13: assert_same 1 0



Tests:  4 passed, 1 failed, 5 total
Assertions: 6 passed, 1 failed, 7 total

 Some tests failed 
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(snapshot): add named snapshots by Chemaclass · Pull Request #988 · TypedDevs/bashunit · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,8 +91,8 @@ function test_should_add() {

```bash
assert_match_snapshot "$output"
# Re-record: delete the snapshot file and re-run; the assertion writes it when missing
# (so a deleted snapshot never fails — read the diff before deleting)
assert_match_named_snapshot "stderr" "$stderr" # multiple snapshots in one test
# Re-record deliberately: ./bashunit --snapshot-update --filter "test name" tests/
```

## Test Isolation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 71 assertions plus spies, mocks, data providers, snapshots and more.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ _bashunit() {
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches
assert_not_contains assert_not_empty assert_not_equals assert_not_matches
assert_not_same assert_same assert_string_ends_with
Expand Down
1 change: 1 addition & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches \
assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
Expand Down
30 changes: 29 additions & 1 deletion docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,7 @@ to narrow it (`bashunit doc json`).
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |
Expand DownExpand Up@@ -1633,6 +1633,34 @@ function test_failure() {
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Expand Down
6 changes: 3 additions & 3 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,10 +575,10 @@ steps:

> `bashunit test --snapshot-update`

Re-record snapshots: every `assert_match_snapshot` /
`assert_match_snapshot_ignore_colors` whose snapshot already exists is
Re-record snapshots: every snapshot assertion whose file already exists is
overwritten with the value this run produced, and reported as a recorded
snapshot instead of a pass. A missing snapshot is written as usual.
snapshot instead of a pass. A missing snapshot is written as usual. This
includes default, named and ignore-colors snapshots.

Use it when an output change is deliberate. It replaces deleting snapshot files
by hand — the path is derived from the test file and function name, so a wrong
Expand Down
5 changes: 3 additions & 2 deletions docs/public/bashunit-skill.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (71 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All@@ -132,7 +132,8 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
- JSON: `assert_json_equals`, `assert_json_contains`, `assert_json_key_exists`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`,
`assert_match_named_snapshot`, `assert_match_named_snapshot_ignore_colors`
- Spies: `assert_have_been_called`, `assert_not_called`, `assert_have_been_called_with`,
`assert_have_been_called_with_any`, `assert_have_been_called_with_args`,
`assert_have_been_called_times`, `assert_have_been_called_nth_with`
Expand Down
24 changes: 23 additions & 1 deletion docs/snapshots.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ Pass `snapshot_file` to point at a specific snapshot — useful to share one sna
By default each test gets its own, named after the test function.

::: tip
You can update the snapshot by deleting it and running its test again.
Update snapshots deliberately with `--snapshot-update`; combine it with `--filter` to
re-record a single test.
:::

::: code-group
Expand DownExpand Up@@ -77,6 +78,27 @@ function test_failure() {
```
:::

## Named snapshots

Use named snapshots when one test needs to capture several independent values:

> `assert_match_named_snapshot "name" "actual"`

```bash
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```

The name becomes a normalized filename suffix, so these resolve beside the default
snapshot as `test_render_modes.compact.snapshot` and
`test_render_modes.verbose.snapshot`. Spaces and punctuation are safe and cannot escape
the test's `snapshots/` directory.

Use `assert_match_named_snapshot_ignore_colors "name" "actual"` for the same behavior
with ANSI escape sequences removed.

## Placeholders

Snapshot files can contain placeholder tokens to ignore variable parts of the output.
Expand Down
41 changes: 39 additions & 2 deletions src/assert/snapshot.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,19 @@ function assert_match_snapshot() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
local snapshot_name=$1
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$2"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
# Only fork sed when the input actually carries an escape sequence; plain,
# colorless output takes a pure-bash fast path. The sed pattern is kept
Expand All@@ -44,6 +57,25 @@ function assert_match_snapshot_ignore_colors() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
local snapshot_name=$1
# Keep this byte-compatible with assert_match_snapshot_ignore_colors: only
# ANSI sequences ending in m/K are stripped, and only when ESC is present.
local stripped=$2
case "$stripped" in
*$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
esac
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$stripped"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

# Strips the `./` resolve_file prepends and squeezes doubled slashes, so a path
# recorded by an assertion and the same path as `find` prints it compare equal.
function bashunit::snapshot::normalize_path() {
Expand DownExpand Up@@ -214,6 +246,7 @@ _BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
local file_hint="$1"
local func_name="$2"
local snapshot_name="${3:-}"

if [ -n "$file_hint" ]; then
_BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
Expand All@@ -233,9 +266,13 @@ function bashunit::snapshot::resolve_file() {
bashunit::helper::normalize_variable_name_to_slot "$base_part"
local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
bashunit::helper::normalize_variable_name_to_slot "$func_name"
local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"
local name=$_BASHUNIT_HELPER_VARNAME_OUT
if [ -n "$snapshot_name" ]; then
bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
fi

_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot"
}

function bashunit::snapshot::initialize() {
Expand Down
12 changes: 8 additions & 4 deletions src/console/test_line.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,16 +162,21 @@ function bashunit::console_results::print_failed_snapshot_test() {

local line
line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
"$function_name" "$snapshot_file")"

if bashunit::dependencies::has_git; then
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
line="$line
$(bashunit::console_results::snapshot_line_diff \
"$(cat "$snapshot_file")" "$actual_content")"
fi

Expand DownExpand Up@@ -282,4 +287,3 @@ function bashunit::console_results::print_worker_stderr() {
"$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
sed 's/^/|/' "$stderr_file"
}

16 changes: 12 additions & 4 deletions tests/acceptance/bashunit_fail_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_a_test_fail_verbose_output_option() {
}

function test_different_verbose_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file" --detailed)"
}

function test_bashunit_when_a_test_fail_simple_output_env() {
Expand DownExpand Up@@ -54,6 +58,10 @@ function test_bashunit_with_a_test_fail_and_exit_immediately() {
}

function test_different_simple_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --simple)"
}
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_snapshot_named_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
FIXTURE="$(pwd)/tests/acceptance/fixtures/snapshot_named/named.sh"
}

function set_up() {
WORKDIR="$(mktemp -d)"
cp "$FIXTURE" "$WORKDIR/named.sh"
SNAPSHOTS="$WORKDIR/snapshots"
DEFAULT="$SNAPSHOTS/named_sh.test_named_snapshots.snapshot"
FIRST="$SNAPSHOTS/named_sh.test_named_snapshots.first_value.snapshot"
SAFE="$SNAPSHOTS/named_sh.test_named_snapshots._______second_.snapshot"
COLORED="$SNAPSHOTS/named_sh.test_named_snapshots.colored.snapshot"
}

function tear_down() {
rm -rf "$WORKDIR"
}

function test_named_snapshots_create_distinct_safe_files() {
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file named.sh) \
>/dev/null 2>&1

assert_same "default value" "$(<"$DEFAULT")"
assert_same "named value" "$(<"$FIRST")"
assert_same "safe value" "$(<"$SAFE")"
assert_same "colored value" "$(<"$COLORED")"
}

function test_named_snapshot_mismatch_names_the_path_and_update_flag() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

local output status=0
output=$(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
named.sh 2>&1) || status=$?

assert_same 1 "$status"
assert_contains "snapshots/named_sh.test_named_snapshots.first_value.snapshot" "$output"
assert_contains "--snapshot-update" "$output"
}

function test_snapshot_update_rewrites_a_named_snapshot() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
--snapshot-update named.sh) >/dev/null 2>&1

assert_same "named value" "$(<"$FIRST")"
}
8 changes: 6 additions & 2 deletions tests/acceptance/bashunit_stop_on_failure_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_stop_on_failure_env() {
}

function test_different_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_stop_on_failure.sh

assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --stop-on-failure "$test_file")"
assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_STOP_ON_FAILURE" "$test_file")"
}

function test_bashunit_when_stop_on_failure_env_simple_output() {
Expand Down
3 changes: 2 additions & 1 deletion tests/acceptance/bashunit_summary_output_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ function test_incomplete_tests_displayed_with_show_incomplete_flag() {
function test_both_flags_can_be_used_together() {
local output
output=$(./bashunit --env "$TEST_ENV_FILE" --no-parallel --simple --show-skipped --show-incomplete \
"tests/acceptance/bashunit_fail_test.sh" "tests/acceptance/bashunit_init_test.sh" 2>&1) || true
"tests/acceptance/bashunit_execution_error_test.sh" \
"tests/acceptance/bashunit_init_test.sh" 2>&1) || true

assert_contains "incomplete test" "$output"
assert_contains "skipped test" "$output"
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/fixtures/snapshot_named/named.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

function test_named_snapshots() {
assert_match_snapshot "default value"
assert_match_named_snapshot "first value" "named value"
assert_match_named_snapshot "../../ second!" "safe value"
assert_match_named_snapshot_ignore_colors "colored" $'\e[31mcolored value\e[0m'
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
..F..

There was 1 failure:

|1) ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
|✗ Failed: Assert failing
| Expected '1'
| but got  '0'
| at ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
| Source:
| 13: assert_same 1 0



Tests:  4 passed, 1 failed, 5 total
Assertions: 6 passed, 1 failed, 7 total

 Some tests failed 
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(snapshot): add named snapshots by Chemaclass · Pull Request #988 · TypedDevs/bashunit · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,8 +91,8 @@ function test_should_add() {

```bash
assert_match_snapshot "$output"
# Re-record: delete the snapshot file and re-run; the assertion writes it when missing
# (so a deleted snapshot never fails — read the diff before deleting)
assert_match_named_snapshot "stderr" "$stderr" # multiple snapshots in one test
# Re-record deliberately: ./bashunit --snapshot-update --filter "test name" tests/
```

## Test Isolation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 71 assertions plus spies, mocks, data providers, snapshots and more.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ _bashunit() {
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches
assert_not_contains assert_not_empty assert_not_equals assert_not_matches
assert_not_same assert_same assert_string_ends_with
Expand Down
1 change: 1 addition & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches \
assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
Expand Down
30 changes: 29 additions & 1 deletion docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,7 @@ to narrow it (`bashunit doc json`).
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |
Expand DownExpand Up@@ -1633,6 +1633,34 @@ function test_failure() {
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Expand Down
6 changes: 3 additions & 3 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,10 +575,10 @@ steps:

> `bashunit test --snapshot-update`

Re-record snapshots: every `assert_match_snapshot` /
`assert_match_snapshot_ignore_colors` whose snapshot already exists is
Re-record snapshots: every snapshot assertion whose file already exists is
overwritten with the value this run produced, and reported as a recorded
snapshot instead of a pass. A missing snapshot is written as usual.
snapshot instead of a pass. A missing snapshot is written as usual. This
includes default, named and ignore-colors snapshots.

Use it when an output change is deliberate. It replaces deleting snapshot files
by hand — the path is derived from the test file and function name, so a wrong
Expand Down
5 changes: 3 additions & 2 deletions docs/public/bashunit-skill.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (71 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All@@ -132,7 +132,8 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
- JSON: `assert_json_equals`, `assert_json_contains`, `assert_json_key_exists`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`,
`assert_match_named_snapshot`, `assert_match_named_snapshot_ignore_colors`
- Spies: `assert_have_been_called`, `assert_not_called`, `assert_have_been_called_with`,
`assert_have_been_called_with_any`, `assert_have_been_called_with_args`,
`assert_have_been_called_times`, `assert_have_been_called_nth_with`
Expand Down
24 changes: 23 additions & 1 deletion docs/snapshots.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ Pass `snapshot_file` to point at a specific snapshot — useful to share one sna
By default each test gets its own, named after the test function.

::: tip
You can update the snapshot by deleting it and running its test again.
Update snapshots deliberately with `--snapshot-update`; combine it with `--filter` to
re-record a single test.
:::

::: code-group
Expand DownExpand Up@@ -77,6 +78,27 @@ function test_failure() {
```
:::

## Named snapshots

Use named snapshots when one test needs to capture several independent values:

> `assert_match_named_snapshot "name" "actual"`

```bash
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```

The name becomes a normalized filename suffix, so these resolve beside the default
snapshot as `test_render_modes.compact.snapshot` and
`test_render_modes.verbose.snapshot`. Spaces and punctuation are safe and cannot escape
the test's `snapshots/` directory.

Use `assert_match_named_snapshot_ignore_colors "name" "actual"` for the same behavior
with ANSI escape sequences removed.

## Placeholders

Snapshot files can contain placeholder tokens to ignore variable parts of the output.
Expand Down
41 changes: 39 additions & 2 deletions src/assert/snapshot.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,19 @@ function assert_match_snapshot() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
local snapshot_name=$1
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$2"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
# Only fork sed when the input actually carries an escape sequence; plain,
# colorless output takes a pure-bash fast path. The sed pattern is kept
Expand All@@ -44,6 +57,25 @@ function assert_match_snapshot_ignore_colors() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
local snapshot_name=$1
# Keep this byte-compatible with assert_match_snapshot_ignore_colors: only
# ANSI sequences ending in m/K are stripped, and only when ESC is present.
local stripped=$2
case "$stripped" in
*$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
esac
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$stripped"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

# Strips the `./` resolve_file prepends and squeezes doubled slashes, so a path
# recorded by an assertion and the same path as `find` prints it compare equal.
function bashunit::snapshot::normalize_path() {
Expand DownExpand Up@@ -214,6 +246,7 @@ _BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
local file_hint="$1"
local func_name="$2"
local snapshot_name="${3:-}"

if [ -n "$file_hint" ]; then
_BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
Expand All@@ -233,9 +266,13 @@ function bashunit::snapshot::resolve_file() {
bashunit::helper::normalize_variable_name_to_slot "$base_part"
local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
bashunit::helper::normalize_variable_name_to_slot "$func_name"
local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"
local name=$_BASHUNIT_HELPER_VARNAME_OUT
if [ -n "$snapshot_name" ]; then
bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
fi

_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot"
}

function bashunit::snapshot::initialize() {
Expand Down
12 changes: 8 additions & 4 deletions src/console/test_line.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,16 +162,21 @@ function bashunit::console_results::print_failed_snapshot_test() {

local line
line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
"$function_name" "$snapshot_file")"

if bashunit::dependencies::has_git; then
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
line="$line
$(bashunit::console_results::snapshot_line_diff \
"$(cat "$snapshot_file")" "$actual_content")"
fi

Expand DownExpand Up@@ -282,4 +287,3 @@ function bashunit::console_results::print_worker_stderr() {
"$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
sed 's/^/|/' "$stderr_file"
}

16 changes: 12 additions & 4 deletions tests/acceptance/bashunit_fail_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_a_test_fail_verbose_output_option() {
}

function test_different_verbose_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file" --detailed)"
}

function test_bashunit_when_a_test_fail_simple_output_env() {
Expand DownExpand Up@@ -54,6 +58,10 @@ function test_bashunit_with_a_test_fail_and_exit_immediately() {
}

function test_different_simple_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --simple)"
}
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_snapshot_named_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
FIXTURE="$(pwd)/tests/acceptance/fixtures/snapshot_named/named.sh"
}

function set_up() {
WORKDIR="$(mktemp -d)"
cp "$FIXTURE" "$WORKDIR/named.sh"
SNAPSHOTS="$WORKDIR/snapshots"
DEFAULT="$SNAPSHOTS/named_sh.test_named_snapshots.snapshot"
FIRST="$SNAPSHOTS/named_sh.test_named_snapshots.first_value.snapshot"
SAFE="$SNAPSHOTS/named_sh.test_named_snapshots._______second_.snapshot"
COLORED="$SNAPSHOTS/named_sh.test_named_snapshots.colored.snapshot"
}

function tear_down() {
rm -rf "$WORKDIR"
}

function test_named_snapshots_create_distinct_safe_files() {
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file named.sh) \
>/dev/null 2>&1

assert_same "default value" "$(<"$DEFAULT")"
assert_same "named value" "$(<"$FIRST")"
assert_same "safe value" "$(<"$SAFE")"
assert_same "colored value" "$(<"$COLORED")"
}

function test_named_snapshot_mismatch_names_the_path_and_update_flag() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

local output status=0
output=$(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
named.sh 2>&1) || status=$?

assert_same 1 "$status"
assert_contains "snapshots/named_sh.test_named_snapshots.first_value.snapshot" "$output"
assert_contains "--snapshot-update" "$output"
}

function test_snapshot_update_rewrites_a_named_snapshot() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
--snapshot-update named.sh) >/dev/null 2>&1

assert_same "named value" "$(<"$FIRST")"
}
8 changes: 6 additions & 2 deletions tests/acceptance/bashunit_stop_on_failure_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_stop_on_failure_env() {
}

function test_different_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_stop_on_failure.sh

assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --stop-on-failure "$test_file")"
assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_STOP_ON_FAILURE" "$test_file")"
}

function test_bashunit_when_stop_on_failure_env_simple_output() {
Expand Down
3 changes: 2 additions & 1 deletion tests/acceptance/bashunit_summary_output_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ function test_incomplete_tests_displayed_with_show_incomplete_flag() {
function test_both_flags_can_be_used_together() {
local output
output=$(./bashunit --env "$TEST_ENV_FILE" --no-parallel --simple --show-skipped --show-incomplete \
"tests/acceptance/bashunit_fail_test.sh" "tests/acceptance/bashunit_init_test.sh" 2>&1) || true
"tests/acceptance/bashunit_execution_error_test.sh" \
"tests/acceptance/bashunit_init_test.sh" 2>&1) || true

assert_contains "incomplete test" "$output"
assert_contains "skipped test" "$output"
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/fixtures/snapshot_named/named.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

function test_named_snapshots() {
assert_match_snapshot "default value"
assert_match_named_snapshot "first value" "named value"
assert_match_named_snapshot "../../ second!" "safe value"
assert_match_named_snapshot_ignore_colors "colored" $'\e[31mcolored value\e[0m'
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
..F..

There was 1 failure:

|1) ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
|✗ Failed: Assert failing
| Expected '1'
| but got  '0'
| at ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
| Source:
| 13: assert_same 1 0



Tests:  4 passed, 1 failed, 5 total
Assertions: 6 passed, 1 failed, 7 total

 Some tests failed 
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(snapshot): add named snapshots by Chemaclass · Pull Request #988 · TypedDevs/bashunit · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -91,8 +91,8 @@ function test_should_add() {

```bash
assert_match_snapshot "$output"
# Re-record: delete the snapshot file and re-run; the assertion writes it when missing
# (so a deleted snapshot never fails — read the diff before deleting)
assert_match_named_snapshot "stderr" "$stderr" # multiple snapshots in one test
# Re-record deliberately: ./bashunit --snapshot-update --filter "test name" tests/
```

## Test Isolation
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,7 @@
## Why bashunit

A lightweight, fast testing framework for **Bash 3.0+**, focused on developer experience.
It ships 71 assertions plus spies, mocks, data providers, snapshots and more.
It ships 73 assertions plus spies, mocks, data providers, snapshots and more.

## Quick start

Expand Down
1 change: 1 addition & 0 deletions completions/_bashunit
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@ _bashunit() {
assert_is_directory_writable assert_is_file assert_is_file_empty
assert_json_contains assert_json_equals assert_json_key_exists
assert_less_or_equal_than assert_less_than assert_line_count
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches
assert_not_contains assert_not_empty assert_not_equals assert_not_matches
assert_not_same assert_same assert_string_ends_with
Expand Down
1 change: 1 addition & 0 deletions completions/bashunit.bash
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,6 +45,7 @@ assert_is_directory_not_writable assert_is_directory_readable \
assert_is_directory_writable assert_is_file assert_is_file_empty \
assert_json_contains assert_json_equals assert_json_key_exists \
assert_less_or_equal_than assert_less_than assert_line_count \
assert_match_named_snapshot assert_match_named_snapshot_ignore_colors \
assert_match_snapshot assert_match_snapshot_ignore_colors assert_matches \
assert_not_contains assert_not_empty assert_not_equals assert_not_matches \
assert_not_same assert_same assert_string_ends_with \
Expand Down
30 changes: 29 additions & 1 deletion docs/assertions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,7 @@ to narrow it (`bashunit doc json`).
| **Arrays** | [assert_arrays_equal](#assert-arrays-equal) · [assert_array_contains](#assert-array-contains) · [assert_array_not_contains](#assert-array-not-contains) · [assert_array_length](#assert-array-length) |
| **JSON** | [assert_json_equals](#assert-json-equals) · [assert_json_contains](#assert-json-contains) · [assert_json_key_exists](#assert-json-key-exists) |
| **Duration** | [assert_duration](#assert-duration) · [assert_duration_less_than](#assert-duration-less-than) · [assert_duration_greater_than](#assert-duration-greater-than) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) |
| **Snapshots** | [assert_match_snapshot](#assert-match-snapshot) · [assert_match_named_snapshot](#assert-match-named-snapshot) · [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors) · [assert_match_named_snapshot_ignore_colors](#assert-match-named-snapshot-ignore-colors) |
| **Spies** | [assert_have_been_called](#assert-have-been-called) · [assert_not_called](#assert-not-called) · [assert_have_been_called_with](#assert-have-been-called-with) · [assert_have_been_called_with_any](#assert-have-been-called-with-any) · [assert_have_been_called_with_args](#assert-have-been-called-with-args) · [assert_have_been_called_nth_with](#assert-have-been-called-nth-with) · [assert_have_been_called_times](#assert-have-been-called-times) |
| **Assertions** | [assert_assertion_passes](#assert-assertion-passes) · [assert_assertion_fails](#assert-assertion-fails) · [assert_assertion_fails_with](#assert-assertion-fails-with) |
| **Manual failure** | [bashunit::fail](#bashunit-fail) |
Expand DownExpand Up@@ -1633,6 +1633,34 @@ function test_failure() {
```
:::

## assert_match_named_snapshot
> `assert_match_named_snapshot "name" "actual"`

Matches `actual` against a snapshot whose filename includes `name`. Use it for multiple independent snapshots in one test without constructing file paths yourself. Names are normalized so they cannot escape the test's `snapshots/` directory.

::: code-group
```bash [Example]
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_match_named_snapshot_ignore_colors
> `assert_match_named_snapshot_ignore_colors "name" "actual"`

Named version of [assert_match_snapshot_ignore_colors](#assert-match-snapshot-ignore-colors). ANSI escape sequences are stripped from `actual` before it is stored or compared.

::: code-group
```bash [Example]
function test_colored_render_modes() {
assert_match_named_snapshot_ignore_colors "compact" "$(./bin/render --compact)"
assert_match_named_snapshot_ignore_colors "verbose" "$(./bin/render --verbose)"
}
```
:::

## assert_have_been_called
> `assert_have_been_called "command"`

Expand Down
6 changes: 3 additions & 3 deletions docs/command-line.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -575,10 +575,10 @@ steps:

> `bashunit test --snapshot-update`

Re-record snapshots: every `assert_match_snapshot` /
`assert_match_snapshot_ignore_colors` whose snapshot already exists is
Re-record snapshots: every snapshot assertion whose file already exists is
overwritten with the value this run produced, and reported as a recorded
snapshot instead of a pass. A missing snapshot is written as usual.
snapshot instead of a pass. A missing snapshot is written as usual. This
includes default, named and ignore-colors snapshots.

Use it when an output change is deliberate. It replaces deleting snapshot files
by hand — the path is derived from the test file and function name, so a wrong
Expand Down
5 changes: 3 additions & 2 deletions docs/public/bashunit-skill.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -118,7 +118,7 @@ cleaned up automatically and are safe under `--parallel`.

## Assertions

`bashunit doc` prints the full catalogue (71 assertions) locally; `bashunit doc contains`
`bashunit doc` prints the full catalogue (73 assertions) locally; `bashunit doc contains`
filters it. The same list is at https://bashunit.com/assertions. **Do not invent names**
— a wrong name is a runtime error, not a failed assertion.

Expand All@@ -132,7 +132,8 @@ filters it. The same list is at https://bashunit.com/assertions. **Do not invent
`assert_directory_exists`, `assert_file_permissions`
- Arrays: `assert_array_contains`, `assert_array_length`, `assert_arrays_equal`
- JSON: `assert_json_equals`, `assert_json_contains`, `assert_json_key_exists`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`
- Snapshots: `assert_match_snapshot`, `assert_match_snapshot_ignore_colors`,
`assert_match_named_snapshot`, `assert_match_named_snapshot_ignore_colors`
- Spies: `assert_have_been_called`, `assert_not_called`, `assert_have_been_called_with`,
`assert_have_been_called_with_any`, `assert_have_been_called_with_args`,
`assert_have_been_called_times`, `assert_have_been_called_nth_with`
Expand Down
24 changes: 23 additions & 1 deletion docs/snapshots.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,8 @@ Pass `snapshot_file` to point at a specific snapshot — useful to share one sna
By default each test gets its own, named after the test function.

::: tip
You can update the snapshot by deleting it and running its test again.
Update snapshots deliberately with `--snapshot-update`; combine it with `--filter` to
re-record a single test.
:::

::: code-group
Expand DownExpand Up@@ -77,6 +78,27 @@ function test_failure() {
```
:::

## Named snapshots

Use named snapshots when one test needs to capture several independent values:

> `assert_match_named_snapshot "name" "actual"`

```bash
function test_render_modes() {
assert_match_named_snapshot "compact" "$(./bin/render --compact)"
assert_match_named_snapshot "verbose" "$(./bin/render --verbose)"
}
```

The name becomes a normalized filename suffix, so these resolve beside the default
snapshot as `test_render_modes.compact.snapshot` and
`test_render_modes.verbose.snapshot`. Spaces and punctuation are safe and cannot escape
the test's `snapshots/` directory.

Use `assert_match_named_snapshot_ignore_colors "name" "actual"` for the same behavior
with ANSI escape sequences removed.

## Placeholders

Snapshot files can contain placeholder tokens to ignore variable parts of the output.
Expand Down
41 changes: 39 additions & 2 deletions src/assert/snapshot.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,19 @@ function assert_match_snapshot() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot() {
local snapshot_name=$1
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$2"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_snapshot_ignore_colors() {
# Only fork sed when the input actually carries an escape sequence; plain,
# colorless output takes a pure-bash fast path. The sed pattern is kept
Expand All@@ -44,6 +57,25 @@ function assert_match_snapshot_ignore_colors() {
bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

function assert_match_named_snapshot_ignore_colors() {
local snapshot_name=$1
# Keep this byte-compatible with assert_match_snapshot_ignore_colors: only
# ANSI sequences ending in m/K are stripped, and only when ESC is present.
local stripped=$2
case "$stripped" in
*$'\e'*) stripped=$(printf '%s' "$stripped" | sed 's/\x1B\[[0-9;]*[mK]//g') ;;
esac
local _snapshot_normalized
bashunit::snapshot::normalize_actual "$stripped"
local actual=$_snapshot_normalized
bashunit::helper::find_test_function_name_to_slot
local test_fn=$_BASHUNIT_HELPER_TESTFN_OUT
bashunit::snapshot::resolve_file "" "$test_fn" "$snapshot_name"
local snapshot_file=$_BASHUNIT_SNAPSHOT_FILE_OUT

bashunit::snapshot::assert "$actual" "$snapshot_file" "$test_fn"
}

# Strips the `./` resolve_file prepends and squeezes doubled slashes, so a path
# recorded by an assertion and the same path as `find` prints it compare equal.
function bashunit::snapshot::normalize_path() {
Expand DownExpand Up@@ -214,6 +246,7 @@ _BASHUNIT_SNAPSHOT_FILE_OUT=""
function bashunit::snapshot::resolve_file() {
local file_hint="$1"
local func_name="$2"
local snapshot_name="${3:-}"

if [ -n "$file_hint" ]; then
_BASHUNIT_SNAPSHOT_FILE_OUT=$file_hint
Expand All@@ -233,9 +266,13 @@ function bashunit::snapshot::resolve_file() {
bashunit::helper::normalize_variable_name_to_slot "$base_part"
local test_file=$_BASHUNIT_HELPER_VARNAME_OUT
bashunit::helper::normalize_variable_name_to_slot "$func_name"
local name="$_BASHUNIT_HELPER_VARNAME_OUT.snapshot"
local name=$_BASHUNIT_HELPER_VARNAME_OUT
if [ -n "$snapshot_name" ]; then
bashunit::helper::normalize_variable_name_to_slot "$snapshot_name"
name="$name.$_BASHUNIT_HELPER_VARNAME_OUT"
fi

_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}"
_BASHUNIT_SNAPSHOT_FILE_OUT="./${dir_part}/snapshots/${test_file}.${name}.snapshot"
}

function bashunit::snapshot::initialize() {
Expand Down
12 changes: 8 additions & 4 deletions src/console/test_line.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -162,16 +162,21 @@ function bashunit::console_results::print_failed_snapshot_test() {

local line
line="$(printf "${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}\n" "$function_name")"
${_BASHUNIT_COLOR_FAINT}Expected to match the snapshot${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Snapshot: %s${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}Re-record with '--snapshot-update'${_BASHUNIT_COLOR_DEFAULT}\n" \
"$function_name" "$snapshot_file")"

if bashunit::dependencies::has_git; then
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
line="$line
$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
line="$line
$(bashunit::console_results::snapshot_line_diff \
"$(cat "$snapshot_file")" "$actual_content")"
fi

Expand DownExpand Up@@ -282,4 +287,3 @@ function bashunit::console_results::print_worker_stderr() {
"$_BASHUNIT_COLOR_SKIPPED" "$test_file" "$_BASHUNIT_COLOR_DEFAULT"
sed 's/^/|/' "$stderr_file"
}

16 changes: 12 additions & 4 deletions tests/acceptance/bashunit_fail_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_a_test_fail_verbose_output_option() {
}

function test_different_verbose_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file" --detailed)"
}

function test_bashunit_when_a_test_fail_simple_output_env() {
Expand DownExpand Up@@ -54,6 +58,10 @@ function test_bashunit_with_a_test_fail_and_exit_immediately() {
}

function test_different_simple_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh

assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_SIMPLE" "$test_file")"
assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" --simple)"
}
54 changes: 54 additions & 0 deletions tests/acceptance/bashunit_snapshot_named_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
FIXTURE="$(pwd)/tests/acceptance/fixtures/snapshot_named/named.sh"
}

function set_up() {
WORKDIR="$(mktemp -d)"
cp "$FIXTURE" "$WORKDIR/named.sh"
SNAPSHOTS="$WORKDIR/snapshots"
DEFAULT="$SNAPSHOTS/named_sh.test_named_snapshots.snapshot"
FIRST="$SNAPSHOTS/named_sh.test_named_snapshots.first_value.snapshot"
SAFE="$SNAPSHOTS/named_sh.test_named_snapshots._______second_.snapshot"
COLORED="$SNAPSHOTS/named_sh.test_named_snapshots.colored.snapshot"
}

function tear_down() {
rm -rf "$WORKDIR"
}

function test_named_snapshots_create_distinct_safe_files() {
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file named.sh) \
>/dev/null 2>&1

assert_same "default value" "$(<"$DEFAULT")"
assert_same "named value" "$(<"$FIRST")"
assert_same "safe value" "$(<"$SAFE")"
assert_same "colored value" "$(<"$COLORED")"
}

function test_named_snapshot_mismatch_names_the_path_and_update_flag() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

local output status=0
output=$(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
named.sh 2>&1) || status=$?

assert_same 1 "$status"
assert_contains "snapshots/named_sh.test_named_snapshots.first_value.snapshot" "$output"
assert_contains "--snapshot-update" "$output"
}

function test_snapshot_update_rewrites_a_named_snapshot() {
mkdir -p "$SNAPSHOTS"
printf 'stale value\n' >"$FIRST"

(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --skip-env-file \
--snapshot-update named.sh) >/dev/null 2>&1

assert_same "named value" "$(<"$FIRST")"
}
8 changes: 6 additions & 2 deletions tests/acceptance/bashunit_stop_on_failure_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,8 +21,12 @@ function test_bashunit_when_stop_on_failure_env() {
}

function test_different_snapshots_matches() {
bashunit::todo \
"The different snapshots for these tests should also be identical, option to choose snapshot name?"
local test_file=./tests/acceptance/fixtures/test_bashunit_when_stop_on_failure.sh

assert_match_named_snapshot "option" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --stop-on-failure "$test_file")"
assert_match_named_snapshot "env" \
"$(./bashunit --no-parallel --env "$TEST_ENV_FILE_STOP_ON_FAILURE" "$test_file")"
}

function test_bashunit_when_stop_on_failure_env_simple_output() {
Expand Down
3 changes: 2 additions & 1 deletion tests/acceptance/bashunit_summary_output_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,7 +42,8 @@ function test_incomplete_tests_displayed_with_show_incomplete_flag() {
function test_both_flags_can_be_used_together() {
local output
output=$(./bashunit --env "$TEST_ENV_FILE" --no-parallel --simple --show-skipped --show-incomplete \
"tests/acceptance/bashunit_fail_test.sh" "tests/acceptance/bashunit_init_test.sh" 2>&1) || true
"tests/acceptance/bashunit_execution_error_test.sh" \
"tests/acceptance/bashunit_init_test.sh" 2>&1) || true

assert_contains "incomplete test" "$output"
assert_contains "skipped test" "$output"
Expand Down
8 changes: 8 additions & 0 deletions tests/acceptance/fixtures/snapshot_named/named.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash

function test_named_snapshots() {
assert_match_snapshot "default value"
assert_match_named_snapshot "first value" "named value"
assert_match_named_snapshot "../../ second!" "safe value"
assert_match_named_snapshot_ignore_colors "colored" $'\e[31mcolored value\e[0m'
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
..F..

There was 1 failure:

|1) ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
|✗ Failed: Assert failing
| Expected '1'
| but got  '0'
| at ./tests/acceptance/fixtures/test_bashunit_when_a_test_fail.sh:12
| Source:
| 13: assert_same 1 0



Tests:  4 passed, 1 failed, 5 total
Assertions: 6 passed, 1 failed, 7 total

 Some tests failed 
Loading
Loading