Get the FREE Ultimate OpenClaw Setup Guide →

testing-obsessive

npx machina-cli add skill JasonWarrenUK/claude-code-config/testing-obsessive --openclaw
Files (1)
SKILL.md
21.9 KB

Testing Foundations

Comprehensive testing guidance for JavaScript/TypeScript applications, with emphasis on Vitest, Svelte component testing, and pragmatic test-after development. Addresses testing as a professional skill for portfolio evidence and code quality.


When This Skill Applies

Use this skill when:

  • Writing new tests for features or components
  • Setting up testing infrastructure
  • Debugging failing tests
  • Discussing testing strategies or coverage goals
  • Refactoring tests for better maintainability
  • Questions about testing best practices
  • Deciding what to test and when

Testing Philosophy

Why Test?

Not for 100% coverage - Test for:

  1. Confidence - Deploy without fear
  2. Documentation - Tests show how code should be used
  3. Refactoring safety - Change implementation without breaking behaviour
  4. Regression prevention - Bugs stay fixed
  5. Design feedback - Hard-to-test code often signals design issues

The Testing Pyramid

       /\
      /  \     E2E Tests
     /    \    (Few, slow, expensive)
    /------\
   /        \  Integration Tests
  /          \ (Some, medium speed)
 /------------\
/              \ Unit Tests
----------------  (Many, fast, cheap)

Distribution target:

  • 70% Unit tests - Fast, isolated, test single functions/modules
  • 20% Integration tests - Test component interactions, API calls
  • 10% E2E tests - Test critical user journeys

Pragmatic Approach

Test-after development workflow:

  1. Implement working feature
  2. Manual verification
  3. Assess risk (see Risk-Based Testing)
  4. Write automated tests for high/medium risk code
  5. Refactor with test safety net

This approach:

  • Lets you prototype quickly
  • Tests based on real implementation
  • Focuses effort where it matters
  • Builds confidence incrementally

Risk-Based Testing

Prioritize testing based on risk assessment

Risk Dimensions

Impact - What breaks if this fails?

  • Critical: Data loss, security breach, payment failures
  • High: Core features broken, bad UX
  • Medium: Minor features affected
  • Low: Cosmetic issues

Complexity - How likely to have bugs?

  • High: Complex algorithms, async logic, edge cases
  • Medium: Standard business logic
  • Low: Simple CRUD, straightforward functions

Change Frequency - How often modified?

  • High: Rapidly evolving features
  • Medium: Occasional updates
  • Low: Set-and-forget code

Testing Priority Matrix

HIGH PRIORITY (Must test):
✓ Payment/financial logic
✓ Authentication/authorization
✓ Data validation and persistence
✓ Critical user journeys
✓ Complex algorithms
✓ API integrations
✓ Security-sensitive code
✓ Accessibility requirements (keyboard nav, screen reader, contrast)

MEDIUM PRIORITY (Should test):
✓ Business logic with multiple branches
✓ Utility functions used across codebase
✓ Form validation
✓ Data transformations
✓ Error handling paths
✓ Frequently changed features

LOW PRIORITY (Optional):
- Simple getters/setters
- UI styling/layout
- Configuration files
- Straightforward CRUD operations
- One-time scripts

Risk Assessment Example

// HIGH RISK - Must test
// Impact: Critical (payments)
// Complexity: High (currency conversion, rounding)
// Change frequency: Medium
function calculateOrderTotal(items, discounts, taxRate) {
  // Complex calculation logic
  // Write comprehensive tests
}

// MEDIUM RISK - Should test
// Impact: Medium (UX issue if broken)
// Complexity: Medium (validation rules)
// Change frequency: Low
function validateEmail(email) {
  // Standard validation
  // Write basic tests
}

// LOW RISK - Optional
// Impact: Low (cosmetic)
// Complexity: Low (simple assignment)
// Change frequency: Low
function getUserDisplayName(user) {
  return user.name || user.email;
  // Can skip testing, or add simple test
}

Test-After Development Workflow

Step 1: Implement Feature

Focus on making it work:

  • Write working code
  • Manual testing in browser/console
  • Get feedback from users/stakeholders
  • Iterate on implementation

Don't worry about tests yet - understand the problem first.

Step 2: Manual Verification

Test the feature manually:

  • Happy path works
  • Edge cases handled
  • Error states graceful
  • Performance acceptable

