🧪 Skills

Test Patterns

Write and run tests across languages and frameworks. Use when setting up test suites, writing unit/integration/E2E tests, measuring coverage, mocking dependencies, or debugging test failures. Covers N

v1.0.0
❤️ 1
⬇️ 2.6k
👁 2
Share

Description


name: test-patterns description: Write and run tests across languages and frameworks. Use when setting up test suites, writing unit/integration/E2E tests, measuring coverage, mocking dependencies, or debugging test failures. Covers Node.js (Jest/Vitest), Python (pytest), Go, Rust, and Bash. metadata: {"clawdbot":{"emoji":"🧪","requires":{"anyBins":["node","python3","go","cargo","bash"]},"os":["linux","darwin","win32"]}}

Test Patterns

Write, run, and debug tests across languages. Covers unit tests, integration tests, E2E tests, mocking, coverage, and TDD workflows.

When to Use

  • Setting up a test suite for a new project
  • Writing unit tests for functions or modules
  • Writing integration tests for APIs or database interactions
  • Setting up code coverage measurement
  • Mocking external dependencies (APIs, databases, file system)
  • Debugging flaky or failing tests
  • Implementing test-driven development (TDD)

Node.js (Jest / Vitest)

Setup

# Jest
npm install -D jest
# Add to package.json: "scripts": { "test": "jest" }

# Vitest (faster, ESM-native)
npm install -D vitest
# Add to package.json: "scripts": { "test": "vitest" }

Unit Tests

// math.js
export function add(a, b) { return a + b; }
export function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

// math.test.js
import { add, divide } from './math.js';

describe('add', () => {
  test('adds two positive numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  test('handles negative numbers', () => {
    expect(add(-1, 1)).toBe(0);
  });

  test('handles zero', () => {
    expect(add(0, 0)).toBe(0);
  });
});

describe('divide', () => {
  test('divides two numbers', () => {
    expect(divide(10, 2)).toBe(5);
  });

  test('throws on division by zero', () => {
    expect(() => divide(10, 0)).toThrow('Division by zero');
  });

  test('handles floating point', () => {
    expect(divide(1, 3)).toBeCloseTo(0.333, 3);
  });
});

Async Tests

// api.test.js
import { fetchUser } from './api.js';

test('fetches user by id', async () => {
  const user = await fetchUser('123');
  expect(user).toHaveProperty('id', '123');
  expect(user).toHaveProperty('name');
  expect(user.name).toBeTruthy();
});

test('throws on missing user', async () => {
  await expect(fetchUser('nonexistent')).rejects.toThrow('Not found');
});

Mocking

// Mock a module
jest.mock('./database.js');
import { getUser } from './database.js';
import { processUser } from './service.js';

test('processes user from database', async () => {
  // Setup mock return value
  getUser.mockResolvedValue({ id: '1', name: 'Alice', active: true });

  const result = await processUser('1');
  expect(result.processed).toBe(true);
  expect(getUser).toHaveBeenCalledWith('1');
  expect(getUser).toHaveBeenCalledTimes(1);
});

// Mock fetch
global.fetch = jest.fn();

test('calls API with correct params', async () => {
  fetch.mockResolvedValue({
    ok: true,
    json: async () => ({ data: 'test' }),
  });

  const result = await myApiCall('/endpoint');
  expect(fetch).toHaveBeenCalledWith('/endpoint', expect.objectContaining({
    method: 'GET',
  }));
});

// Spy on existing method (don't replace, just observe)
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
// ... run code ...
expect(consoleSpy).toHaveBeenCalledWith('expected message');
consoleSpy.mockRestore();

Coverage

# Jest
npx jest --coverage

# Vitest
npx vitest --coverage

# Check coverage thresholds (jest.config.js)
# coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } }

Python (pytest)

Setup

pip install pytest pytest-cov

Unit Tests

# calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Division by zero")
    return a / b

# test_calculator.py
import pytest
from calculator import add, divide

def test_add():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, 1) == 0

def test_divide():
    assert divide(10, 2) == 5.0

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Division by zero"):
        divide(10, 0)

def test_divide_float():
    assert divide(1, 3) == pytest.approx(0.333, abs=0.001)

Parametrized Tests

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
    (100, -50, 50),
])
def test_add_cases(a, b, expected):
    assert add(a, b) == expected

Fixtures

