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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
run: |
docker run --rm -v "$(pwd)":/project alpine:latest /bin/sh -c " \
apk update && \
apk add --no-cache bash make git && \
apk add --no-cache bash make git jq && \
adduser -D builder && \
chown -R builder /project && \
su - builder -c 'cd /project; make test';"
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added
- Add `assert_have_been_called_nth_with` for verifying arguments on the Nth invocation of a spy
- Add `assert_string_matches_format` and `assert_string_not_matches_format` with format placeholders (`%d`, `%s`, `%f`, `%i`, `%x`, `%e`, `%%`)
- Add JSON assertions: `assert_json_key_exists`, `assert_json_contains`, `assert_json_equals` (requires `jq`)

### Changed
- Split Windows CI test jobs into parallel chunks to avoid timeouts
Expand Down
54 changes: 54 additions & 0 deletions docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,60 @@ function test_failure() {
```
:::

## assert_json_key_exists
> `assert_json_key_exists "key" "json"`

Reports an error if `key` does not exist in the JSON string. Uses [jq](https://jqlang.github.io/jq/) syntax for key paths. Requires `jq` to be installed; if missing the test is skipped.

::: code-group
```bash [Example]
function test_success() {
assert_json_key_exists ".name" '{"name":"bashunit","version":"1.0"}'
assert_json_key_exists ".data.id" '{"data":{"id":42}}'
}

function test_failure() {
assert_json_key_exists ".missing" '{"name":"bashunit"}'
}
```
:::

## assert_json_contains
> `assert_json_contains "key" "expected" "json"`

Reports an error if `key` does not exist in the JSON string or its value does not equal `expected`. Uses [jq](https://jqlang.github.io/jq/) syntax for key paths. Requires `jq` to be installed; if missing the test is skipped.

::: code-group
```bash [Example]
function test_success() {
assert_json_contains ".name" "bashunit" '{"name":"bashunit","version":"1.0"}'
assert_json_contains ".count" "42" '{"count":42}'
}

function test_failure() {
assert_json_contains ".name" "other" '{"name":"bashunit"}'
assert_json_contains ".missing" "value" '{"name":"bashunit"}'
}
```
:::

## assert_json_equals
> `assert_json_equals "expected" "actual"`

Reports an error if the two JSON strings are not structurally equal. Key order is ignored. Requires `jq` to be installed; if missing the test is skipped.

::: code-group
```bash [Example]
function test_success() {
assert_json_equals '{"b":2,"a":1}' '{"a":1,"b":2}'
}

function test_failure() {
assert_json_equals '{"a":1}' '{"a":2}'
}
```
:::

## bashunit::fail
> `bashunit::fail "failure message"`

Expand Down
87 changes: 87 additions & 0 deletions src/assert_json.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env bash

function bashunit::assert_json::require_jq() {
if ! command -v jq >/dev/null 2>&1; then
bashunit::skip "jq is required for JSON assertions"
return 1
fi
return 0
}

function assert_json_key_exists() {
bashunit::assert::should_skip && return 0
bashunit::assert_json::require_jq || return 0

local key="$1"
local json="$2"

local result
if ! result=$(printf '%s' "$json" | jq -e "$key" 2>/dev/null) || [ "$result" = "null" ]; then
local test_fn
test_fn="$(bashunit::helper::find_test_function_name)"
local label
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
bashunit::assert::mark_failed
bashunit::console_results::print_failed_test "${label}" "${json}" "to have key" "${key}"
return
fi

bashunit::state::add_assertions_passed
}

function assert_json_contains() {
bashunit::assert::should_skip && return 0
bashunit::assert_json::require_jq || return 0

local key="$1"
local expected="$2"
local json="$3"

local result
if ! result=$(printf '%s' "$json" | jq -e -r "$key" 2>/dev/null) || [ "$result" = "null" ]; then
local test_fn
test_fn="$(bashunit::helper::find_test_function_name)"
local label
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
bashunit::assert::mark_failed
bashunit::console_results::print_failed_test "${label}" "${json}" "to have key" "${key}"
return
fi

if [ "$result" != "$expected" ]; then
local test_fn
test_fn="$(bashunit::helper::find_test_function_name)"
local label
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
bashunit::assert::mark_failed
bashunit::console_results::print_failed_test "${label}" "${expected}" "but got " "${result}"
return
fi

bashunit::state::add_assertions_passed
}

function assert_json_equals() {
bashunit::assert::should_skip && return 0
bashunit::assert_json::require_jq || return 0

local expected="$1"
local actual="$2"

local expected_sorted
expected_sorted=$(printf '%s' "$expected" | jq -S '.' 2>/dev/null)
local actual_sorted
actual_sorted=$(printf '%s' "$actual" | jq -S '.' 2>/dev/null)

if [ "$expected_sorted" != "$actual_sorted" ]; then
local test_fn
test_fn="$(bashunit::helper::find_test_function_name)"
local label
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
bashunit::assert::mark_failed
bashunit::console_results::print_failed_test "${label}" "${expected}" "but got " "${actual}"
return
fi

bashunit::state::add_assertions_passed
}
1 change: 1 addition & 0 deletions src/assertions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "$BASHUNIT_ROOT_DIR/src/assert.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_arrays.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_files.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_folders.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_json.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_snapshot.sh"
source "$BASHUNIT_ROOT_DIR/src/skip_todo.sh"
source "$BASHUNIT_ROOT_DIR/src/test_doubles.sh"
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ Reports an error if `expected` and `actual` are not equals.
Reports an error if `expected` and `actual` are not equals.

- assert_files_equals is the inverse of this assertion and takes the same arguments.


## assert_json_equals
--------------
> `assert_json_equals "expected" "actual"`

Reports an error if the two JSON strings are not structurally equal. Key order is ignored. Requires `jq` to be installed; if missing the test is skipped.

77 changes: 77 additions & 0 deletions tests/unit/assert_json_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# shellcheck disable=SC2329

_JQ_AVAILABLE=false
if command -v jq >/dev/null 2>&1; then
_JQ_AVAILABLE=true
fi

function test_successful_assert_json_key_exists() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
assert_empty "$(assert_json_key_exists ".name" '{"name":"bashunit","version":"1.0"}')"
}

function test_successful_assert_json_key_exists_nested() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
assert_empty "$(assert_json_key_exists ".data.id" '{"data":{"id":42}}')"
}

function test_unsuccessful_assert_json_key_exists() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
local json='{"name":"bashunit"}'

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert json key exists" \
"$json" "to have key" ".missing")" \
"$(assert_json_key_exists ".missing" "$json")"
}

function test_successful_assert_json_contains() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
assert_empty "$(assert_json_contains ".name" "bashunit" '{"name":"bashunit","version":"1.0"}')"
}

function test_successful_assert_json_contains_numeric() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
assert_empty "$(assert_json_contains ".count" "42" '{"count":42}')"
}

function test_unsuccessful_assert_json_contains_wrong_value() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
local json='{"name":"bashunit"}'

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert json contains wrong value" \
"other" "but got " "bashunit")" \
"$(assert_json_contains ".name" "other" "$json")"
}

function test_unsuccessful_assert_json_contains_missing_key() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
local json='{"name":"bashunit"}'

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert json contains missing key" \
"$json" "to have key" ".missing")" \
"$(assert_json_contains ".missing" "value" "$json")"
}

function test_successful_assert_json_equals() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
assert_empty "$(assert_json_equals '{"b":2,"a":1}' '{"a":1,"b":2}')"
}

function test_unsuccessful_assert_json_equals() {
if [ "$_JQ_AVAILABLE" = false ]; then bashunit::skip "jq required"; return; fi
local expected='{"a":1}'
local actual='{"a":2}'

assert_same \
"$(bashunit::console_results::print_failed_test \
"Unsuccessful assert json equals" \
"$expected" "but got " "$actual")" \
"$(assert_json_equals "$expected" "$actual")"
}
Loading