Document interesting cases - these become test scenarios.

Step 3: Risk Assessment

Ask yourself:

  • What's the impact if this breaks?
  • How complex is this code?
  • Will this change frequently?
  • Are there edge cases I'm worried about?

Use the priority matrix to decide testing level.

Step 4: Write Automated Tests

Based on risk assessment:

High priority - Comprehensive tests:

describe('calculateOrderTotal', () => {
  it('should calculate total with single item');
  it('should apply percentage discount');
  it('should apply fixed discount');
  it('should calculate tax correctly');
  it('should handle multiple currencies');
  it('should round to 2 decimal places');
  it('should throw on negative prices');
  it('should handle empty cart');
});

Medium priority - Essential tests:

describe('validateEmail', () => {
  it('should accept valid email');
  it('should reject invalid format');
  it('should reject missing domain');
});

Low priority - Skip or minimal:

// Maybe one smoke test if you're feeling thorough
it('should return user name when available', () => {
  expect(getUserDisplayName({ name: 'Alice' })).toBe('Alice');
});

Step 5: Refactor with Confidence

Now that tests exist:

  • Optimize performance
  • Improve code structure
  • Rename variables
  • Extract functions

Tests catch regressions while you improve code.


Test-Driven Bug Fixing

When bugs are found, use this workflow:

1. Reproduce Bug

Write failing test that reproduces the issue:

it('should handle empty cart without crashing', () => {
  // This currently fails
  const result = calculateOrderTotal([], [], 0.2);
  expect(result).toBe(0);
});

2. Fix Bug

Implement the fix:

function calculateOrderTotal(items, discounts, taxRate) {
  if (items.length === 0) return 0; // Fix
  
  // Rest of logic...
}

3. Verify Test Passes

Run test suite, confirm:

  • New test passes
  • No regressions

4. Keep Test for Regression

This test now prevents the bug from returning.

Benefits:

  • Documents bug fixes
  • Builds test suite organically
  • Prevents regression
  • Forces you to understand the bug

Vitest Setup

Installation

npm install -D vitest @vitest/ui
npm install -D @testing-library/svelte @testing-library/jest-dom

Configuration (vitest.config.ts)

import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte({ hot: !process.env.VITEST })],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      exclude: ['**/*.test.ts', '**/*.spec.ts', '**/types.ts']
    }
  }
});

Setup File (src/test/setup.ts)

import '@testing-library/jest-dom';
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/svelte';

// Cleanup after each test
afterEach(() => {
  cleanup();
});

Basic Test Structure

Unit Test Pattern

import { describe, it, expect, beforeEach, afterEach } from 'vitest';

describe('calculateTotal', () => {
  it('should sum array of numbers', () => {
    const result = calculateTotal([1, 2, 3]);
    expect(result).toBe(6);
  });

  it('should return 0 for empty array', () => {
    const result = calculateTotal([]);
    expect(result).toBe(0);
  });

  it('should handle negative numbers', () => {
    const result = calculateTotal([-1, -2, 3]);
    expect(result).toBe(0);
  });
});

AAA Pattern (Arrange-Act-Assert)

it('should update user profile', () => {
  // Arrange - Set up test data
  const user = { id: '1', name: 'Alice' };
  const updates = { name: 'Alicia' };
  
  // Act - Execute the code under test
  const result = updateProfile(user, updates);
  
  // Assert - Verify the outcome
  expect(result.name).toBe('Alicia');
  expect(result.id).toBe('1');
});

Svelte Component Testing

Basic Component Test

import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.svelte';

