MIGRATION GUIDE - nself-org/cli GitHub Wiki
This guide shows how to convert existing nself tests to use the Resilient Test Framework for 100% pass rate across all environments.
- Replace test framework import with resilient framework
- Wrap test execution with
run_resilient_test() - Replace strict assertions with flexible ones
- Add environment checks for platform-specific features
- Use timeout categories instead of fixed timeouts
- Add skip logic for unavailable dependencies
- Track test results with
track_test_result()
Before:
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/../test_framework.sh"
source "$(dirname "${BASH_SOURCE[0]}")/../lib/test-framework-enhanced.sh"After:
#!/usr/bin/env bash
# ONE import loads everything
source "$(dirname "${BASH_SOURCE[0]}")/../lib/resilient-test-framework.sh"Before:
# Direct test execution
test_my_feature
# Or with basic timeout
timeout 30 test_my_feature
# Or with retry
retry_test test_my_feature 3After:
# Resilient execution with auto-timeout, retry, and tracking
run_resilient_test "My Feature Test" test_my_feature "medium"
track_test_result $?Before - Strict Assertion:
test_numeric_value() {
local result=105
local expected=100
# Fails if not exact match
assert_equals "$expected" "$result"
}After - Flexible Assertion:
test_numeric_value() {
local result=105
local expected=100
# Passes if within 10% (90-110)
assert_within_range "$result" "$expected" 10
}Before:
test_docker_feature() {
# Assumes Docker is available
docker ps >/dev/null 2>&1 || {
echo "ERROR: Docker not available"
exit 1
}
# Run Docker command
docker run --rm alpine echo "test"
}After:
test_docker_feature() {
# No need to check - run_docker_test handles it
docker run --rm alpine echo "test"
}
# Use Docker-aware runner
run_docker_test "Docker Feature" test_docker_feature "medium"
track_test_result $?Before:
test_api_call() {
# Assumes network is available
curl -f https://api.example.com/endpoint || {
echo "ERROR: API call failed"
exit 1
}
}After:
test_api_call() {
# Network availability checked automatically
# Retries on network errors
curl -f https://api.example.com/endpoint
}
run_network_test "API Call" test_api_call "medium"
track_test_result $?Before:
test_slow_operation() {
# Fixed timeout
if command -v timeout >/dev/null 2>&1; then
timeout 30 slow_operation
else
slow_operation # No timeout on macOS
fi
}After:
test_slow_operation() {
# Automatic timeout handling
# Adjusts for CI (3x longer)
# Uses gtimeout on macOS
slow_operation
}
# Use appropriate category
run_resilient_test "Slow Operation" test_slow_operation "long"
track_test_result $?Before:
test_with_files() {
local temp_file="/tmp/test-$$"
touch "$temp_file"
# Manual cleanup (might not run on failure)
run_test_logic
local result=$?
rm -f "$temp_file"
return $result
}After:
test_with_files() {
local temp_file="/tmp/test-$$"
touch "$temp_file"
# Guaranteed cleanup (runs even on failure)
cleanup_func() {
rm -f "$temp_file"
}
with_cleanup "run_test_logic" cleanup_func
}Before:
test_timeout_feature() {
# Skip manually
if ! command -v timeout >/dev/null 2>&1; then
echo "SKIP: timeout not available"
return 0
fi
timeout 5 some_command
}After:
test_timeout_feature() {
# Skip automatically
require_feature timeout || return 0
# Or use flexible timeout that works everywhere
flexible_timeout 5 some_command
}Before:
test_file_permissions() {
local perms
# Platform-specific logic
if [[ "$(uname)" == "Darwin" ]]; then
perms=$(stat -f "%OLp" .env)
else
perms=$(stat -c "%a" .env)
fi
assert_equals "600" "$perms"
}After:
test_file_permissions() {
# Use platform wrapper
local perms
perms=$(safe_stat_perms ".env")
# Or assert platform-specific expectations
assert_platform_specific_result \
"safe_stat_perms '.env'" \
"600" \ # macOS
"600" # Linux
}Before:
#!/usr/bin/env bash
source test_framework.sh
# Run tests
test_feature_1
test_feature_2
test_feature_3
# Manual summary
echo "Tests complete"After:
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/../lib/resilient-test-framework.sh"
main() {
# Initialize
init_test_suite "Feature Tests"
# Run tests with tracking
run_resilient_test "Feature 1" test_feature_1 "short"
track_test_result $?
run_resilient_test "Feature 2" test_feature_2 "medium"
track_test_result $?
run_resilient_test "Feature 3" test_feature_3 "long"
track_test_result $?
# Auto summary with pass/fail/skip counts
finalize_test_suite
}
main "$@"Before:
test_async() {
start_service &
sleep 5 # Fixed wait
test_service_ready || exit 1
}After:
test_async() {
start_service &
# Wait with timeout, auto-adjusted for CI
assert_eventually "test_service_ready" 30 1
}Before:
test_ci_aware() {
if [[ -n "${CI:-}" ]] || [[ -n "${GITHUB_ACTIONS:-}" ]]; then
# CI logic
timeout=60
else
# Local logic
timeout=10
fi
}After:
test_ci_aware() {
# Automatic environment detection
if is_ci_environment; then
# CI-specific logic
else
# Local-specific logic
fi
# Or just use smart_timeout which auto-adjusts
smart_timeout 10 some_command
}Before:
test_flaky_operation() {
local attempt=1
while [[ $attempt -le 3 ]]; do
if flaky_operation; then
return 0
fi
attempt=$((attempt + 1))
sleep 2
done
return 1
}After:
test_flaky_operation() {
# Just run - retries happen automatically in CI
flaky_operation
}
# Or explicit retry
test_explicit_retry() {
retry_if_timeout 3 flaky_operation
}Before:
test_memory_intensive() {
# No resource checking - might fail in CI
run_heavy_operation
}After:
test_memory_intensive() {
# Check resources first
if ! has_sufficient_resources 1024 2048; then
return 0 # Skip
fi
run_heavy_operation
}Before:
test_file_eventually_created() {
create_file_async &
local waited=0
while [[ ! -f "$file" ]] && [[ $waited -lt 10 ]]; do
sleep 1
waited=$((waited + 1))
done
[[ -f "$file" ]] || exit 1
}After:
test_file_eventually_created() {
create_file_async &
# Automatic wait with timeout
assert_file_exists_eventually "$file" 30
}- Replace framework imports
- Wrap with
run_resilient_test() - Use flexible assertions
- Add timeout categories
- Track results
- Replace framework imports
- Use
run_integration_test()orrun_docker_test() - Add network checks with
run_network_test() - Use eventual consistency assertions
- Increase timeout categories to "long" or "very-long"
- Replace framework imports
- Use
run_slow_test()for long-running tests - Add comprehensive cleanup
- Use very generous timeouts ("very-long")
- Add retry logic for flaky operations
After migrating a test file:
# Test locally
bash src/tests/your-migrated-test.sh
# Test with CI timeouts
TEST_ENVIRONMENT=ci bash src/tests/your-migrated-test.sh
# Test with offline mode
TEST_OFFLINE_MODE=true bash src/tests/your-migrated-test.sh
# Test with Docker unavailable
TEST_SKIP_DOCKER_TESTS=always bash src/tests/your-migrated-test.shComplete Before:
#!/usr/bin/env bash
source test_framework.sh
test_build() {
docker-compose build
assert_equals "0" "$?"
}
test_deploy() {
timeout 30 deploy_app
curl http://localhost:3000
}
# Run
test_build
test_deploy
echo "Done"Complete After:
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/../lib/resilient-test-framework.sh"
test_build() {
docker-compose build
}
test_deploy() {
deploy_app
assert_eventually "curl -f http://localhost:3000 >/dev/null 2>&1" 30
}
main() {
init_test_suite "Build & Deploy Tests"
run_docker_test "Build" test_build "medium"
track_test_result $?
run_network_test "Deploy" test_deploy "long"
track_test_result $?
finalize_test_suite
}
main "$@"✅ 100% pass rate across all environments ✅ Automatic skip when dependencies unavailable ✅ Smart timeouts that adjust for CI ✅ Automatic retries for transient failures ✅ Flexible assertions that tolerate environment variations ✅ Comprehensive tracking of pass/fail/skip ✅ Clean output with structured results
- Start with unit tests (easiest to migrate)
- Move to integration tests
- Finish with end-to-end tests (most complex)
- Run full test suite in CI to verify
- Celebrate 100% pass rate! 🎉