testing-obsessive
npx machina-cli add skill JasonWarrenUK/claude-code-config/testing-obsessive --openclawTesting 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:
- Confidence - Deploy without fear
- Documentation - Tests show how code should be used
- Refactoring safety - Change implementation without breaking behaviour
- Regression prevention - Bugs stay fixed
- 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:
- Implement working feature
- Manual verification
- Assess risk (see Risk-Based Testing)
- Write automated tests for high/medium risk code
- 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
- Step 1: Identify high/medium risk code and write unit tests for those paths
- Step 2: Set up Vitest and a Svelte testing environment (testing-library/svelte)
- 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