describe('Counter', () => {
  it('should render initial count', () => {
    render(Counter, { props: { initialCount: 0 } });
    
    expect(screen.getByText('Count: 0')).toBeInTheDocument();
  });

  it('should increment count on button click', async () => {
    render(Counter, { props: { initialCount: 0 } });
    
    const button = screen.getByRole('button', { name: /increment/i });
    await fireEvent.click(button);
    
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
});

Testing Component Props

it('should accept and display custom label', () => {
  render(Button, { 
    props: { 
      label: 'Click Me',
      variant: 'primary' 
    } 
  });
  
  const button = screen.getByRole('button');
  expect(button).toHaveTextContent('Click Me');
  expect(button).toHaveClass('btn--primary');
});

Testing Events

it('should emit custom event on click', async () => {
  const { component } = render(Button);
  
  const handleClick = vi.fn();
  component.$on('click', handleClick);
  
  const button = screen.getByRole('button');
  await fireEvent.click(button);
  
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Testing Reactive Statements

import { tick } from 'svelte';

it('should update derived value when input changes', async () => {
  const { component } = render(Calculator);
  
  const input = screen.getByLabelText('Number');
  await fireEvent.input(input, { target: { value: '5' } });
  await tick(); // Wait for reactive statements to run
  
  expect(screen.getByText('Doubled: 10')).toBeInTheDocument();
});

Mocking Strategies

Mocking Functions

import { vi } from 'vitest';

it('should call API with correct parameters', async () => {
  const mockFetch = vi.fn().mockResolvedValue({
    ok: true,
    json: async () => ({ id: '1', name: 'Test' })
  });
  
  global.fetch = mockFetch;
  
  await fetchUser('1');
  
  expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
});

Mocking Modules

import { vi } from 'vitest';

// Mock entire module
vi.mock('$lib/api', () => ({
  fetchUsers: vi.fn().mockResolvedValue([
    { id: '1', name: 'Alice' }
  ])
}));

// Or mock specific exports
vi.mock('$lib/utils', async () => {
  const actual = await vi.importActual('$lib/utils');
  return {
    ...actual,
    generateId: vi.fn(() => 'test-id')
  };
});

Mocking Supabase

import { vi } from 'vitest';

const mockSupabase = {
  from: vi.fn().mockReturnValue({
    select: vi.fn().mockReturnValue({
      eq: vi.fn().mockResolvedValue({
        data: [{ id: '1', email: 'test@example.com' }],
        error: null
      })
    }),
    insert: vi.fn().mockResolvedValue({
      data: { id: '1' },
      error: null
    })
  })
};

vi.mock('$lib/supabaseClient', () => ({
  supabase: mockSupabase
}));

Mocking Stores

import { vi } from 'vitest';
import { writable } from 'svelte/store';

// Mock store module
vi.mock('$lib/stores/user', () => ({
  userStore: writable({ id: '1', name: 'Test User' })
}));

// Or spy on store methods
it('should update store on success', async () => {
  const { subscribe, set } = writable(null);
  const setSpy = vi.spyOn({ set }, 'set');
  
  await loadUserData();
  
  expect(setSpy).toHaveBeenCalledWith({ id: '1', name: 'Alice' });
});

Testing Async Code

Promises

it('should fetch user data', async () => {
  const user = await fetchUser('1');
  
  expect(user).toEqual({
    id: '1',
    name: 'Alice'
  });
});

Callbacks

it('should call callback with result', (done) => {
  fetchUser('1', (user) => {
    expect(user.name).toBe('Alice');
    done();
  });
});

Waiting for DOM Updates

import { waitFor } from '@testing-library/svelte';

it('should show loading then data', async () => {
  render(UserProfile, { props: { userId: '1' } });
  
  expect(screen.getByText('Loading...')).toBeInTheDocument();
  
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
  });
});

Test Organization

File Structure

src/
├── lib/
│   ├── components/
│   │   ├── Button.svelte
│   │   └── Button.test.ts
│   ├── utils/
│   │   ├── date.ts
│   │   └── date.test.ts
│   └── api/
│       ├── users.ts
│       └── users.test.ts
└── test/
    ├── setup.ts
    ├── helpers.ts
    └── mocks/
        ├── supabase.ts
        └── api.ts

Naming Conventions

✓ Button.test.ts
✓ Button.spec.ts
✗ test-button.ts
✗ ButtonTests.ts

Test Helpers

// test/helpers.ts
export function renderWithProviders(component, props = {}) {
  return render(component, {
    props,
    context: new Map([
      ['supabase', mockSupabase],
      ['user', testUser]
    ])
  });
}

export const testUser = {
  id: 'test-id',
  email: 'test@example.com',
  name: 'Test User'
};

Coverage Goals

What to Aim For

Not 100% - Diminishing returns after ~80%

Focus coverage on:

  • ✅ Business logic functions
  • ✅ Complex algorithms
  • ✅ Utilities used across codebase
  • ✅ Critical user journeys
  • ✅ Bug-prone areas

Lower priority:

  • ❌ Simple getters/setters
  • ❌ Type definitions
  • ❌ Configuration files
  • ❌ Generated code

Running Coverage

# Run tests with coverage
npm run test:coverage

# View HTML report
open coverage/index.html

Coverage Configuration

// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      statements: 80,
      branches: 75,
      functions: 80,
      lines: 80,
      exclude: [
        '**/*.config.ts',
        '**/*.d.ts',
        '**/types.ts',
        'src/test/**'
      ]
    }
  }
});

