unit-testing

Unit testing standards following TDD methodology, test pyramid principles, and comprehensive coverage practices. Covers pytest, Jest, mocking, fixtures, and CI integration for reliable test suites.

$ Installer

git clone https://github.com/williamzujkowski/standards /tmp/standards && cp -r /tmp/standards/skills/testing/unit-testing ~/.claude/skills/standards

// tip: Run this command in your terminal to install the skill


name: unit-testing description: Unit testing standards following TDD methodology, test pyramid principles, and comprehensive coverage practices. Covers pytest, Jest, mocking, fixtures, and CI integration for reliable test suites.

Unit Testing Standards

Quick Navigation: Level 1: Quick Start (5 min) → Level 2: Implementation (30 min) → Level 3: Mastery (Extended)


Level 1: Quick Start (<2,000 tokens, 5 minutes)

Core Principles

  1. Test-Driven Development (TDD): Write tests before implementation (Red-Green-Refactor)
  2. Test Pyramid: 70% unit tests, 20% integration, 10% E2E
  3. Fast and Isolated: Tests run in milliseconds, no external dependencies
  4. Comprehensive Coverage: Aim for 80-90% code coverage minimum
  5. Clear and Maintainable: Tests serve as living documentation

Essential Checklist

  • TDD workflow: Write failing test → Implement → Refactor
  • Coverage targets: 80%+ overall, 95%+ for critical paths
  • Naming convention: test_<function>_<scenario>_<expected_result>
  • AAA pattern: Arrange, Act, Assert structure in every test
  • Mocking: Mock external dependencies (database, APIs, filesystem)
  • Fixtures: Reusable test data and setup/teardown
  • Parametrized tests: Test multiple inputs efficiently
  • CI integration: Tests run automatically on every commit

Quick Example

# pytest unit testing example
import pytest
from datetime import datetime

def calculate_discount(user_age: int, purchase_amount: float) -> float:
    """Calculate discount based on age and purchase amount."""
    if user_age < 18:
        return 0.0
    elif user_age >= 65:
        return purchase_amount * 0.15
    elif purchase_amount >= 100:
        return purchase_amount * 0.10
    return 0.0

# Unit tests following TDD
def test_calculate_discount_no_discount_for_minors():
    """Test that users under 18 receive no discount."""
    # Arrange
    user_age = 16
    purchase_amount = 100.0

    # Act
    discount = calculate_discount(user_age, purchase_amount)

    # Assert
    assert discount == 0.0

def test_calculate_discount_senior_discount():
    """Test that seniors (65+) receive 15% discount."""
    assert calculate_discount(65, 100.0) == 15.0
    assert calculate_discount(70, 200.0) == 30.0

def test_calculate_discount_large_purchase():
    """Test that purchases >= $100 receive 10% discount."""
    assert calculate_discount(30, 100.0) == 10.0
    assert calculate_discount(30, 150.0) == 15.0

@pytest.mark.parametrize("age,amount,expected", [
    (16, 100, 0.0),   # Minor
    (30, 50, 0.0),    # No discount
    (30, 100, 10.0),  # Large purchase
    (65, 50, 7.5),    # Senior
    (70, 200, 30.0),  # Senior large purchase
])
def test_calculate_discount_parametrized(age, amount, expected):
    """Test multiple discount scenarios."""
    assert calculate_discount(age, amount) == expected

Quick Links to Level 2


Level 2: Implementation (<5,000 tokens, 30 minutes)

TDD Workflow

Red-Green-Refactor Cycle (see resources/tdd-workflow.md)

# Step 1: RED - Write failing test
def test_user_authentication_success():
    """Test successful user authentication."""
    auth_service = AuthenticationService()
    result = auth_service.authenticate('user@example.com', 'password123')
    assert result.success is True
    assert result.token is not None

# Step 2: GREEN - Write minimum code to pass
class AuthenticationService:
    def authenticate(self, email: str, password: str):
        # Minimal implementation
        return AuthResult(success=True, token='dummy_token')

# Step 3: REFACTOR - Improve implementation
class AuthenticationService:
    def __init__(self, user_repository, token_generator):
        self.user_repo = user_repository
        self.token_gen = token_generator

    def authenticate(self, email: str, password: str):
        user = self.user_repo.find_by_email(email)
        if not user or not user.verify_password(password):
            return AuthResult(success=False, error='Invalid credentials')

        token = self.token_gen.generate(user.id)
        return AuthResult(success=True, token=token)

Test Organization

Test Structure (see templates/test-template-pytest.py)

