Bash Scripting for QA
Why QA Engineers Write Scripts
Bash scripts automate repetitive QA tasks: health-checking environments, setting up test data, running smoke tests, analyzing logs, and managing test infrastructure. A well-written script saves hours of manual work and reduces human error.
Bash Fundamentals
Script Structure
#!/bin/bash
# The shebang line (first line) tells the system to use bash
# Exit immediately if a command fails
set -e
# Treat unset variables as an error
set -u
# Fail on pipe errors (not just the last command in a pipe)
set -o pipefail
# Your script logic goes here
echo "Script started at $(date)"
Make scripts executable: chmod +x my-script.sh
Run scripts: ./my-script.sh or bash my-script.sh
Variables
# Assignment (no spaces around =)
NAME="QA Engineer"
PORT=3000
BASE_URL="https://staging.example.com"
# Using variables
echo "Hello, $NAME"
echo "Server is at ${BASE_URL}:${PORT}"
# Default values (use default if variable is not set)
TIMEOUT=${TEST_TIMEOUT:-30000}
BROWSER=${TEST_BROWSER:-chromium}
# Command substitution (capture command output in a variable)
CURRENT_DATE=$(date +%Y-%m-%d)
GIT_HASH=$(git rev-parse --short HEAD)
Arrays
# Declare an array
ENVIRONMENTS=("dev" "staging" "preprod")
BROWSERS=("chromium" "firefox" "webkit")
# Access elements
echo "${ENVIRONMENTS[0]}" # First element: "dev"
echo "${ENVIRONMENTS[@]}" # All elements
echo "${#ENVIRONMENTS[@]}" # Count: 3
# Loop through an array
for env in "${ENVIRONMENTS[@]}"; do
echo "Testing $env..."
done
Control Flow
Conditionals
# If/else
if [ "$STATUS" -eq 200 ]; then
echo "PASS: Service is healthy"
elif [ "$STATUS" -eq 503 ]; then
echo "WARNING: Service unavailable"
else
echo "FAIL: Unexpected status $STATUS"
fi
# String comparison
if [ "$ENVIRONMENT" = "production" ]; then
echo "Running production smoke tests"
fi
# File checks
if [ -f "test-config.json" ]; then
echo "Config file exists"
fi
if [ -d "test-results" ]; then
echo "Results directory exists"
fi
# Logical operators
if [ "$STATUS" -eq 200 ] && [ "$RESPONSE_TIME" -lt 5 ]; then
echo "Fast and healthy"
fi
Common Test Operators
| Operator | Meaning | Example |
|---|---|---|
-eq |
Equal (numeric) | [ "$a" -eq 200 ] |
-ne |
Not equal (numeric) | [ "$a" -ne 0 ] |
-lt |
Less than | [ "$a" -lt 100 ] |
-gt |
Greater than | [ "$a" -gt 0 ] |
= |
Equal (string) | [ "$a" = "hello" ] |
!= |
Not equal (string) | [ "$a" != "" ] |
-f |
File exists | [ -f "config.json" ] |
-d |
Directory exists | [ -d "results/" ] |
-z |
String is empty | [ -z "$VAR" ] |
-n |
String is not empty | [ -n "$VAR" ] |
Loops
# For loop with a list
for browser in chromium firefox webkit; do
echo "Running tests in $browser"
npx playwright test --project="$browser"
done
# For loop with a range
for i in $(seq 1 5); do
echo "Test run $i of 5"
done
# While loop
ATTEMPTS=0
MAX_ATTEMPTS=10
while [ "$ATTEMPTS" -lt "$MAX_ATTEMPTS" ]; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/health")
if [ "$STATUS" -eq 200 ]; then
echo "Service is up after $ATTEMPTS attempts"
break
fi
ATTEMPTS=$((ATTEMPTS + 1))
sleep 5
done
Functions
# Define a function
check_health() {
local env=$1
local url="https://${env}.example.com/health"
local status
status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "$url")
if [ "$status" -eq 200 ]; then
echo "PASS: $env ($status)"
return 0
else
echo "FAIL: $env ($status)"
return 1
fi
}
# Call the function
check_health "staging"
check_health "production"
Practical Script: Smoke Test Multiple Environments
#!/bin/bash
# run-smoke-tests.sh -- Health-check multiple environments
set -euo pipefail
ENVIRONMENTS=("dev" "staging" "preprod")
FAIL_COUNT=0
RESULTS=()
check_health() {
local env=$1
local url="https://${env}.example.com/health"
local status
local time
status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 "$url" 2>/dev/null || echo "000")
time=$(curl -s -o /dev/null -w "%{time_total}" --max-time 30 "$url" 2>/dev/null || echo "0")
if [ "$status" -eq 200 ]; then
RESULTS+=("PASS: $env (HTTP $status, ${time}s)")
return 0
else
RESULTS+=("FAIL: $env (HTTP $status, ${time}s)")
return 1
fi
}
echo "=== Environment Health Check ==="
echo "Date: $(date)"
echo "================================"
for env in "${ENVIRONMENTS[@]}"; do
check_health "$env" || FAIL_COUNT=$((FAIL_COUNT + 1))
done
echo ""
for result in "${RESULTS[@]}"; do
echo " $result"
done
echo ""
echo "Results: $((${#ENVIRONMENTS[@]} - FAIL_COUNT))/${#ENVIRONMENTS[@]} passed"
if [ "$FAIL_COUNT" -gt 0 ]; then
echo "STATUS: SOME ENVIRONMENTS DOWN"
exit 1
else
echo "STATUS: ALL ENVIRONMENTS HEALTHY"
exit 0
fi
Practical Script: Wait for Service Readiness
#!/bin/bash
# wait-for-service.sh -- Wait until a service is ready before running tests
set -euo pipefail
URL="${1:?Usage: $0 <url> [timeout_seconds]}"
TIMEOUT="${2:-120}"
INTERVAL=5
ELAPSED=0
echo "Waiting for $URL to be ready (timeout: ${TIMEOUT}s)..."
while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$URL" 2>/dev/null || echo "000")
if [ "$STATUS" -eq 200 ]; then
echo "Service is ready (HTTP $STATUS) after ${ELAPSED}s"
exit 0
fi
echo " Not ready (HTTP $STATUS), retrying in ${INTERVAL}s... (${ELAPSED}/${TIMEOUT}s)"
sleep "$INTERVAL"
ELAPSED=$((ELAPSED + INTERVAL))
done
echo "ERROR: Service not ready after ${TIMEOUT}s"
exit 1
Usage: ./wait-for-service.sh https://staging.example.com/health 180
Practical Script: Test Data Cleanup
#!/bin/bash
# cleanup-test-data.sh -- Remove test data older than N days
set -euo pipefail
DAYS="${1:-7}"
DIRS=("test-results" "screenshots" "videos" "traces" "coverage")
echo "Cleaning up test artifacts older than $DAYS days..."
for dir in "${DIRS[@]}"; do
if [ -d "$dir" ]; then
COUNT=$(find "$dir" -type f -mtime +"$DAYS" | wc -l)
if [ "$COUNT" -gt 0 ]; then
echo " Removing $COUNT files from $dir/"
find "$dir" -type f -mtime +"$DAYS" -delete
else
echo " $dir/ -- nothing to clean"
fi
fi
done
echo "Cleanup complete"
Script Error Handling
# Trap errors and clean up
cleanup() {
echo "Cleaning up..."
docker compose down -v 2>/dev/null || true
rm -f /tmp/test-data-*.json
}
# Call cleanup on exit (success or failure)
trap cleanup EXIT
# Call cleanup on specific signals
trap cleanup SIGINT SIGTERM
Script Best Practices
| Practice | Why |
|---|---|
Use set -euo pipefail |
Catch errors early instead of silently continuing |
Use local in functions |
Prevent variable leaks between functions |
| Quote all variables | Prevent word splitting: "$VAR" not $VAR |
Use shellcheck |
Static analysis tool that catches common bash mistakes |
| Add usage messages | echo "Usage: $0 <url> [timeout]" |
| Use meaningful exit codes | 0 = success, 1 = failure, 2 = usage error |
| Add comments for non-obvious logic | Future you will thank present you |
Hands-On Exercise
- Write a smoke test script that checks 3 URLs and reports pass/fail
- Write a wait-for-service script and use it before running your test suite
- Write a cleanup script that removes test artifacts older than 7 days
- Install
shellcheckand run it on your scripts. Fix any warnings. - Add error handling (trap) to one of your scripts
- Write a script that takes environment name as an argument and runs the appropriate test suite