# Testing Obsessive

> 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.

- Skill: `majiayu000/testing-obsessive` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/testing-obsessive`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/testing-obsessive/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/testing-obsessive

---


---
name: testing-obsessive
description: Pragmatic testing with Vitest: risk-based strategy, Svelte component testing, test-after development.
user-invocable: false
effort: medium
paths:
  - "**/*.test.*"
  - "**/*.spec.*"
allowed-tools:
  - Read
  - Glob
  - Grep
  - Bash
---

# 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
```typescript
// 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:
```typescript
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:
```typescript
describe('validateEmail', () => {
  it('should accept valid email');
  it('should reject invalid format');
  it('should reject missing domain');
});
```

**Low priority** - Skip or minimal:
```typescript
// 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:
```typescript
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:
```typescript
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
```bash
npm install -D vitest @vitest/ui
npm install -D @testing-library/svelte @testing-library/jest-dom
```

### Configuration (`vitest.config.ts`)
```typescript
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`)
```typescript
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
```typescript
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)
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
it('should fetch user data', async () => {
  const user = await fetchUser('1');

  expect(user).toEqual({
    id: '1',
    name: 'Alice'
  });
});
```

### Callbacks
```typescript
it('should call callback with result', (done) => {
  fetchUser('1', (user) => {
    expect(user.name).toBe('Alice');
    done();
  });
});
```

### Waiting for DOM Updates
```typescript
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
```typescript
// 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
```bash
# Run tests with coverage
npm run test:coverage

# View HTML report
open coverage/index.html
```

### Coverage Configuration
```typescript
// 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
```typescript
// ✗ 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
```typescript
// ✗ 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
```typescript
// ✗ 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
```typescript
// ✗ 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
```typescript
// 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
```typescript
// 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
```bash
# 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**:
```markdown
## 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
```typescript
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
```typescript
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
```typescript
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
