diff --git a/CHANGELOG.md b/CHANGELOG.md index e9eb7bbf..77ccd24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +### Fixed +- Custom assertions now display the correct test function name in failure messages + ## [0.28.0](https://github.com/TypedDevs/bashunit/compare/0.27.0...0.28.0) - 2025-12-01 ### Added diff --git a/docs/custom-asserts.md b/docs/custom-asserts.md index a85e4c83..b8f3f117 100644 --- a/docs/custom-asserts.md +++ b/docs/custom-asserts.md @@ -10,16 +10,33 @@ Check the internal functional tests: `tests/functional/custom_asserts_test.sh` ( When using the bashunit facade, assertions automatically respect the guard behavior: if a previous assertion in the same test already failed, subsequent assertions are skipped. This matches popular testing libraries default behavior. ::: -## assertion_failed +::: info Test name detection +Custom assertions automatically display the correct **test function name** in failure messages, not the custom assertion name. This makes it easy to identify which test failed, even when using deeply nested custom assertions. +::: + +## API Reference + +### assertion_failed > `bashunit::assertion_failed ` -## assertion_passed +Marks the current assertion as failed and prints a failure message. + +| Parameter | Description | +|-----------|-------------| +| `expected` | The expected value | +| `actual` | The actual value received | +| `failure_condition_message` | Optional message describing the failure condition (default: "but got") | + +### assertion_passed > `bashunit::assertion_passed` -## Example +Marks the current assertion as passed. Call this when your custom assertion succeeds. + +## Examples + +### Basic custom assertion ```bash -# Your custom assert using the bashunit facade function assert_foo() { local actual="$1" @@ -31,8 +48,76 @@ function assert_foo() { bashunit::assertion_passed } -# Your test using your custom assert -function test_assert_foo_passed() { - assert_foo "foo" +function test_value_is_foo() { + assert_foo "foo" # Passes +} + +function test_value_is_not_foo() { + assert_foo "bar" # Fails with: "Failed: Value is not foo" +} +``` + +### Using fail() for simple messages + +You can also use `fail` for custom assertions that just need a message: + +```bash +function assert_valid_json() { + local json="$1" + + if ! echo "$json" | jq . > /dev/null 2>&1; then + fail "Invalid JSON: $json" + return + fi + + bashunit::assertion_passed +} + +function test_api_returns_valid_json() { + local response='{"status": "ok"}' + assert_valid_json "$response" } ``` + +### Composing with existing assertions + +Custom assertions can call other bashunit assertions internally: + +```bash +function assert_http_success() { + local status_code="$1" + + assert_greater_or_equal_than "200" "$status_code" + assert_less_than "300" "$status_code" +} + +function test_api_returns_success() { + local status_code=200 + assert_http_success "$status_code" +} +``` + +### Custom assertion with custom failure message + +```bash +function assert_positive_number() { + local actual="$1" + + if [[ "$actual" -le 0 ]]; then + bashunit::assertion_failed "positive number" "$actual" "got" + return + fi + + bashunit::assertion_passed +} +``` + +## Best practices + +1. **Always return after failure**: Call `return` after `bashunit::assertion_failed` or `fail` to stop execution of your custom assertion. + +2. **Always mark success**: Call `bashunit::assertion_passed` or `state::add_assertions_passed` when your assertion succeeds. + +3. **Use descriptive names**: Name your custom assertions clearly, e.g., `assert_valid_email`, `assert_file_contains_header`. + +4. **Keep assertions focused**: Each custom assertion should test one specific condition. diff --git a/src/assert.sh b/src/assert.sh index 6f0f3099..85bf19ee 100755 --- a/src/assert.sh +++ b/src/assert.sh @@ -11,8 +11,10 @@ function fail() { local message="${1:-${FUNCNAME[1]}}" + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failure_message "${label}" "$message" } @@ -77,8 +79,10 @@ function run_command_or_eval() { function handle_bool_assertion_failure() { local expected="$1" local got="$2" + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[2]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "$label" "$expected" "but got " "$got" @@ -91,8 +95,10 @@ function assert_same() { local actual="$2" if [[ "$expected" != "$actual" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${expected}" "but got " "${actual}" return @@ -113,8 +119,10 @@ function assert_equals() { expected_cleaned=$(str::strip_ansi "$expected") if [[ "$expected_cleaned" != "$actual_cleaned" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${expected_cleaned}" "but got " "${actual_cleaned}" return @@ -135,8 +143,10 @@ function assert_not_equals() { expected_cleaned=$(str::strip_ansi "$expected") if [[ "$expected_cleaned" == "$actual_cleaned" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${expected_cleaned}" "but got " "${actual_cleaned}" return @@ -151,8 +161,10 @@ function assert_empty() { local expected="$1" if [[ "$expected" != "" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "to be empty" "but got " "${expected}" return @@ -167,8 +179,10 @@ function assert_not_empty() { local expected="$1" if [[ "$expected" == "" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "to not be empty" "but got " "${expected}" return @@ -184,8 +198,10 @@ function assert_not_same() { local actual="$2" if [[ "$expected" == "$actual" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${expected}" "but got " "${actual}" return @@ -203,8 +219,10 @@ function assert_contains() { actual=$(printf '%s\n' "${actual_arr[@]}") if ! [[ $actual == *"$expected"* ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to contain" "${expected}" return @@ -222,8 +240,10 @@ function assert_contains_ignore_case() { shopt -s nocasematch if ! [[ $actual =~ $expected ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to contain" "${expected}" shopt -u nocasematch @@ -243,8 +263,10 @@ function assert_not_contains() { actual=$(printf '%s\n' "${actual_arr[@]}") if [[ $actual == *"$expected"* ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to not contain" "${expected}" return @@ -262,8 +284,10 @@ function assert_matches() { actual=$(printf '%s\n' "${actual_arr[@]}") if ! [[ $actual =~ $expected ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to match" "${expected}" return @@ -281,8 +305,10 @@ function assert_not_matches() { actual=$(printf '%s\n' "${actual_arr[@]}") if [[ $actual =~ $expected ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to not match" "${expected}" return @@ -364,8 +390,10 @@ function assert_exec() { fi if [[ $failed -eq 1 ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "$label" "$expected_desc" "but got " "$actual_desc" return @@ -381,8 +409,10 @@ function assert_exit_code() { local expected_exit_code="$1" if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be" "${expected_exit_code}" return @@ -398,8 +428,10 @@ function assert_successful_code() { local expected_exit_code=0 if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}" return @@ -413,8 +445,10 @@ function assert_unsuccessful_code() { (( _ASSERTION_FAILED_IN_TEST )) && return 0 if [[ "$actual_exit_code" -eq 0 ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be non-zero" "but was 0" return @@ -430,8 +464,10 @@ function assert_general_error() { local expected_exit_code=1 if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}" return @@ -447,8 +483,10 @@ function assert_command_not_found() { local expected_exit_code=127 if [[ $actual_exit_code -ne "$expected_exit_code" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual_exit_code}" "to be exactly" "${expected_exit_code}" return @@ -466,8 +504,10 @@ function assert_string_starts_with() { actual=$(printf '%s\n' "${actual_arr[@]}") if [[ $actual != "$expected"* ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to start with" "${expected}" return @@ -483,8 +523,10 @@ function assert_string_not_starts_with() { local actual="$2" if [[ $actual == "$expected"* ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to not start with" "${expected}" return @@ -502,8 +544,10 @@ function assert_string_ends_with() { actual=$(printf '%s\n' "${actual_arr[@]}") if [[ $actual != *"$expected" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to end with" "${expected}" return @@ -521,8 +565,10 @@ function assert_string_not_ends_with() { actual=$(printf '%s\n' "${actual_arr[@]}") if [[ $actual == *"$expected" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to not end with" "${expected}" return @@ -538,8 +584,10 @@ function assert_less_than() { local actual="$2" if ! [[ "$actual" -lt "$expected" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to be less than" "${expected}" return @@ -555,8 +603,10 @@ function assert_less_or_equal_than() { local actual="$2" if ! [[ "$actual" -le "$expected" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to be less or equal than" "${expected}" return @@ -572,8 +622,10 @@ function assert_greater_than() { local actual="$2" if ! [[ "$actual" -gt "$expected" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to be greater than" "${expected}" return @@ -589,8 +641,10 @@ function assert_greater_or_equal_than() { local actual="$2" if ! [[ "$actual" -ge "$expected" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${actual}" "to be greater or equal than" "${expected}" return @@ -618,8 +672,10 @@ function assert_line_count() { fi if [[ "$expected" != "$actual" ]]; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${input_str}"\ diff --git a/src/assert_arrays.sh b/src/assert_arrays.sh index fb45bf43..c5fe6ac6 100644 --- a/src/assert_arrays.sh +++ b/src/assert_arrays.sh @@ -2,8 +2,10 @@ function assert_array_contains() { local expected="$1" + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" shift local actual=("${@}") @@ -19,7 +21,10 @@ function assert_array_contains() { function assert_array_not_contains() { local expected="$1" - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label + label="$(helper::normalize_test_function_name "$test_fn")" shift local actual=("$@") diff --git a/src/assert_files.sh b/src/assert_files.sh index e72fc312..a8384ac6 100644 --- a/src/assert_files.sh +++ b/src/assert_files.sh @@ -2,7 +2,9 @@ function assert_file_exists() { local expected="$1" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${3:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -f "$expected" ]]; then state::add_assertions_failed @@ -15,7 +17,9 @@ function assert_file_exists() { function assert_file_not_exists() { local expected="$1" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${3:-$(helper::normalize_test_function_name "$test_fn")}" if [[ -f "$expected" ]]; then state::add_assertions_failed @@ -28,7 +32,9 @@ function assert_file_not_exists() { function assert_is_file() { local expected="$1" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${3:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -f "$expected" ]]; then state::add_assertions_failed @@ -41,7 +47,9 @@ function assert_is_file() { function assert_is_file_empty() { local expected="$1" - local label="${3:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${3:-$(helper::normalize_test_function_name "$test_fn")}" if [[ -s "$expected" ]]; then state::add_assertions_failed @@ -57,8 +65,10 @@ function assert_files_equals() { local actual="$2" if [[ "$(diff -u "$expected" "$actual")" != '' ]] ; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" state::add_assertions_failed console_results::print_failed_test "${label}" "${expected}" "Compared" "${actual}" \ @@ -74,8 +84,10 @@ function assert_files_not_equals() { local actual="$2" if [[ "$(diff -u "$expected" "$actual")" == '' ]] ; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" state::add_assertions_failed console_results::print_failed_test "${label}" "${expected}" "Compared" "${actual}" \ @@ -91,8 +103,10 @@ function assert_file_contains() { local string="$2" if ! grep -F -q "$string" "$file"; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" state::add_assertions_failed console_results::print_failed_test "${label}" "${file}" "to contain" "${string}" @@ -107,8 +121,10 @@ function assert_file_not_contains() { local string="$2" if grep -q "$string" "$file"; then + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" + label="$(helper::normalize_test_function_name "$test_fn")" state::add_assertions_failed console_results::print_failed_test "${label}" "${file}" "to not contain" "${string}" diff --git a/src/assert_folders.sh b/src/assert_folders.sh index 418819a3..c8f0ac74 100644 --- a/src/assert_folders.sh +++ b/src/assert_folders.sh @@ -2,7 +2,9 @@ function assert_directory_exists() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -d "$expected" ]]; then state::add_assertions_failed @@ -15,7 +17,9 @@ function assert_directory_exists() { function assert_directory_not_exists() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ -d "$expected" ]]; then state::add_assertions_failed @@ -28,7 +32,9 @@ function assert_directory_not_exists() { function assert_is_directory() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -d "$expected" ]]; then state::add_assertions_failed @@ -41,7 +47,9 @@ function assert_is_directory() { function assert_is_directory_empty() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -d "$expected" || -n "$(ls -A "$expected")" ]]; then state::add_assertions_failed @@ -54,7 +62,9 @@ function assert_is_directory_empty() { function assert_is_directory_not_empty() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -d "$expected" || -z "$(ls -A "$expected")" ]]; then state::add_assertions_failed @@ -67,7 +77,9 @@ function assert_is_directory_not_empty() { function assert_is_directory_readable() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -d "$expected" || ! -r "$expected" || ! -x "$expected" ]]; then state::add_assertions_failed @@ -80,7 +92,9 @@ function assert_is_directory_readable() { function assert_is_directory_not_readable() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -d "$expected" ]] || [[ -r "$expected" && -x "$expected" ]]; then state::add_assertions_failed @@ -93,7 +107,9 @@ function assert_is_directory_not_readable() { function assert_is_directory_writable() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -d "$expected" || ! -w "$expected" ]]; then state::add_assertions_failed @@ -106,7 +122,9 @@ function assert_is_directory_writable() { function assert_is_directory_not_writable() { local expected="$1" - local label="${2:-$(helper::normalize_test_function_name "${FUNCNAME[1]}")}" + local test_fn + test_fn="$(helper::find_test_function_name)" + local label="${2:-$(helper::normalize_test_function_name "$test_fn")}" if [[ ! -d "$expected" || -w "$expected" ]]; then state::add_assertions_failed diff --git a/src/assert_snapshot.sh b/src/assert_snapshot.sh index 8a1954d4..33bb40f2 100644 --- a/src/assert_snapshot.sh +++ b/src/assert_snapshot.sh @@ -3,26 +3,30 @@ function assert_match_snapshot() { local actual=$(echo -n "$1" | tr -d '\r') - local snapshot_file=$(snapshot::resolve_file "${2:-}" "${FUNCNAME[1]}") + local test_fn + test_fn="$(helper::find_test_function_name)" + local snapshot_file=$(snapshot::resolve_file "${2:-}" "$test_fn") if [[ ! -f "$snapshot_file" ]]; then snapshot::initialize "$snapshot_file" "$actual" return fi - snapshot::compare "$actual" "$snapshot_file" "${FUNCNAME[1]}" + snapshot::compare "$actual" "$snapshot_file" "$test_fn" } function assert_match_snapshot_ignore_colors() { local actual=$(echo -n "$1" | sed 's/\x1B\[[0-9;]*[mK]//g' | tr -d '\r') - local snapshot_file=$(snapshot::resolve_file "${2:-}" "${FUNCNAME[1]}") + local test_fn + test_fn="$(helper::find_test_function_name)" + local snapshot_file=$(snapshot::resolve_file "${2:-}" "$test_fn") if [[ ! -f "$snapshot_file" ]]; then snapshot::initialize "$snapshot_file" "$actual" return fi - snapshot::compare "$actual" "$snapshot_file" "${FUNCNAME[1]}" + snapshot::compare "$actual" "$snapshot_file" "$test_fn" } function snapshot::match_with_placeholder() { diff --git a/src/bashunit.sh b/src/bashunit.sh index 196c84b7..30fd32cf 100644 --- a/src/bashunit.sh +++ b/src/bashunit.sh @@ -11,8 +11,10 @@ function bashunit::assertion_failed() { local actual=$2 local failure_condition_message=${3:-"but got "} + local test_fn + test_fn="$(helper::find_test_function_name)" local label - label="$(helper::normalize_test_function_name "${FUNCNAME[2]}")" + label="$(helper::normalize_test_function_name "$test_fn")" assert::mark_failed console_results::print_failed_test "${label}" "${expected}" \ "$failure_condition_message" "${actual}" diff --git a/src/helpers.sh b/src/helpers.sh index df6c9585..e8b23fdb 100755 --- a/src/helpers.sh +++ b/src/helpers.sh @@ -2,6 +2,33 @@ declare -r BASHUNIT_GIT_REPO="https://github.com/TypedDevs/bashunit" +# +# Walks up the call stack to find the first function that looks like a test function. +# A test function is one that starts with "test_" or "test" (camelCase). +# If no test function is found, falls back to the caller of the assertion function. +# +# @param $1 number Optional fallback depth (default: 2, i.e., the caller of the assertion) +# +# @return string The test function name, or fallback function name +# +function helper::find_test_function_name() { + local fallback_depth="${1:-2}" + local i + for ((i = 0; i < ${#FUNCNAME[@]}; i++)); do + local fn="${FUNCNAME[$i]}" + # Check if function starts with "test_" or "test" followed by uppercase + if [[ "$fn" == test_* ]] || [[ "$fn" =~ ^test[A-Z] ]]; then + echo "$fn" + return + fi + done + # No test function found, use fallback (caller of the assertion) + # FUNCNAME[0] = helper::find_test_function_name + # FUNCNAME[1] = the assertion function (e.g., assert_same) + # FUNCNAME[2] = caller of the assertion + echo "${FUNCNAME[$fallback_depth]:-}" +} + # # @param $1 string Eg: "test_some_logic_camelCase" # diff --git a/tests/unit/custom_assertions_test.sh b/tests/unit/custom_assertions_test.sh new file mode 100644 index 00000000..7c811704 --- /dev/null +++ b/tests/unit/custom_assertions_test.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash + +# Custom assertion that uses fail internally +function _assert_valid_json() { + local json="$1" + + if ! echo "$json" | jq . > /dev/null 2>&1; then + fail "Invalid json: $json" + return + fi + + state::add_assertions_passed +} + +# Custom assertion that uses bashunit::assertion_failed +function _assert_positive_number() { + local number="$1" + + if ! [[ "$number" =~ ^[0-9]+$ ]] || [[ "$number" -le 0 ]]; then + bashunit::assertion_failed "positive number" "$number" + return + fi + + bashunit::assertion_passed +} + +# Custom assertion that uses assert_same internally +function _assert_length_equals() { + local expected_length="$1" + local string="$2" + local actual_length=${#string} + + assert_same "$expected_length" "$actual_length" +} + +# Tests + +function test_custom_assertion_with_fail_shows_correct_test_name() { + # This test verifies that when a custom assertion uses fail(), + # the failure message shows the test function name, not the custom assertion name + local output + output="$( + # Temporarily override state::print_line to capture output + _captured_output="" + # shellcheck disable=SC2317,SC2329 + state::print_line() { + _captured_output="$2" + echo "$_captured_output" + } + + # Force a failure using our custom assertion with invalid JSON + _ASSERTION_FAILED_IN_TEST=0 + _assert_valid_json "invalid json" + + echo "$_captured_output" + )" + + # The output should contain the test function name (normalized from the test function) + assert_contains "Custom assertion with fail shows correct test name" "$output" + assert_not_contains "Assert valid json" "$output" +} + +function test_custom_assertion_with_bashunit_assertion_failed_shows_correct_test_name() { + # This test verifies that when a custom assertion uses bashunit::assertion_failed(), + # the failure message shows the test function name, not the custom assertion name + local output + output="$( + _captured_output="" + # shellcheck disable=SC2317,SC2329 + state::print_line() { + _captured_output="$2" + echo "$_captured_output" + } + + _ASSERTION_FAILED_IN_TEST=0 + _assert_positive_number "-5" + + echo "$_captured_output" + )" + + assert_contains \ + "Custom assertion with bashunit assertion failed shows correct test name" "$output" + assert_not_contains "Assert positive number" "$output" +} + +function test_custom_assertion_calling_assert_same_shows_correct_test_name() { + # This test verifies that when a custom assertion calls another assertion like assert_same, + # the failure message shows the test function name, not the intermediate assertion name + local output + output="$( + _captured_output="" + # shellcheck disable=SC2317,SC2329 + state::print_line() { + _captured_output="$2" + echo "$_captured_output" + } + + _ASSERTION_FAILED_IN_TEST=0 + _assert_length_equals "5" "abc" # length is 3, not 5 + + echo "$_captured_output" + )" + + assert_contains "Custom assertion calling assert same shows correct test name" "$output" + assert_not_contains "Assert length equals" "$output" + assert_not_contains "Assert same" "$output" +} + +function test_helper_find_test_function_name_finds_test() { + # Test that helper::find_test_function_name correctly finds the test function + local found_name + found_name="$(helper::find_test_function_name)" + + assert_same "test_helper_find_test_function_name_finds_test" "$found_name" +} + +function test_helper_find_test_function_name_from_nested_function() { + # Test that helper::find_test_function_name works from nested functions + _inner_function() { + helper::find_test_function_name + } + + local found_name + found_name="$(_inner_function)" + + assert_same "test_helper_find_test_function_name_from_nested_function" "$found_name" +} + +function test_helper_find_test_function_name_from_deeply_nested() { + # Test from deeply nested functions + _level3() { + helper::find_test_function_name + } + + _level2() { + _level3 + } + + _level1() { + _level2 + } + + local found_name + found_name="$(_level1)" + + assert_same "test_helper_find_test_function_name_from_deeply_nested" "$found_name" +} +