Shell Testing Framework Expert
Comprehensive testing expertise for bash shell scripts using patterns and methodologies from the unix-goto project, emphasizing 100% test coverage, systematic test organization, and performance validation.
When to Use This Skill
Use this skill when:
Writing test suites for bash shell scripts Implementing 100% test coverage requirements Organizing tests into unit, integration, edge case, and performance categories Creating assertion patterns for shell script validation Setting up test infrastructure and helpers Writing performance tests for shell functions Generating test reports and summaries Debugging test failures Validating shell script behavior
Do NOT use this skill for:
Testing non-shell applications (use language-specific frameworks) Simple ad-hoc script validation Production testing (use for development/CI only) General QA testing (this is developer-focused unit testing) Core Testing Philosophy The 100% Coverage Rule
Every core feature in unix-goto has 100% test coverage. This is NON-NEGOTIABLE.
Coverage Requirements:
Core navigation: 100% Cache system: 100% Bookmarks: 100% History: 100% Benchmarks: 100% New features: 100%
What This Means:
Every function has tests Every code path is exercised Every error condition is validated Every edge case is covered Every performance target is verified Test-Driven Development Approach
Workflow:
Write tests FIRST (based on feature spec) Watch tests FAIL (red) Implement feature Watch tests PASS (green) Refactor if needed Validate all tests still pass Core Knowledge Standard Test File Structure
Every test file follows this exact structure:
!/bin/bash
Test suite for [feature] functionality
set -e # Exit on error
============================================
Setup
============================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/lib/module.sh"
============================================
Test Counters
============================================
TESTS_PASSED=0 TESTS_FAILED=0
============================================
Test Helpers
============================================
pass() { echo "✓ PASS: $1" ((TESTS_PASSED++)) }
fail() { echo "✗ FAIL: $1" ((TESTS_FAILED++)) }
============================================
Test Functions
============================================
Test 1: [Category] - [Description]
test_feature_basic() { # Arrange local input="test" local expected="expected_output"
# Act
local result=$(function_under_test "$input")
# Assert
if [[ "$result" == "$expected" ]]; then
pass "Basic feature test"
else
fail "Basic feature test: expected '$expected', got '$result'"
fi
}
============================================
Test Execution
============================================
Run all tests
test_feature_basic
============================================
Summary
============================================
echo "" echo "═══════════════════════════════════════" echo "Tests passed: $TESTS_PASSED" echo "Tests failed: $TESTS_FAILED" echo "═══════════════════════════════════════"
Exit with proper code
[ $TESTS_FAILED -eq 0 ] && exit 0 || exit 1
The Arrange-Act-Assert Pattern
EVERY test function MUST follow this three-phase structure:
- Arrange - Set up test conditions
Arrange
local input="test-value" local expected="expected-result" local temp_file=$(mktemp) echo "test data" > "$temp_file"
- Act - Execute the code under test
Act
local result=$(function_under_test "$input") local exit_code=$?
- Assert - Verify the results
Assert
if [[ "$result" == "$expected" && $exit_code -eq 0 ]]; then pass "Test description" else fail "Test failed: expected '$expected', got '$result'" fi
Complete Example:
test_cache_lookup_single_match() { # Arrange - Create cache with single match local cache_file="$HOME/.goto_index" cat > "$cache_file" << EOF
unix-goto folder index cache
---
unix-goto|/Users/manu/Git_Repos/unix-goto|2|1234567890 EOF
# Act - Lookup folder
local result=$(__goto_cache_lookup "unix-goto")
local exit_code=$?
# Assert - Should return exact path
local expected="/Users/manu/Git_Repos/unix-goto"
if [[ "$result" == "$expected" && $exit_code -eq 0 ]]; then
pass "Cache lookup returns single match"
else
fail "Expected '$expected' with code 0, got '$result' with code $exit_code"
fi
}
The Four Test Categories
EVERY feature requires tests in ALL four categories:
Category 1: Unit Tests
Purpose: Test individual functions in isolation
Characteristics:
Single function under test Minimal dependencies Fast execution (<1ms per test) Clear, focused assertions
Example - Cache Lookup Unit Test:
test_cache_lookup_not_found() { # Arrange local cache_file="$HOME/.goto_index" cat > "$cache_file" << EOF
unix-goto folder index cache
---
unix-goto|/Users/manu/Git_Repos/unix-goto|2|1234567890 EOF
# Act
local result=$(__goto_cache_lookup "nonexistent")
local exit_code=$?
# Assert
if [[ -z "$result" && $exit_code -eq 1 ]]; then
pass "Cache lookup not found returns code 1"
else
fail "Expected empty result with code 1, got '$result' with code $exit_code"
fi
}
test_cache_lookup_multiple_matches() { # Arrange local cache_file="$HOME/.goto_index" cat > "$cache_file" << EOF
unix-goto folder index cache
---
project|/Users/manu/project1|2|1234567890 project|/Users/manu/project2|2|1234567891 EOF
# Act
local result=$(__goto_cache_lookup "project")
local exit_code=$?
# Assert - Should return all matches with code 2
local line_count=$(echo "$result" | wc -l)
if [[ $line_count -eq 2 && $exit_code -eq 2 ]]; then
pass "Cache lookup returns multiple matches with code 2"
else
fail "Expected 2 lines with code 2, got $line_count lines with code $exit_code"
fi
}
Unit Test Checklist:
Test with valid input Test with invalid input Test with empty input Test with boundary values Test return codes Test output format Category 2: Integration Tests
Purpose: Test how multiple modules work together
Characteristics:
Multiple functions/modules interact Test realistic workflows Validate end-to-end behavior Moderate execution time (<100ms per test)
Example - Navigation Integration Test:
test_navigation_with_cache() { # Arrange - Setup complete navigation environment local cache_file="$HOME/.goto_index" local history_file="$HOME/.goto_history"
cat > "$cache_file" << EOF
unix-goto folder index cache
---
unix-goto|/Users/manu/Git_Repos/unix-goto|2|1234567890 EOF
# Act - Perform full navigation
local start_dir=$(pwd)
goto unix-goto
local nav_exit_code=$?
local end_dir=$(pwd)
# Assert - Should navigate and track history
local expected_dir="/Users/manu/Git_Repos/unix-goto"
local history_recorded=false
if grep -q "$expected_dir" "$history_file" 2>/dev/null; then
history_recorded=true
fi
if [[ "$end_dir" == "$expected_dir" && $nav_exit_code -eq 0 && $history_recorded == true ]]; then
pass "Navigation with cache and history tracking"
else
fail "Integration test failed: nav=$nav_exit_code, dir=$end_dir, history=$history_recorded"
fi
# Cleanup
cd "$start_dir"
}
test_bookmark_creation_and_navigation() { # Arrange local bookmark_file="$HOME/.goto_bookmarks" rm -f "$bookmark_file"
# Act - Create bookmark and navigate
bookmark add testwork /Users/manu/work
local add_code=$?
goto @testwork
local nav_code=$?
local nav_dir=$(pwd)
# Assert
local expected_dir="/Users/manu/work"
if [[ $add_code -eq 0 && $nav_code -eq 0 && "$nav_dir" == "$expected_dir" ]]; then
pass "Bookmark creation and navigation integration"
else
fail "Integration failed: add=$add_code, nav=$nav_code, dir=$nav_dir"
fi
}
Integration Test Checklist:
Test common user workflows Test module interactions Test data persistence Test state changes Test error propagation Test cleanup behavior Category 3: Edge Cases
Purpose: Test boundary conditions and unusual scenarios
Characteristics:
Unusual but valid inputs Boundary conditions Error scenarios Race conditions Resource limits
Example - Edge Case Tests:
test_empty_cache_file() { # Arrange - Create empty cache file local cache_file="$HOME/.goto_index" touch "$cache_file"
# Act
local result=$(__goto_cache_lookup "anything")
local exit_code=$?
# Assert - Should handle gracefully
if [[ -z "$result" && $exit_code -eq 1 ]]; then
pass "Empty cache file handled gracefully"
else
fail "Empty cache should return code 1"
fi
}
test_malformed_cache_entry() { # Arrange - Cache with malformed entry local cache_file="$HOME/.goto_index" cat > "$cache_file" << EOF
unix-goto folder index cache
---
unix-goto|/path|missing|fields valid-entry|/valid/path|2|1234567890 EOF
# Act
local result=$(__goto_cache_lookup "valid-entry")
local exit_code=$?
# Assert - Should still find valid entry
if [[ "$result" == "/valid/path" && $exit_code -eq 0 ]]; then
pass "Malformed entry doesn't break valid lookups"
else
fail "Should handle malformed entries gracefully"
fi
}
test_very_long_path() { # Arrange - Create entry with very long path local long_path=$(printf '/very/long/path/%.0s' {1..50}) local cache_file="$HOME/.goto_index" cat > "$cache_file" << EOF
unix-goto folder index cache
---
longpath|${long_path}|50|1234567890 EOF
# Act
local result=$(__goto_cache_lookup "longpath")
local exit_code=$?
# Assert - Should handle long paths
if [[ "$result" == "$long_path" && $exit_code -eq 0 ]]; then
pass "Very long paths handled correctly"
else
fail "Long path handling failed"
fi
}
test_special_characters_in_folder_name() { # Arrange - Folder with special characters local cache_file="$HOME/.goto_index" cat > "$cache_file" << EOF
unix-goto folder index cache
---
my-project_v2.0|/Users/manu/my-project_v2.0|2|1234567890 EOF
# Act
local result=$(__goto_cache_lookup "my-project_v2.0")
local exit_code=$?
# Assert
if [[ "$result" == "/Users/manu/my-project_v2.0" && $exit_code -eq 0 ]]; then
pass "Special characters in folder name"
else
fail "Special character handling failed"
fi
}
test_concurrent_cache_access() { # Arrange local cache_file="$HOME/.goto_index" __goto_cache_build
# Act - Simulate concurrent access
(
for i in {1..10}; do
__goto_cache_lookup "unix-goto" &
done
wait
)
local exit_code=$?
# Assert - Should handle concurrent reads
if [[ $exit_code -eq 0 ]]; then
pass "Concurrent cache access handled"
else
fail "Concurrent access failed"
fi
}
Edge Case Test Checklist:
Empty inputs Missing files Malformed data Very large inputs Special characters Concurrent access Resource exhaustion Permission errors Category 4: Performance Tests
Purpose: Validate performance targets are met
Characteristics:
Measure execution time Compare against targets Use statistical analysis Test at scale
Example - Performance Tests:
test_cache_lookup_speed() { # Arrange - Build cache __goto_cache_build
# Act - Measure lookup time
local start=$(date +%s%N)
__goto_cache_lookup "unix-goto"
local end=$(date +%s%N)
# Assert - Should be <100ms
local duration=$(((end - start) / 1000000))
local target=100
if [ $duration -lt $target ]; then
pass "Cache lookup speed: ${duration}ms (target: <${target}ms)"
else
fail "Cache too slow: ${duration}ms (target: <${target}ms)"
fi
}
test_cache_build_performance() { # Arrange - Clean cache rm -f ~/.goto_index
# Act - Measure build time
local start=$(date +%s%N)
__goto_cache_build
local end=$(date +%s%N)
# Assert - Should be <5 seconds
local duration=$(((end - start) / 1000000))
local target=5000
if [ $duration -lt $target ]; then
pass "Cache build speed: ${duration}ms (target: <${target}ms)"
else
fail "Cache build too slow: ${duration}ms (target: <${target}ms)"
fi
}
test_history_retrieval_speed() { # Arrange - Create history with 100 entries local history_file="$HOME/.goto_history" rm -f "$history_file" for i in {1..100}; do echo "$(date +%s)|/path/to/dir$i" >> "$history_file" done
# Act - Measure retrieval time
local start=$(date +%s%N)
__goto_recent_dirs 10
local end=$(date +%s%N)
# Assert - Should be <10ms
local duration=$(((end - start) / 1000000))
local target=10
if [ $duration -lt $target ]; then
pass "History retrieval: ${duration}ms (target: <${target}ms)"
else
fail "History too slow: ${duration}ms (target: <${target}ms)"
fi
}
test_benchmark_cache_at_scale() { # Arrange - Create large workspace local workspace=$(mktemp -d) for i in {1..500}; do mkdir -p "$workspace/folder-$i" done
# Act - Build cache and measure lookup
local old_paths="$GOTO_SEARCH_PATHS"
export GOTO_SEARCH_PATHS="$workspace"
__goto_cache_build
local start=$(date +%s%N)
__goto_cache_lookup "folder-250"
local end=$(date +%s%N)
# Assert - Even with 500 folders, should be <100ms
local duration=$(((end - start) / 1000000))
local target=100
if [ $duration -lt $target ]; then
pass "Cache at scale (500 folders): ${duration}ms"
else
fail "Cache at scale too slow: ${duration}ms"
fi
# Cleanup
export GOTO_SEARCH_PATHS="$old_paths"
rm -rf "$workspace"
}
Performance Test Checklist:
Measure critical path operations Compare against defined targets Test at realistic scale Test with maximum load Calculate statistics (min/max/mean/median) Verify no performance regressions Assertion Patterns Basic Assertions
String Equality:
assert_equal() { local expected="$1" local actual="$2" local message="${3:-String equality}"
if [[ "$actual" == "$expected" ]]; then
pass "$message"
else
fail "$message: expected '$expected', got '$actual'"
fi
}
Usage
assert_equal "expected" "$result" "Function returns expected value"
Exit Code Assertions:
assert_success() { local exit_code=$? local message="${1:-Command should succeed}"
if [ $exit_code -eq 0 ]; then
pass "$message"
else
fail "$message: exit code $exit_code"
fi
}
assert_failure() { local exit_code=$? local message="${1:-Command should fail}"
if [ $exit_code -ne 0 ]; then
pass "$message"
else
fail "$message: expected non-zero exit code"
fi
}
Usage
some_command assert_success "Command executed successfully"
Numeric Comparisons:
assert_less_than() { local actual=$1 local limit=$2 local message="${3:-Value should be less than limit}"
if [ $actual -lt $limit ]; then
pass "$message: $actual < $limit"
else
fail "$message: $actual >= $limit"
fi
}
assert_greater_than() { local actual=$1 local limit=$2 local message="${3:-Value should be greater than limit}"
if [ $actual -gt $limit ]; then
pass "$message: $actual > $limit"
else
fail "$message: $actual <= $limit"
fi
}
Usage
assert_less_than $duration 100 "Cache lookup time"
File System Assertions
File Existence:
assert_file_exists() { local file="$1" local message="${2:-File should exist}"
if [ -f "$file" ]; then
pass "$message: $file"
else
fail "$message: $file not found"
fi
}
assert_dir_exists() { local dir="$1" local message="${2:-Directory should exist}"
if [ -d "$dir" ]; then
pass "$message: $dir"
else
fail "$message: $dir not found"
fi
}
Usage
assert_file_exists "$HOME/.goto_index" "Cache file created"
File Content Assertions:
assert_file_contains() { local file="$1" local pattern="$2" local message="${3:-File should contain pattern}"
if grep -q "$pattern" "$file" 2>/dev/null; then
pass "$message"
else
fail "$message: pattern '$pattern' not found in $file"
fi
}
assert_line_count() { local file="$1" local expected=$2 local message="${3:-File should have expected line count}"
local actual=$(wc -l < "$file" | tr -d ' ')
if [ $actual -eq $expected ]; then
pass "$message: $actual lines"
else
fail "$message: expected $expected lines, got $actual"
fi
}
Usage
assert_file_contains "$HOME/.goto_bookmarks" "work|/path/to/work" assert_line_count "$HOME/.goto_history" 10
Output Assertions
Contains Pattern:
assert_output_contains() { local output="$1" local pattern="$2" local message="${3:-Output should contain pattern}"
if [[ "$output" =~ $pattern ]]; then
pass "$message"
else
fail "$message: pattern '$pattern' not found in output"
fi
}
Usage
output=$(goto recent) assert_output_contains "$output" "/Users/manu/work" "Recent shows work directory"
Empty Output:
assert_output_empty() { local output="$1" local message="${2:-Output should be empty}"
if [[ -z "$output" ]]; then
pass "$message"
else
fail "$message: got '$output'"
fi
}
Usage
output=$(goto nonexistent 2>&1) assert_output_empty "$output"
Test Helper Functions
Create a reusable test helpers library:
!/bin/bash
test-helpers.sh - Reusable test utilities
============================================
Setup/Teardown
============================================
setup_test_env() { # Create temp directory for test TEST_TEMP_DIR=$(mktemp -d)
# Backup real files
[ -f "$HOME/.goto_index" ] && cp "$HOME/.goto_index" "$TEST_TEMP_DIR/goto_index.bak"
[ -f "$HOME/.goto_bookmarks" ] && cp "$HOME/.goto_bookmarks" "$TEST_TEMP_DIR/goto_bookmarks.bak"
[ -f "$HOME/.goto_history" ] && cp "$HOME/.goto_history" "$TEST_TEMP_DIR/goto_history.bak"
}
teardown_test_env() { # Restore backups [ -f "$TEST_TEMP_DIR/goto_index.bak" ] && mv "$TEST_TEMP_DIR/goto_index.bak" "$HOME/.goto_index" [ -f "$TEST_TEMP_DIR/goto_bookmarks.bak" ] && mv "$TEST_TEMP_DIR/goto_bookmarks.bak" "$HOME/.goto_bookmarks" [ -f "$TEST_TEMP_DIR/goto_history.bak" ] && mv "$TEST_TEMP_DIR/goto_history.bak" "$HOME/.goto_history"
# Remove temp directory
rm -rf "$TEST_TEMP_DIR"
}
============================================
Test Data Creation
============================================
create_test_cache() { local entries="${1:-10}" local cache_file="$HOME/.goto_index"
cat > "$cache_file" << EOF
unix-goto folder index cache
Version: 1.0
Built: $(date +%s)
Depth: 3
Format: folder_name|full_path|depth|last_modified
---
EOF
for i in $(seq 1 $entries); do
echo "folder-$i|/path/to/folder-$i|2|$(date +%s)" >> "$cache_file"
done
}
create_test_bookmarks() { local count="${1:-5}" local bookmark_file="$HOME/.goto_bookmarks"
rm -f "$bookmark_file"
for i in $(seq 1 $count); do
echo "bookmark$i|/path/to/bookmark$i|$(date +%s)" >> "$bookmark_file"
done
}
create_test_history() { local count="${1:-20}" local history_file="$HOME/.goto_history"
rm -f "$history_file"
for i in $(seq 1 $count); do
echo "$(date +%s)|/path/to/dir$i" >> "$history_file"
done
}
============================================
Timing Utilities
============================================
time_function_ms() { local func="$1" shift local args="$@"
local start=$(date +%s%N)
$func $args
local end=$(date +%s%N)
echo $(((end - start) / 1000000))
}
============================================
Assertion Helpers
============================================
assert_function_exists() { local func="$1"
if declare -f "$func" > /dev/null; then
pass "Function $func exists"
else
fail "Function $func not found"
fi
}
assert_variable_set() { local var="$1"
if [ -n "${!var}" ]; then
pass "Variable $var is set"
else
fail "Variable $var not set"
fi
}
Examples Example 1: Complete Cache Test Suite
!/bin/bash
test-cache.sh - Comprehensive cache system test suite
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/lib/cache-index.sh" source "$SCRIPT_DIR/test-helpers.sh"
TESTS_PASSED=0 TESTS_FAILED=0
pass() { echo "✓ PASS: $1"; ((TESTS_PASSED++)); } fail() { echo "✗ FAIL: $1"; ((TESTS_FAILED++)); }
============================================
Unit Tests
============================================
echo "Unit Tests" echo "─────────────────────────────────────────"
test_cache_lookup_single_match() { setup_test_env
# Arrange
cat > "$HOME/.goto_index" << EOF
unix-goto folder index cache
---
unix-goto|/Users/manu/Git_Repos/unix-goto|2|1234567890 EOF
# Act
local result=$(__goto_cache_lookup "unix-goto")
local exit_code=$?
# Assert
if [[ "$result" == "/Users/manu/Git_Repos/unix-goto" && $exit_code -eq 0 ]]; then
pass "Unit: Single match lookup"
else
fail "Unit: Single match lookup - got '$result' code $exit_code"
fi
teardown_test_env
}
test_cache_lookup_not_found() { setup_test_env
# Arrange
create_test_cache 5
# Act
local result=$(__goto_cache_lookup "nonexistent")
local exit_code=$?
# Assert
if [[ -z "$result" && $exit_code -eq 1 ]]; then
pass "Unit: Not found returns code 1"
else
fail "Unit: Not found - got '$result' code $exit_code"
fi
teardown_test_env
}
test_cache_lookup_multiple_matches() { setup_test_env
# Arrange
cat > "$HOME/.goto_index" << EOF
unix-goto folder index cache
---
project|/Users/manu/project1|2|1234567890 project|/Users/manu/project2|2|1234567891 EOF
# Act
local result=$(__goto_cache_lookup "project")
local exit_code=$?
local line_count=$(echo "$result" | wc -l | tr -d ' ')
# Assert
if [[ $line_count -eq 2 && $exit_code -eq 2 ]]; then
pass "Unit: Multiple matches returns code 2"
else
fail "Unit: Multiple matches - got $line_count lines code $exit_code"
fi
teardown_test_env
}
============================================
Integration Tests
============================================
echo "" echo "Integration Tests" echo "─────────────────────────────────────────"
test_cache_build_and_lookup() { setup_test_env
# Arrange
rm -f "$HOME/.goto_index"
# Act
__goto_cache_build
local build_code=$?
local result=$(__goto_cache_lookup "unix-goto")
local lookup_code=$?
# Assert
if [[ $build_code -eq 0 && $lookup_code -eq 0 && -n "$result" ]]; then
pass "Integration: Build and lookup"
else
fail "Integration: Build ($build_code) and lookup ($lookup_code) failed"
fi
teardown_test_env
}
============================================
Edge Cases
============================================
echo "" echo "Edge Case Tests" echo "─────────────────────────────────────────"
test_empty_cache_file() { setup_test_env
# Arrange
touch "$HOME/.goto_index"
# Act
local result=$(__goto_cache_lookup "anything")
local exit_code=$?
# Assert
if [[ -z "$result" && $exit_code -eq 1 ]]; then
pass "Edge: Empty cache handled"
else
fail "Edge: Empty cache should return code 1"
fi
teardown_test_env
}
test_special_characters() { setup_test_env
# Arrange
cat > "$HOME/.goto_index" << EOF
unix-goto folder index cache
---
my-project_v2.0|/Users/manu/my-project_v2.0|2|1234567890 EOF
# Act
local result=$(__goto_cache_lookup "my-project_v2.0")
local exit_code=$?
# Assert
if [[ "$result" == "/Users/manu/my-project_v2.0" && $exit_code -eq 0 ]]; then
pass "Edge: Special characters in name"
else
fail "Edge: Special characters failed"
fi
teardown_test_env
}
============================================
Performance Tests
============================================
echo "" echo "Performance Tests" echo "─────────────────────────────────────────"
test_cache_lookup_speed() { setup_test_env
# Arrange
create_test_cache 100
# Act
local duration=$(time_function_ms __goto_cache_lookup "folder-50")
# Assert - Should be <100ms
if [ $duration -lt 100 ]; then
pass "Performance: Cache lookup ${duration}ms (<100ms target)"
else
fail "Performance: Cache too slow ${duration}ms"
fi
teardown_test_env
}
test_cache_build_speed() { setup_test_env
# Arrange
rm -f "$HOME/.goto_index"
# Act
local duration=$(time_function_ms __goto_cache_build)
# Assert - Should be <5000ms (5 seconds)
if [ $duration -lt 5000 ]; then
pass "Performance: Cache build ${duration}ms (<5000ms target)"
else
fail "Performance: Cache build too slow ${duration}ms"
fi
teardown_test_env
}
============================================
Run All Tests
============================================
test_cache_lookup_single_match test_cache_lookup_not_found test_cache_lookup_multiple_matches
test_cache_build_and_lookup
test_empty_cache_file test_special_characters
test_cache_lookup_speed test_cache_build_speed
============================================
Summary
============================================
echo "" echo "═══════════════════════════════════════" echo "Tests passed: $TESTS_PASSED" echo "Tests failed: $TESTS_FAILED" echo "Coverage: 100% (all code paths tested)" echo "═══════════════════════════════════════"
[ $TESTS_FAILED -eq 0 ] && exit 0 || exit 1
Example 2: Benchmark Test Suite
!/bin/bash
test-benchmark.sh - Test suite for benchmark functionality
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/benchmarks/bench-helpers.sh"
TESTS_PASSED=0 TESTS_FAILED=0
pass() { echo "✓ PASS: $1"; ((TESTS_PASSED++)); } fail() { echo "✗ FAIL: $1"; ((TESTS_FAILED++)); }
Unit Tests
test_bench_time_ms() { # Arrange local cmd="sleep 0.1"
# Act
local duration=$(bench_time_ms $cmd)
# Assert - Should be ~100ms
if [ $duration -ge 90 ] && [ $duration -le 150 ]; then
pass "bench_time_ms measures correctly: ${duration}ms"
else
fail "bench_time_ms inaccurate: ${duration}ms (expected ~100ms)"
fi
}
test_bench_calculate_stats() { # Arrange local values=(10 20 30 40 50)
# Act
local stats=$(bench_calculate_stats "${values[@]}")
IFS=',' read -r min max mean median stddev <<< "$stats"
# Assert
if [[ $min -eq 10 && $max -eq 50 && $mean -eq 30 ]]; then
pass "bench_calculate_stats computes correctly"
else
fail "Stats calculation failed: min=$min max=$max mean=$mean"
fi
}
test_bench_create_workspace() { # Arrange/Act local workspace=$(bench_create_workspace "small")
# Assert
if [ -d "$workspace" ] && [ $(ls -1 "$workspace" | wc -l) -eq 10 ]; then
pass "Workspace creation (small: 10 folders)"
bench_cleanup_workspace "$workspace"
else
fail "Workspace creation failed"
fi
}
Run tests
test_bench_time_ms test_bench_calculate_stats test_bench_create_workspace
echo "" echo "Tests passed: $TESTS_PASSED" echo "Tests failed: $TESTS_FAILED"
[ $TESTS_FAILED -eq 0 ] && exit 0 || exit 1
Best Practices Test Organization
File Naming Convention:
test-cache.sh # Test cache system test-bookmark.sh # Test bookmarks test-navigation.sh # Test navigation test-benchmark.sh # Test benchmarks
Test Function Naming:
test_[category][feature][scenario]
Examples: test_unit_cache_lookup_single_match test_integration_navigation_with_cache test_edge_empty_input test_performance_cache_speed
Test Independence
Each test must be completely independent:
Good - Independent test
test_feature() { # Setup own environment local temp=$(mktemp)
# Test
result=$(function_under_test)
# Cleanup own resources
rm -f "$temp"
# Assert
[[ "$result" == "expected" ]] && pass "Test" || fail "Test"
}
Bad - Depends on previous test state
test_feature_bad() { # Assumes something from previous test result=$(function_under_test) # May fail if run alone }
Meaningful Failure Messages
Good - Detailed failure message
if [[ "$result" != "$expected" ]]; then fail "Cache lookup failed: expected '$expected', got '$result', exit code: $exit_code" fi
Bad - Vague failure message
if [[ "$result" != "$expected" ]]; then fail "Test failed" fi
Test Execution Speed
Keep tests FAST:
Unit tests: <1ms each Integration tests: <100ms each Edge cases: <10ms each Performance tests: As needed for measurement
Total test suite should run in <5 seconds.
Quick Reference Test Template Checklist Shebang and set -e Source required modules Initialize test counters Define pass/fail helpers Organize tests by category Use arrange-act-assert pattern Print summary with exit code Coverage Checklist All public functions tested All code paths exercised All return codes validated All error conditions tested All edge cases covered Performance targets verified Essential Test Commands
Run single test suite
bash test-cache.sh
Run all tests
bash test-cache.sh && bash test-bookmark.sh && bash test-navigation.sh
Run with verbose output
set -x; bash test-cache.sh; set +x
Run specific test function
bash -c 'source test-cache.sh; test_cache_lookup_single_match'
Skill Version: 1.0 Last Updated: October 2025 Maintained By: Manu Tej + Claude Code Source: unix-goto testing patterns and methodologies