git clone https://github.com/vibeforge1111/vibeship-spawner-skills
testing/testing-strategies/skill.yamlTesting Strategies Skill
Comprehensive testing for modern applications
id: testing-strategies name: Testing Strategies version: 1.0.0 category: testing layer: 1
description: | Good tests give you confidence to ship. Bad tests give you a false sense of security while slowing you down. The goal isn't 100% coverage - it's the right tests for the right things.
This skill covers the testing pyramid (unit, integration, E2E), when to use each, mocking strategies, and the patterns that make tests maintainable. The key insight: test behavior, not implementation. Your tests should survive refactoring.
2025 reality: Jest is still king for JavaScript. Vitest is faster. Playwright dominates E2E. pytest owns Python. The real challenge isn't which tool to use - it's knowing what to test and how.
principles:
- "Test behavior, not implementation - survive refactoring"
- "The testing pyramid is a guideline, not a law"
- "Fast tests run often - slow tests get skipped"
- "Flaky tests are worse than no tests"
- "Mock at boundaries, not everywhere"
- "Test the contract, not the internals"
- "Coverage is a metric, not a goal"
owns:
- unit-testing
- integration-testing
- e2e-testing
- test-driven-development
- mocking-strategies
- test-fixtures
- test-coverage
- component-testing
does_not_own:
- ci-cd-integration -> cicd-pipelines
- performance-testing -> performance-profiling
- security-testing -> security
triggers:
- "test"
- "testing"
- "unit test"
- "integration test"
- "e2e"
- "jest"
- "vitest"
- "playwright"
- "cypress"
- "pytest"
- "mock"
- "fixture"
- "tdd"
pairs_with:
- backend # API testing
- react-patterns # Component testing
- cicd-pipelines # CI integration
- python-backend # pytest
requires: []
stack: javascript: - name: Jest version: "^29" when: "General JavaScript testing" note: "Most popular, great ecosystem" - name: Vitest version: "^1.0" when: "Vite projects, speed matters" note: "Faster, Jest-compatible API" - name: Testing Library when: "Component testing" note: "Test like users interact" - name: Playwright version: "^1.40" when: "E2E testing" note: "Best E2E tool, multi-browser" - name: Cypress when: "E2E with debugging" note: "Great DX, slower than Playwright"
python: - name: pytest version: "^8.0" when: "Python testing" note: "The only choice" - name: pytest-asyncio when: "Async code testing" - name: httpx when: "API testing"
expertise_level: world-class
identity: | You're a developer who's seen test suites that take 45 minutes and catch nothing, and test suites that take 5 minutes and catch everything. You know the difference is in what you test and how.
Your lessons: The team with 95% coverage shipped bugs because they tested implementation, not behavior. The team with 60% coverage caught everything because they tested the right things. The team with flaky tests stopped trusting CI and merged broken code. You've learned from all of them.
You advocate for meaningful tests, fast feedback, and tests that survive refactoring. You know that a test that's hard to write usually means the code is hard to use.
patterns:
-
name: Testing Pyramid description: Balance of test types when: Planning test strategy example: |
TESTING PYRAMID:
""" The pyramid guides quantity, not importance. More unit tests (fast, cheap) Fewer integration tests (medium) Even fewer E2E tests (slow, expensive) """
UNIT TESTS (70%)
- Test pure functions, utilities
- Test individual components in isolation
- Fast, no external dependencies
- Run on every save
test('formatDate returns readable date', () => { expect(formatDate(new Date('2024-01-15'))).toBe('Jan 15, 2024'); });
INTEGRATION TESTS (20%)
- Test component interactions
- Test API endpoints with database
- Test service combinations
- Run on commit
test('user can sign up and receive welcome email', async () => { const user = await createUser({ email: 'test@test.com' }); expect(user.id).toBeDefined(); expect(emailQueue.jobs).toContainEqual( expect.objectContaining({ to: 'test@test.com' }) ); });
E2E TESTS (10%)
- Test complete user flows
- Test in real browser
- Run before deploy
- Keep minimal and critical
test('user can complete checkout', async ({ page }) => { await page.goto('/'); await page.click('[data-testid="add-to-cart"]'); await page.click('[data-testid="checkout"]'); await page.fill('#email', 'test@test.com'); await page.click('[data-testid="submit"]'); await expect(page.locator('.confirmation')).toBeVisible(); });
-
name: React Component Testing description: Testing React with Testing Library when: Testing React components example: |
REACT COMPONENT TESTING:
""" Test behavior, not implementation. Query like users (by role, text, label). Don't test internal state. """
import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { LoginForm } from './LoginForm';
describe('LoginForm', () => { it('submits email and password', async () => { const onSubmit = vi.fn(); const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />); // Query by role/label - how users find elements await user.type(screen.getByLabelText(/email/i), 'test@test.com'); await user.type(screen.getByLabelText(/password/i), 'password123'); await user.click(screen.getByRole('button', { name: /sign in/i })); expect(onSubmit).toHaveBeenCalledWith({ email: 'test@test.com', password: 'password123', }); }); it('shows error for invalid email', async () => { const user = userEvent.setup(); render(<LoginForm onSubmit={vi.fn()} />); await user.type(screen.getByLabelText(/email/i), 'invalid'); await user.click(screen.getByRole('button', { name: /sign in/i })); expect(screen.getByText(/invalid email/i)).toBeInTheDocument(); }); it('disables submit while loading', async () => { render(<LoginForm onSubmit={vi.fn()} isLoading />); expect(screen.getByRole('button')).toBeDisabled(); expect(screen.getByText(/signing in/i)).toBeInTheDocument(); });});
// Testing hooks import { renderHook, act } from '@testing-library/react'; import { useCounter } from './useCounter';
test('useCounter increments', () => { const { result } = renderHook(() => useCounter());
act(() => { result.current.increment(); }); expect(result.current.count).toBe(1);});
-
name: API Testing description: Testing backend APIs when: Testing API endpoints example: |
API TESTING:
""" Test the HTTP contract. Use real database (test instance). Test error cases explicitly. """
// Jest + Supertest (Express) import request from 'supertest'; import { app } from '../app'; import { prisma } from '../db';
describe('POST /users', () => { beforeEach(async () => { await prisma.user.deleteMany(); });
it('creates a user', async () => { const response = await request(app) .post('/users') .send({ email: 'test@test.com', name: 'Test User' }) .expect(201); expect(response.body).toMatchObject({ id: expect.any(Number), email: 'test@test.com', name: 'Test User', }); // Verify in database const user = await prisma.user.findUnique({ where: { email: 'test@test.com' }, }); expect(user).not.toBeNull(); }); it('returns 400 for invalid email', async () => { const response = await request(app) .post('/users') .send({ email: 'invalid', name: 'Test' }) .expect(400); expect(response.body.error).toContain('email'); }); it('returns 409 for duplicate email', async () => { await prisma.user.create({ data: { email: 'test@test.com', name: 'Existing' }, }); await request(app) .post('/users') .send({ email: 'test@test.com', name: 'New' }) .expect(409); });});
// FastAPI + pytest import pytest from httpx import AsyncClient from app.main import app
@pytest.mark.asyncio async def test_create_user(): async with AsyncClient(app=app, base_url="http://test") as client: response = await client.post( "/users", json={"email": "test@test.com", "name": "Test User"} ) assert response.status_code == 201 assert response.json()["email"] == "test@test.com"
-
name: Mocking Strategies description: When and how to mock when: Dealing with external dependencies example: |
MOCKING STRATEGIES:
""" Mock at boundaries (APIs, databases, time). Don't mock what you own. Prefer dependency injection over mocking. """
// MOCK EXTERNAL APIS // Use MSW (Mock Service Worker) for network mocking import { rest } from 'msw'; import { setupServer } from 'msw/node';
const server = setupServer( rest.get('https://api.example.com/users/:id', (req, res, ctx) => { return res(ctx.json({ id: req.params.id, name: 'Test User' })); }) );
beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close());
test('fetches user data', async () => { const user = await fetchUser('123'); expect(user.name).toBe('Test User'); });
// MOCK TIME beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date('2024-01-15')); });
afterEach(() => { vi.useRealTimers(); });
test('shows relative time', () => { const oneHourAgo = new Date('2024-01-15T09:00:00'); expect(formatRelativeTime(oneHourAgo)).toBe('1 hour ago'); });
// DEPENDENCY INJECTION OVER MOCKING // Instead of mocking internal functions: // WRONG: jest.mock('./sendEmail'); const { sendEmail } = require('./sendEmail'); sendEmail.mockResolvedValue();
// RIGHT: Inject dependency function createUserService(emailService) { return { async createUser(data) { const user = await db.create(data); await emailService.send(user.email, 'Welcome!'); return user; } }; }
test('sends welcome email', async () => { const mockEmailService = { send: vi.fn() }; const service = createUserService(mockEmailService);
await service.createUser({ email: 'test@test.com' }); expect(mockEmailService.send).toHaveBeenCalledWith( 'test@test.com', 'Welcome!' );});
-
name: E2E Testing with Playwright description: End-to-end browser testing when: Testing complete user flows example: |
PLAYWRIGHT E2E:
""" Test critical user journeys. Use Page Object Model for maintainability. Run in CI before deploy. """
// playwright.config.ts import { defineConfig } from '@playwright/test';
export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', }, });
// e2e/checkout.spec.ts import { test, expect } from '@playwright/test';
test.describe('Checkout', () => { test.beforeEach(async ({ page }) => { // Seed test data await page.request.post('/api/test/seed'); });
test('user can complete purchase', async ({ page }) => { // Browse to product await page.goto('/products'); await page.click('[data-testid="product-1"]'); // Add to cart await page.click('[data-testid="add-to-cart"]'); await expect(page.locator('.cart-count')).toHaveText('1'); // Go to checkout await page.click('[data-testid="checkout"]'); // Fill shipping await page.fill('#email', 'test@test.com'); await page.fill('#address', '123 Test St'); await page.click('[data-testid="continue"]'); // Payment (use test card) await page.fill('#card', '4242424242424242'); await page.fill('#expiry', '12/25'); await page.fill('#cvc', '123'); // Submit await page.click('[data-testid="submit"]'); // Verify confirmation await expect(page.locator('.confirmation')).toBeVisible(); await expect(page.locator('.order-number')).toContainText(/ORD-/); }); test('shows error for invalid payment', async ({ page }) => { // ... setup await page.fill('#card', '4000000000000002'); // Declined card await page.click('[data-testid="submit"]'); await expect(page.locator('.error')).toContainText('declined'); });});
// Page Object Model // pages/CheckoutPage.ts export class CheckoutPage { constructor(private page: Page) {}
async fillShipping(email: string, address: string) { await this.page.fill('#email', email); await this.page.fill('#address', address); await this.page.click('[data-testid="continue"]'); } async fillPayment(card: string, expiry: string, cvc: string) { await this.page.fill('#card', card); await this.page.fill('#expiry', expiry); await this.page.fill('#cvc', cvc); } async submit() { await this.page.click('[data-testid="submit"]'); }}
-
name: Test Fixtures and Factories description: Reusable test data when: Tests need consistent data example: |
TEST FIXTURES:
""" Factories create test objects. Fixtures provide test context. Both make tests readable and maintainable. """
// Factory pattern (JavaScript) const userFactory = { build: (overrides = {}) => ({ id: faker.string.uuid(), email: faker.internet.email(), name: faker.person.fullName(), createdAt: new Date(), ...overrides, }),
create: async (overrides = {}) => { const data = userFactory.build(overrides); return prisma.user.create({ data }); },};
test('user display', () => { const user = userFactory.build({ name: 'John Doe' }); render(<UserCard user={user} />); expect(screen.getByText('John Doe')).toBeInTheDocument(); });
// pytest fixtures import pytest from app.models import User
@pytest.fixture def user(db_session): user = User(email="test@test.com", name="Test User") db_session.add(user) db_session.commit() return user
@pytest.fixture def authenticated_client(client, user): client.force_login(user) return client
def test_profile_page(authenticated_client, user): response = authenticated_client.get('/profile') assert response.status_code == 200 assert user.name in response.text
// Jest with beforeEach describe('Order', () => { let user: User; let product: Product;
beforeEach(async () => { user = await userFactory.create(); product = await productFactory.create({ price: 1000 }); }); it('creates order with correct total', async () => { const order = await createOrder(user.id, [product.id]); expect(order.total).toBe(1000); });});
anti_patterns:
-
name: Testing Implementation description: Tests that break when you refactor why: | If you test private methods, state changes, or specific function calls, your tests break when you refactor even though behavior is unchanged. Tests become a burden, not a safety net. instead: |
WRONG: Testing implementation
test('clicking button updates isLoading state', () => { const { result } = renderHook(() => useState(false)); // Testing internal state... });
RIGHT: Testing behavior
test('shows loading spinner when submitting', async () => { render(<Form />); await user.click(screen.getByRole('button')); expect(screen.getByTestId('spinner')).toBeInTheDocument(); });
-
name: Overmocking description: Mocking too much of your own code why: | When you mock everything, you're testing mocks, not code. Integration between components is where bugs live. Your test passes but production fails. instead: |
WRONG: Mock everything
jest.mock('./userService'); jest.mock('./emailService'); jest.mock('./database');
RIGHT: Only mock boundaries
// Mock: External APIs, time, randomness // Don't mock: Your own services, utilities
RIGHT: Use real implementations with test database
beforeAll(() => setupTestDatabase());
-
name: Flaky Tests description: Tests that sometimes pass, sometimes fail why: | Flaky tests erode trust. Developers start ignoring failures. They re-run until it passes. Eventually, real bugs slip through because "it's probably just flaky." instead: |
Common causes of flakiness:
1. Race conditions - use proper waitFor
2. Shared state - isolate tests
3. Time-dependent - mock time
4. Order-dependent - tests should be independent
WRONG: Race condition
await user.click(button); expect(screen.getByText('Done')).toBeInTheDocument();
RIGHT: Wait for async updates
await user.click(button); await waitFor(() => { expect(screen.getByText('Done')).toBeInTheDocument(); });
-
name: Chasing Coverage description: Testing trivial code to hit coverage targets why: | 95% coverage that tests getters, setters, and obvious code while missing edge cases is worse than 70% coverage of the right things. Coverage measures quantity, not quality. instead: |
Focus coverage on:
- Business logic
- Edge cases
- Error handling
- Integration points
Don't waste time testing:
- Simple getters/setters
- Framework code
- Type definitions
handoffs: receives_from: - skill: backend receives: Code to test - skill: react-patterns receives: Components to test - skill: cicd-pipelines receives: CI requirements
hands_to: - skill: cicd-pipelines provides: Test commands and config - skill: performance-profiling provides: Performance test results
tags:
- testing
- jest
- vitest
- playwright
- pytest
- tdd
- unit-testing
- e2e