import pytest
import json
import tempfile
import os

@pytest.fixture
def sample_users():
    """Provide test user data."""
    return [
        {"id": 1, "name": "Alice", "email": "alice@test.com"},
        {"id": 2, "name": "Bob", "email": "bob@test.com"},
    ]

@pytest.fixture
def temp_db(tmp_path):
    """Provide a temporary SQLite database."""
    import sqlite3
    db_path = tmp_path / "test.db"
    conn = sqlite3.connect(str(db_path))
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
    conn.commit()
    yield conn
    conn.close()

def test_insert_users(temp_db, sample_users):
    for user in sample_users:
        temp_db.execute("INSERT INTO users VALUES (?, ?, ?)",
                       (user["id"], user["name"], user["email"]))
    temp_db.commit()
    count = temp_db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
    assert count == 2

# Fixture with cleanup
@pytest.fixture
def temp_config_file():
    path = tempfile.mktemp(suffix=".json")
    with open(path, "w") as f:
        json.dump({"key": "value"}, f)
    yield path
    os.unlink(path)

Mocking

from unittest.mock import patch, MagicMock, AsyncMock

# Mock a function
@patch('mymodule.requests.get')
def test_fetch_data(mock_get):
    mock_get.return_value.status_code = 200
    mock_get.return_value.json.return_value = {"data": "test"}

    result = fetch_data("https://api.example.com")
    assert result == {"data": "test"}
    mock_get.assert_called_once_with("https://api.example.com")

# Mock async
@patch('mymodule.aiohttp.ClientSession.get', new_callable=AsyncMock)
async def test_async_fetch(mock_get):
    mock_get.return_value.__aenter__.return_value.json = AsyncMock(return_value={"ok": True})
    result = await async_fetch("/endpoint")
    assert result["ok"] is True

# Context manager mock
def test_file_reader():
    with patch("builtins.open", MagicMock(return_value=MagicMock(
        read=MagicMock(return_value='{"key": "val"}'),
        __enter__=MagicMock(return_value=MagicMock(read=MagicMock(return_value='{"key": "val"}'))),
        __exit__=MagicMock(return_value=False),
    ))):
        result = read_config("fake.json")
        assert result["key"] == "val"

Coverage

# Run with coverage
pytest --cov=mypackage --cov-report=term-missing

# HTML report
pytest --cov=mypackage --cov-report=html
# Open htmlcov/index.html

# Fail if coverage below threshold
pytest --cov=mypackage --cov-fail-under=80

Go

Unit Tests

// math.go
package math

import "errors"

func Add(a, b int) int { return a + b }

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// math_test.go
package math

import (
    "testing"
    "math"
)

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, 1, 0},
        {"zeros", 0, 0, 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.expected {
                t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
            }
        })
    }
}

func TestDivide(t *testing.T) {
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if math.Abs(result-5.0) > 0.001 {
        t.Errorf("Divide(10, 2) = %f, want 5.0", result)
    }
}

func TestDivideByZero(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Error("expected error for division by zero")
    }
}

Run Tests

# All tests
go test ./...

# Verbose
go test -v ./...

# Specific package
go test ./pkg/math/

# With coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# Run specific test
go test -run TestAdd ./...

# Race condition detection
go test -race ./...

# Benchmark
go test -bench=. ./...

Rust

Unit Tests

// src/math.rs
pub fn add(a: i64, b: i64) -> i64 { a + b }

