home / skills / manutej / luxor-claude-marketplace / shell-testing-framework
/plugins/luxor-testing-essentials/skills/shell-testing-framework
This skill enables thorough bash shell script testing with 100% coverage, structured categories, and performance validation to improve reliability.
npx playbooks add skill manutej/luxor-claude-marketplace --skill shell-testing-frameworkReview the files below or copy the command above to add this skill to your agents.
---
name: shell-testing-framework
description: Shell script testing expertise using bash test framework patterns from unix-goto, covering test structure (arrange-act-assert), 4 test categories, assertion patterns, 100% coverage requirements, and performance testing
---
# 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:**
1. Write tests FIRST (based on feature spec)
2. Watch tests FAIL (red)
3. Implement feature
4. Watch tests PASS (green)
5. Refactor if needed
6. Validate all tests still pass
## Core Knowledge
### Standard Test File Structure
Every test file follows this exact structure:
```bash
#!/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:
**1. Arrange** - Set up test conditions
```bash
# Arrange
local input="test-value"
local expected="expected-result"
local temp_file=$(mktemp)
echo "test data" > "$temp_file"
```
**2. Act** - Execute the code under test
```bash
# Act
local result=$(function_under_test "$input")
local exit_code=$?
```
**3. Assert** - Verify the results
```bash
# Assert
if [[ "$result" == "$expected" && $exit_code -eq 0 ]]; then
pass "Test description"
else
fail "Test failed: expected '$expected', got '$result'"
fi
```
**Complete Example:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:**
```bash
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:
```bash
#!/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
```bash
#!/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
```bash
#!/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:
```bash
# 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
```bash
# 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
```bash
# 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
This skill provides expert guidance for building and maintaining bash test suites using unix-goto patterns. It enforces a strict arrange-act-assert structure, four required test categories, and a mandate of 100% coverage for core features. It also includes patterns for assertions, test helpers, and performance measurement.
The skill describes a standard test file layout, helper functions for pass/fail counting, and test execution with clear exit codes. It explains how to write unit, integration, edge case, and performance tests, using the arrange-act-assert pattern for every test function. It also details coverage rules, performance targets, and practical examples for cache, history, bookmarks, and navigation behaviors.
How do I ensure tests are deterministic when they touch the filesystem?
Use temporary files or directories (mktemp), set explicit fixtures, and always clean up or restore state at the end of each test to avoid cross-test contamination.
What counts toward the 100% coverage requirement?
Every exported function and core feature path must have tests: success paths, error conditions, edge cases, and performance targets. New features must include tests before merge.