# tests/test_user_service.py
import pytest
from unittest.mock import Mock, MagicMock
from app.services.user_service import UserService
from app.models.user import User

@pytest.fixture
def mock_database():
    """Create mock database connection."""
    db = Mock()
    db.query = MagicMock(return_value=[])
    return db

@pytest.fixture
def user_service(mock_database):
    """Create UserService with mocked dependencies."""
    return UserService(database=mock_database)

class TestUserService:
    """Test suite for UserService."""

    def test_create_user_success(self, user_service, mock_database):
        """Test successful user creation."""
        # Arrange
        user_data = {'email': 'test@example.com', 'name': 'Test User'}
        mock_database.insert = MagicMock(return_value=1)

        # Act
        user_id = user_service.create_user(user_data)

        # Assert
        assert user_id == 1
        mock_database.insert.assert_called_once()

    def test_create_user_duplicate_email(self, user_service, mock_database):
        """Test user creation fails with duplicate email."""
        # Arrange
        user_data = {'email': 'existing@example.com', 'name': 'Test'}
        mock_database.find_by_email = MagicMock(return_value=User(id=1))

        # Act & Assert
        with pytest.raises(DuplicateEmailError):
            user_service.create_user(user_data)

Mocking and Fixtures

Advanced Mocking (see templates/test-mocking-examples.py)

from unittest.mock import Mock, patch, MagicMock
import pytest

# Mock external API calls
@patch('requests.get')
def test_fetch_user_data(mock_get):
    """Test fetching user data from external API."""
    # Arrange
    mock_response = Mock()
    mock_response.json.return_value = {'id': 1, 'name': 'John'}
    mock_response.status_code = 200
    mock_get.return_value = mock_response

    # Act
    service = ExternalAPIService()
    user_data = service.fetch_user(1)

    # Assert
    assert user_data['name'] == 'John'
    mock_get.assert_called_once_with('https://api.example.com/users/1')

# Mock database operations
@pytest.fixture
def mock_db_session():
    """Create mock database session."""
    session = MagicMock()
    session.query = MagicMock()
    session.add = MagicMock()
    session.commit = MagicMock()
    return session

def test_save_user(mock_db_session):
    """Test saving user to database."""
    repository = UserRepository(session=mock_db_session)
    user = User(name='Test', email='test@example.com')

    repository.save(user)

    mock_db_session.add.assert_called_once_with(user)
    mock_db_session.commit.assert_called_once()

Coverage Analysis

Coverage Configuration (see resources/configs/pytest.ini)

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
    --verbose
    --cov=src
    --cov-report=term-missing
    --cov-report=html
    --cov-report=xml
    --cov-fail-under=80

[coverage:run]
source = src
omit =
    */tests/*
    */venv/*
    */__pycache__/*
    */site-packages/*

[coverage:report]
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError
    if __name__ == .__main__.:
    if TYPE_CHECKING:

Running Coverage

# Run tests with coverage
pytest --cov=src tests/

# Generate HTML report
pytest --cov=src --cov-report=html tests/
open htmlcov/index.html

# Coverage for specific module
pytest --cov=src.services.user_service tests/test_user_service.py

Best Practices

Test Naming and Organization

# ✅ Good: Descriptive test names
def test_calculate_total_with_discount_applies_10_percent_for_loyalty_members():
    """Test that loyalty members receive 10% discount on total."""
    pass

# ❌ Bad: Vague test names
def test_calculate():
    pass

# ✅ Good: Group related tests
class TestUserAuthentication:
    def test_successful_login(self):
        pass

    def test_failed_login_invalid_password(self):
        pass

    def test_failed_login_nonexistent_user(self):
        pass

# ✅ Good: Test one thing
def test_user_creation_generates_unique_id():
    user = create_user('test@example.com')
    assert isinstance(user.id, str)
    assert len(user.id) == 36  # UUID length

# ❌ Bad: Testing multiple things
def test_user_creation():
    user = create_user('test@example.com')
    assert user.id is not None
    assert user.email == 'test@example.com'
    assert user.created_at is not None
    assert user.is_active is True  # Too many assertions

Parameterized Testing

@pytest.mark.parametrize("input_value,expected_output", [
    (0, 0),
    (1, 1),
    (2, 4),
    (3, 9),
    (10, 100),
])
def test_square_function(input_value, expected_output):
    """Test square function with multiple inputs."""
    assert square(input_value) == expected_output

@pytest.mark.parametrize("email", [
    "invalid.email",
    "@example.com",
    "user@",
    "user @example.com",
    "",
])
def test_email_validation_rejects_invalid(email):
    """Test email validation rejects invalid formats."""
    with pytest.raises(ValidationError):
        validate_email(email)

JavaScript/Jest Testing

Jest Configuration (see resources/configs/jest.config.js)

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.{js,jsx}',
    '!src/**/*.test.{js,jsx}',
    '!src/index.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js']
};