Common Pitfalls

Over-Mocking

// ✗ Bad: Mocking everything, testing nothing
it('should update user', async () => {
  vi.mock('./updateUser');
  const result = await updateUser(user);
  expect(result).toBeDefined(); // What are we even testing?
});

// ✓ Good: Test real code, mock external dependencies
it('should update user', async () => {
  const mockFetch = vi.fn().mockResolvedValue({ ok: true });
  global.fetch = mockFetch;
  
  const result = await updateUser(user);
  
  expect(mockFetch).toHaveBeenCalledWith('/api/users/1', {
    method: 'PUT',
    body: JSON.stringify(user)
  });
  expect(result.success).toBe(true);
});

Testing Implementation Details

// ✗ Bad: Testing internal state
it('should set loading to true', () => {
  const { component } = render(UserList);
  expect(component.loading).toBe(true); // Internal detail
});

// ✓ Good: Testing observable behaviour
it('should show loading indicator', () => {
  render(UserList);
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

Brittle Selectors

// ✗ Bad: Fragile selectors
const button = container.querySelector('.btn.btn--primary.large');

// ✓ Good: Semantic queries
const button = screen.getByRole('button', { name: 'Submit' });

Not Testing Error Cases

// ✗ Bad: Only happy path
it('should fetch user', async () => {
  const user = await fetchUser('1');
  expect(user).toBeDefined();
});

// ✓ Good: Test errors too
describe('fetchUser', () => {
  it('should return user on success', async () => {
    const user = await fetchUser('1');
    expect(user.id).toBe('1');
  });

  it('should throw on network error', async () => {
    global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
    
    await expect(fetchUser('1')).rejects.toThrow('Network error');
  });

  it('should return null when user not found', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 404
    });
    
    const user = await fetchUser('999');
    expect(user).toBeNull();
  });
});

Quick Reference

Common Matchers

// Equality
expect(value).toBe(5);              // Strict equality
expect(object).toEqual({ a: 1 });   // Deep equality
expect(array).toContain('item');    // Array contains

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThanOrEqual(5);
expect(value).toBeCloseTo(0.3, 2);  // Floating point

// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain('substring');

// Arrays/Objects
expect(array).toHaveLength(3);
expect(object).toHaveProperty('key', 'value');

// Errors
expect(() => fn()).toThrow();
expect(() => fn()).toThrow(Error);
expect(() => fn()).toThrow('message');

// Async
await expect(promise).resolves.toBe('value');
await expect(promise).rejects.toThrow();

Common Testing Library Queries

// Preferred (accessible)
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText('Email');
screen.getByPlaceholderText('Enter email');
screen.getByText('Welcome');

// Fallbacks
screen.getByTestId('submit-button');

// Query variants
screen.getBy...    // Throws if not found
screen.queryBy...  // Returns null if not found
screen.findBy...   // Async, waits for element

// Multiple elements
screen.getAllByRole('listitem');

Running Tests

# Run all tests
npm test

# Watch mode
npm test -- --watch

# Run specific file
npm test Button.test.ts

# Run tests matching pattern
npm test -- --grep "Button"

# Run with coverage
npm test -- --coverage

# UI mode
npm test -- --ui

Portfolio Evidence

KSBs Demonstrated by Testing:

  • S9: Create Analysis Artefacts (test plans, coverage reports, risk assessments)
  • S10: Analyse Problem Reports (reproduction tests, debugging tests)
  • S11: Apply Appropriate Recovery Techniques (regression tests)
  • S14: Follow Company Procedures (testing standards, CI integration)

How to Document:

  • Screenshot coverage reports showing strategic testing
  • Document risk assessment decisions in README/docs
  • Show test files alongside features
  • Explain testing decisions in code review
  • Document bug reproduction tests
  • Demonstrate professional judgment about test priorities

Evidence Example:

## Testing Strategy

Applied risk-based testing approach to this feature:

HIGH PRIORITY (Tested):
- Payment calculation logic - Complex algorithm, financial impact
- User authentication - Security critical
- Data validation - Prevents data corruption