pub fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { return Err("division by zero".into()); }
    Ok(a / b)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    fn test_divide() {
        let result = divide(10.0, 2.0).unwrap();
        assert!((result - 5.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_divide_by_zero() {
        assert!(divide(10.0, 0.0).is_err());
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn test_overflow_panics() {
        let _ = add(i64::MAX, 1); // Will panic on overflow in debug
    }
}
cargo test
cargo test -- --nocapture  # Show println output
cargo test test_add        # Run specific test
cargo tarpaulin            # Coverage (install: cargo install cargo-tarpaulin)

Bash Tests

Simple Test Runner

#!/bin/bash
# test.sh - Minimal bash test framework
PASS=0 FAIL=0

assert_eq() {
  local actual="$1" expected="$2" label="$3"
  if [ "$actual" = "$expected" ]; then
    echo "  PASS: $label"
    ((PASS++))
  else
    echo "  FAIL: $label (got '$actual', expected '$expected')"
    ((FAIL++))
  fi
}

assert_exit_code() {
  local cmd="$1" expected="$2" label="$3"
  eval "$cmd" >/dev/null 2>&1
  assert_eq "$?" "$expected" "$label"
}

assert_contains() {
  local actual="$1" substring="$2" label="$3"
  if echo "$actual" | grep -q "$substring"; then
    echo "  PASS: $label"
    ((PASS++))
  else
    echo "  FAIL: $label ('$actual' does not contain '$substring')"
    ((FAIL++))
  fi
}

# --- Tests ---
echo "Running tests..."

# Test your scripts
output=$(./my-script.sh --help 2>&1)
assert_exit_code "./my-script.sh --help" "0" "help flag exits 0"
assert_contains "$output" "Usage" "help shows usage"

output=$(./my-script.sh --invalid 2>&1)
assert_exit_code "./my-script.sh --invalid" "1" "invalid flag exits 1"

# Test command outputs
assert_eq "$(echo 'hello' | wc -c | tr -d ' ')" "6" "echo hello is 6 bytes"

echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1

Integration Testing Patterns

API Integration Test (any language)

#!/bin/bash
# test-api.sh - Start server, run tests, tear down
SERVER_PID=""
cleanup() { [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null; }
trap cleanup EXIT

# Start server in background
npm start &
SERVER_PID=$!
sleep 2  # Wait for server

# Run tests against live server
BASE_URL=http://localhost:3000 npx jest --testPathPattern=integration
EXIT_CODE=$?

exit $EXIT_CODE

Database Integration Test (Python)

import pytest
import sqlite3

@pytest.fixture
def db():
    """Fresh database for each test."""
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, price REAL)")
    yield conn
    conn.close()

def test_insert_and_query(db):
    db.execute("INSERT INTO items (name, price) VALUES (?, ?)", ("Widget", 9.99))
    db.commit()
    row = db.execute("SELECT name, price FROM items WHERE name = ?", ("Widget",)).fetchone()
    assert row == ("Widget", 9.99)

def test_empty_table(db):
    count = db.execute("SELECT COUNT(*) FROM items").fetchone()[0]
    assert count == 0

TDD Workflow

The red-green-refactor cycle:

  1. Red: Write a failing test for the next piece of behavior
  2. Green: Write the minimum code to make it pass
  3. Refactor: Clean up without changing behavior (tests stay green)
# Tight feedback loop
# Jest watch mode
npx jest --watch

# Vitest watch (default)
npx vitest

# pytest watch (with pytest-watch)
pip install pytest-watch
ptw

# Go (with air or entr)
ls *.go | entr -c go test ./...

Debugging Failed Tests

Common Issues

Test passes alone, fails in suite → shared state. Check for:

  • Global variables modified between tests
  • Database not cleaned up
  • Mocks not restored (afterEach / teardown)

Test fails intermittently (flaky) → timing or ordering issue:

  • Async operations without proper await
  • Tests depending on execution order
  • Time-dependent logic (use clock mocking)
  • Network calls in unit tests (should be mocked)

Coverage shows uncovered branches → missing edge cases:

  • Error paths (what if the API returns 500?)
  • Empty inputs (empty string, null, empty array)
  • Boundary values (0, -1, MAX_INT)

Run Single Test

# Jest
npx jest -t "test name substring"

# pytest
pytest -k "test_divide_by_zero"

# Go
go test -run TestDivideByZero ./...

# Rust
cargo test test_divide

Tips

  • Test behavior, not implementation. Tests should survive refactors.
  • One assertion per concept (not necessarily one assert per test, but one logical check).
  • Name tests descriptively: test_returns_empty_list_when_no_users_exist beats test_get_users_2.
  • Don't mock what you don't own — write thin wrappers around external libraries, mock the wrapper.
  • Integration tests catch bugs unit tests miss. Don't skip them.
  • Use tmp_path (pytest), t.TempDir() (Go), or tempfile (Node) for file-based tests.
  • Snapshot tests are great for detecting unintended changes, bad for evolving formats.

Reviews (0)

Sign in to write a review.

No reviews yet. Be the first to review!

Comments (0)

Sign in to join the discussion.

No comments yet. Be the first to share your thoughts!

Compatible Platforms

Pricing

Free

Related Configs