Jest Testing Example (see templates/test-template-jest.js)

// src/calculator.test.js
const { add, subtract, divide } = require('./calculator');

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

    test('should add negative numbers', () => {
      expect(add(-2, -3)).toBe(-5);
    });
  });

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

    test('should throw error when dividing by zero', () => {
      expect(() => divide(10, 0)).toThrow('Division by zero');
    });
  });
});

// Mock testing
jest.mock('./apiService');
const apiService = require('./apiService');

test('fetches user data successfully', async () => {
  const mockUser = { id: 1, name: 'John' };
  apiService.getUser.mockResolvedValue(mockUser);

  const user = await fetchUser(1);

  expect(user).toEqual(mockUser);
  expect(apiService.getUser).toHaveBeenCalledWith(1);
});

Go Testing

Go Test Template (see templates/test-template-go.go)

// calculator_test.go
package calculator

import (
    "testing"
)

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -2, -3, -5},
        {"zero", 0, 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

func TestDivide(t *testing.T) {
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result != 5 {
        t.Errorf("got %d, want 5", result)
    }
}

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

Level 3: Mastery Resources

Advanced Topics

Templates & Examples

Configuration Files

Related Skills


Quick Reference Commands

# pytest
pytest                          # Run all tests
pytest tests/test_user.py       # Run specific file
pytest -k "test_auth"           # Run tests matching pattern
pytest --cov=src --cov-report=html  # Coverage report
pytest -v -s                    # Verbose with stdout
pytest --tb=short               # Short traceback

# Jest
npm test                        # Run all tests
npm test -- --coverage          # With coverage
npm test -- --watch             # Watch mode
npm test -- user.test.js        # Specific file

# Go
go test ./...                   # All packages
go test -v                      # Verbose
go test -cover                  # Coverage
go test -bench=.                # Benchmarks

Examples

Basic Usage

// TODO: Add basic example for unit-testing
// This example demonstrates core functionality

Advanced Usage

// TODO: Add advanced example for unit-testing
// This example shows production-ready patterns

Integration Example

// TODO: Add integration example showing how unit-testing
// works with other systems and services

See examples/unit-testing/ for complete working examples.

Integration Points

This skill integrates with:

Upstream Dependencies

  • Tools: pytest, Jest, Go test, unittest
  • Prerequisites: Basic understanding of testing concepts

Downstream Consumers

  • Applications: Production systems requiring unit-testing functionality
  • CI/CD Pipelines: Automated testing and deployment workflows
  • Monitoring Systems: Observability and logging platforms

Related Skills

Common Integration Patterns

  1. Development Workflow: How this skill fits into daily development
  2. Production Deployment: Integration with production systems
  3. Monitoring & Alerting: Observability integration points

Common Pitfalls

Pitfall 1: Insufficient Testing

Problem: Not testing edge cases and error conditions leads to production bugs

Solution: Implement comprehensive test coverage including:

  • Happy path scenarios
  • Error handling and edge cases
  • Integration points with external systems

Prevention: Enforce minimum code coverage (80%+) in CI/CD pipeline

Pitfall 2: Hardcoded Configuration

Problem: Hardcoding values makes applications inflexible and environment-dependent

Solution: Use environment variables and configuration management:

  • Separate config from code
  • Use environment-specific configuration files
  • Never commit secrets to version control

Prevention: Use tools like dotenv, config validators, and secret scanners

Pitfall 3: Ignoring Security Best Practices

Problem: Security vulnerabilities from not following established security patterns

Solution: Follow security guidelines:

  • Input validation and sanitization
  • Proper authentication and authorization
  • Encrypted data transmission (TLS/SSL)
  • Regular security audits and updates

Prevention: Use security linters, SAST tools, and regular dependency updates

Best Practices:

  • Follow established patterns and conventions for unit-testing
  • Keep dependencies up to date and scan for vulnerabilities
  • Write comprehensive documentation and inline comments
  • Use linting and formatting tools consistently
  • Implement proper error handling and logging
  • Regular code reviews and pair programming
  • Monitor production metrics and set up alerts

Validation

  • ✅ Token count: Level 1 <2,000, Level 2 <5,000
  • ✅ TDD workflow: Complete Red-Green-Refactor cycle
  • ✅ Coverage: 80-90% minimum standards
  • ✅ Code examples: Python, JavaScript, Go