MEDIUM PRIORITY (Basic tests):
- Form validation - Standard patterns, low complexity
- API error handling - Important but straightforward

LOW PRIORITY (Manual testing only):
- UI styling - Visual verification sufficient
- Configuration loading - One-time, low risk

Accessibility Testing

Accessibility is a testable requirement, not a subjective preference. Include it in the testing strategy alongside functional tests.

Automated Accessibility Checks

import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('should have no accessibility violations', async () => {
	const { container } = render(LoginForm);
	const results = await axe(container);
	expect(results).toHaveNoViolations();
});

Keyboard Navigation Tests

it('should be navigable by keyboard', async () => {
	render(LoginForm);

	// Tab to email input
	await userEvent.tab();
	expect(screen.getByLabelText('Email')).toHaveFocus();

	// Tab to password input
	await userEvent.tab();
	expect(screen.getByLabelText('Password')).toHaveFocus();

	// Tab to submit button
	await userEvent.tab();
	expect(screen.getByRole('button', { name: /log in/i })).toHaveFocus();

	// Enter submits
	await userEvent.keyboard('{Enter}');
	// Assert form submitted
});

Screen Reader Assertions

it('should announce errors to screen readers', async () => {
	render(LoginForm);

	const submitButton = screen.getByRole('button', { name: /log in/i });
	await fireEvent.click(submitButton);

	// Error messages should be associated with inputs
	const emailInput = screen.getByLabelText('Email');
	const errorId = emailInput.getAttribute('aria-describedby');
	expect(errorId).toBeTruthy();
	expect(document.getElementById(errorId)).toHaveTextContent('Email is required');
});

What to Test for Accessibility

HIGH PRIORITY:
✓ Forms: labels, error association, keyboard submit
✓ Modals: focus trap, escape to close, focus return
✓ Navigation: keyboard traversal, skip links
✓ Dynamic content: aria-live announcements

MEDIUM PRIORITY:
✓ Colour contrast (automated via axe)
✓ Image alt text presence
✓ Heading hierarchy

AUTOMATED (run on every component):
✓ axe-core violations check

Success Criteria

Tests are effective when they:

  • Pass reliably (no flakiness)
  • Run quickly (<1s for unit tests)
  • Test behaviour, not implementation
  • Catch real bugs before production
  • Give confidence to refactor
  • Focus on high-risk code
  • Serve as documentation
  • Reflect professional judgment about priorities
  • Include accessibility checks for user-facing components

Source

git clone https://github.com/JasonWarrenUK/claude-code-config/blob/main/skills/testing-obsessive/SKILL.mdView on GitHub

Overview

Testing Foundations provides practical guidance for JavaScript/TypeScript projects, with emphasis on Vitest and Svelte component testing. It promotes a pragmatic, risk-based approach to test strategy, coverage goals, and maintaining test suites that support reliable, maintainable code.

How This Skill Works

It outlines testing philosophy, the Testing Pyramid with distributions, and a test-after development workflow, then shows how to apply Risk-Based Testing to prioritize and write tests that protect critical functionality.

When to Use It

  • Writing new tests for features or components
  • Setting up testing infrastructure
  • Debugging failing tests
  • Discussing testing strategies or coverage goals
  • Refactoring tests for better maintainability

Quick Start

  1. Step 1: Identify high/medium risk code and write unit tests for those paths
  2. Step 2: Set up Vitest and a Svelte testing environment (testing-library/svelte)
  3. Step 3: Run tests, fix failures, and gradually extend coverage with risk-based priorities

Best Practices

  • Prioritize tests by risk: assess impact, complexity, and change frequency to decide what to test
  • Follow a test-after development workflow: implement feature, then verify manually before automating
  • Adhere to the Testing Pyramid: aim for ~70% unit, 20% integration, 10% E2E tests
  • Focus automated tests on high/medium risk code and critical paths
  • Keep tests fast, isolated, and maintainable, mirroring real implementation

Example Use Cases

  • Add unit tests for a pure function in a TypeScript module using Vitest
  • Write integration tests for a Svelte component that makes API calls
  • Refactor an existing test suite to reduce flakiness and improve readability
  • Set up a Vitest + Svelte testing environment in a new project
  • Create end-to-end tests for a critical user journey while maintaining test safety nets

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers