qa-engineer

QA Engineer & Software Testing Expert

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "qa-engineer" with this command: npx skills add johanruttens/paddle-battle/johanruttens-paddle-battle-qa-engineer

QA Engineer & Software Testing Expert

Expert guidance for comprehensive software testing, quality assurance, and bug detection.

Testing Philosophy

Core Principles

  • Shift left — Find bugs early; prevention over detection

  • Risk-based testing — Prioritize high-impact, high-probability failure areas

  • Test pyramid — Many unit tests, fewer integration tests, minimal E2E tests

  • Automation first — Automate repetitive tests; manual for exploratory

  • Clean test code — Tests are production code; maintain them accordingly

Test Pyramid Distribution

    /\
   /  \      E2E (5-10%)
  /----\     - Critical user journeys
 /      \    
/--------\   Integration (15-25%)

/ \ - API contracts, DB interactions /------------\ / \ Unit (65-80%) /________________\ - Functions, components, logic

Test Case Design

Structure (Arrange-Act-Assert)

describe('ShoppingCart', () => { describe('addItem', () => { it('should increase quantity when adding existing item', () => { // Arrange const cart = new ShoppingCart(); cart.addItem({ id: '1', name: 'Apple', quantity: 1 });

  // Act
  cart.addItem({ id: '1', name: 'Apple', quantity: 2 });
  
  // Assert
  expect(cart.getItem('1').quantity).toBe(3);
});

}); });

Naming Convention

[Unit][Scenario][ExpectedResult]

Examples:

  • calculateTotal_withEmptyCart_returnsZero
  • login_withInvalidPassword_showsErrorMessage
  • submitOrder_whenOutOfStock_preventsCheckout

Test Case Categories

Positive Tests — Valid inputs produce expected outputs

it('should create user with valid email and password', async () => { const user = await createUser('test@example.com', 'ValidPass123!'); expect(user.id).toBeDefined(); expect(user.email).toBe('test@example.com'); });

Negative Tests — Invalid inputs handled gracefully

it('should reject user creation with invalid email', async () => { await expect(createUser('invalid-email', 'ValidPass123!')) .rejects.toThrow('Invalid email format'); });

Boundary Tests — Edge cases at limits

it('should accept password with exactly 8 characters (minimum)', () => { expect(() => validatePassword('Pass123!')).not.toThrow(); });

it('should reject password with 7 characters (below minimum)', () => { expect(() => validatePassword('Pass12!')).toThrow(); });

Error Handling Tests — Failures fail gracefully

it('should handle network timeout gracefully', async () => { mockApi.simulateTimeout(); const result = await fetchUserData('123'); expect(result.error).toBe('Request timed out. Please try again.'); expect(result.data).toBeNull(); });

Test Types & Frameworks

Unit Testing

JavaScript/TypeScript — Jest/Vitest

// Function to test export function calculateDiscount(price: number, percentage: number): number { if (percentage < 0 || percentage > 100) { throw new Error('Invalid percentage'); } return price * (1 - percentage / 100); }

// Test file import { calculateDiscount } from './pricing';

describe('calculateDiscount', () => { it('applies 20% discount correctly', () => { expect(calculateDiscount(100, 20)).toBe(80); });

it('handles 0% discount', () => { expect(calculateDiscount(100, 0)).toBe(100); });

it('handles 100% discount', () => { expect(calculateDiscount(100, 100)).toBe(0); });

it('throws on negative percentage', () => { expect(() => calculateDiscount(100, -10)).toThrow('Invalid percentage'); });

it('handles decimal prices', () => { expect(calculateDiscount(99.99, 10)).toBeCloseTo(89.99, 2); }); });

React Components — React Testing Library

import { render, screen, fireEvent } from '@testing-library/react'; import { Counter } from './Counter';

describe('Counter', () => { it('renders initial count', () => { render(<Counter initialCount={5} />); expect(screen.getByText('Count: 5')).toBeInTheDocument(); });

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

it('calls onChange callback when count changes', () => { const handleChange = jest.fn(); render(<Counter initialCount={0} onChange={handleChange} />); fireEvent.click(screen.getByRole('button', { name: /increment/i })); expect(handleChange).toHaveBeenCalledWith(1); }); });

Integration Testing

API Integration — Supertest

import request from 'supertest'; import { app } from '../app'; import { db } from '../db';

describe('POST /api/users', () => { beforeEach(async () => { await db.clear('users'); });

it('creates user and returns 201', async () => { const response = await request(app) .post('/api/users') .send({ email: 'test@example.com', password: 'SecurePass123!' }) .expect(201);

expect(response.body.user.email).toBe('test@example.com');
expect(response.body.user.password).toBeUndefined(); // Not exposed

// Verify database state
const dbUser = await db.users.findByEmail('test@example.com');
expect(dbUser).toBeDefined();

});

it('returns 409 for duplicate email', async () => { await db.users.create({ email: 'test@example.com', password: 'hash' });

await request(app)
  .post('/api/users')
  .send({ email: 'test@example.com', password: 'SecurePass123!' })
  .expect(409);

}); });

Database Integration

describe('UserRepository', () => { let repo: UserRepository;

beforeAll(async () => { await setupTestDatabase(); repo = new UserRepository(testDb); });

afterEach(async () => { await testDb.clear('users'); });

afterAll(async () => { await teardownTestDatabase(); });

it('persists and retrieves user correctly', async () => { const created = await repo.create({ name: 'John', email: 'john@test.com' }); const retrieved = await repo.findById(created.id);

expect(retrieved).toMatchObject({
  name: 'John',
  email: 'john@test.com',
});

}); });

E2E Testing

Playwright

import { test, expect } from '@playwright/test';

test.describe('User Authentication', () => { test('complete login flow', async ({ page }) => { await page.goto('/login');

await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');

await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]'))
  .toContainText('Welcome back');

});

test('shows error for invalid credentials', async ({ page }) => { await page.goto('/login');

await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');

await expect(page.locator('[data-testid="error-message"]'))
  .toContainText('Invalid credentials');
await expect(page).toHaveURL('/login');

}); });

Mobile E2E — Detox (React Native)

describe('Shopping List', () => { beforeAll(async () => { await device.launchApp({ newInstance: true }); });

it('should add item to shopping list', async () => { await element(by.id('add-item-button')).tap(); await element(by.id('item-name-input')).typeText('Milk'); await element(by.id('item-quantity-input')).typeText('2'); await element(by.id('save-button')).tap();

await expect(element(by.text('Milk'))).toBeVisible();
await expect(element(by.text('2'))).toBeVisible();

});

it('should mark item as bought', async () => { await element(by.id('item-checkbox-milk')).tap(); await expect(element(by.id('item-milk'))).toHaveToggleValue(true); }); });

Mocking Strategies

Function Mocks

// Mock external service jest.mock('../services/emailService', () => ({ sendEmail: jest.fn().mockResolvedValue({ success: true }), }));

// Test with mock it('sends welcome email on registration', async () => { await registerUser({ email: 'test@example.com', password: 'Pass123!' });

expect(emailService.sendEmail).toHaveBeenCalledWith({ to: 'test@example.com', template: 'welcome', }); });

API Mocks — MSW (Mock Service Worker)

import { rest } from 'msw'; import { setupServer } from 'msw/node';

const server = setupServer( rest.get('/api/users/:id', (req, res, ctx) => { return res(ctx.json({ id: req.params.id, name: 'Test User' })); }), rest.post('/api/users', (req, res, ctx) => { return res(ctx.status(201), ctx.json({ id: '123', ...req.body })); }) );

beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close());

it('handles server error gracefully', async () => { server.use( rest.get('/api/users/:id', (req, res, ctx) => { return res(ctx.status(500)); }) );

const result = await fetchUser('123'); expect(result.error).toBe('Server error'); });

Time Mocks

beforeEach(() => { jest.useFakeTimers(); jest.setSystemTime(new Date('2024-01-15T10:00:00Z')); });

afterEach(() => { jest.useRealTimers(); });

it('expires session after 30 minutes', () => { const session = createSession();

jest.advanceTimersByTime(31 * 60 * 1000); // 31 minutes

expect(session.isExpired()).toBe(true); });

Bug Report Template

Bug Report: [Short descriptive title]

Severity: Critical | High | Medium | Low Priority: P0 | P1 | P2 | P3 Environment: Production | Staging | Development Platform: iOS 17.2 / Android 14 / Chrome 120 / etc.

Summary

[One sentence description of the issue]

Steps to Reproduce

  1. Navigate to [page/screen]
  2. Enter [specific data]
  3. Click [button/action]
  4. Observe [behavior]

Expected Behavior

[What should happen]

Actual Behavior

[What actually happens]

Evidence

  • Screenshots: [attached]
  • Video: [link]
  • Console logs: [attached]
  • Network trace: [attached]

Impact

[Who is affected and how severely]

Workaround

[If any temporary solution exists]

Additional Context

  • First noticed: [date]
  • Frequency: Always | Intermittent (X/10 attempts)
  • Related issues: #123, #456

Test Plan Template

Test Plan: [Feature/Release Name]

Overview

Objective: [What we're testing] Scope: [In scope / Out of scope] Timeline: [Start date - End date]

Test Strategy

Test Levels

LevelCoverageAutomation
Unit80%+100%
IntegrationCritical paths90%
E2EHappy paths70%
ManualEdge casesN/A

Risk Assessment

RiskLikelihoodImpactMitigation
Payment failuresMediumCriticalExtra payment gateway tests
Data migrationLowHighRollback testing

Test Cases

Functional Tests

  • TC001: User can create account with valid data
  • TC002: User cannot create account with duplicate email
  • TC003: User receives verification email ...

Non-Functional Tests

  • Performance: Page load < 2s
  • Security: SQL injection prevention
  • Accessibility: WCAG 2.1 AA compliance

Entry/Exit Criteria

Entry:

  • Code complete and deployed to staging
  • Test data prepared
  • Test environment stable

Exit:

  • All critical tests pass
  • No P0/P1 bugs open
  • Test coverage meets targets
  • Sign-off from QA lead

Code Review Checklist

Functionality

  • Code does what the ticket/PR describes

  • Edge cases handled

  • Error handling is appropriate

  • No hardcoded values that should be configurable

Security

  • No sensitive data logged or exposed

  • Input validation present

  • SQL/NoSQL injection prevented

  • Authentication/authorization checked

Performance

  • No N+1 queries

  • Appropriate indexes used

  • No memory leaks (event listeners cleaned up)

  • Large lists virtualized

Maintainability

  • Code is readable and self-documenting

  • Complex logic has comments

  • No duplicate code

  • Functions are single-purpose

Testing

  • Unit tests added for new logic

  • Edge cases tested

  • Tests are deterministic (no flaky tests)

  • Mocks are appropriate

Coverage Strategies

Minimum Coverage Targets

// jest.config.js module.exports = { coverageThreshold: { global: { branches: 70, functions: 80, lines: 80, statements: 80, }, './src/critical/': { branches: 90, functions: 95, lines: 95, }, }, };

Coverage Commands

Generate coverage report

npm test -- --coverage

View HTML report

open coverage/lcov-report/index.html

Check specific file

npm test -- --coverage --collectCoverageFrom="src/utils/pricing.ts"

Debugging Techniques

Systematic Debugging

  • Reproduce — Confirm the bug consistently

  • Isolate — Narrow down to smallest failing case

  • Identify — Find the root cause (not symptoms)

  • Fix — Apply minimal, targeted fix

  • Verify — Confirm fix and no regressions

  • Document — Add test to prevent recurrence

Debug Logging

// Temporary debug logging (remove before commit) console.log('[DEBUG] Input:', JSON.stringify(input, null, 2)); console.log('[DEBUG] State before:', { ...state }); // ... operation console.log('[DEBUG] State after:', { ...state });

Binary Search Debugging

// Comment out half the code to isolate issue // If bug persists: problem in remaining half // If bug disappears: problem in commented half // Repeat until isolated

Performance Testing

Load Testing with k6

import http from 'k6/http'; import { check, sleep } from 'k6';

export const options = { stages: [ { duration: '1m', target: 50 }, // Ramp up { duration: '3m', target: 50 }, // Steady state { duration: '1m', target: 100 }, // Peak { duration: '1m', target: 0 }, // Ramp down ], thresholds: { http_req_duration: ['p(95)<500'], // 95% under 500ms http_req_failed: ['rate<0.01'], // <1% errors }, };

export default function () { const res = http.get('https://api.example.com/products'); check(res, { 'status is 200': (r) => r.status === 200, 'response time < 500ms': (r) => r.timings.duration < 500, }); sleep(1); }

Accessibility Testing

Automated 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(); });

Manual Checklist

  • Keyboard navigation works (Tab, Enter, Escape)

  • Focus indicators visible

  • Screen reader announces content correctly

  • Color contrast meets WCAG AA (4.5:1)

  • Form inputs have associated labels

  • Images have alt text

  • Error messages are announced

Common Anti-Patterns to Avoid

❌ Testing implementation details

// Bad: Testing internal state expect(component.state.isLoading).toBe(true);

// Good: Testing observable behavior expect(screen.getByRole('progressbar')).toBeInTheDocument();

❌ Flaky tests

// Bad: Time-dependent expect(Date.now() - startTime).toBeLessThan(100);

// Good: Mock time jest.useFakeTimers();

❌ Test interdependence

// Bad: Tests share state let counter = 0; it('test 1', () => { counter++; }); it('test 2', () => { expect(counter).toBe(1); }); // Depends on test 1

// Good: Isolated tests beforeEach(() => { counter = 0; });

❌ Over-mocking

// Bad: Mock everything jest.mock('../db'); jest.mock('../cache'); jest.mock('../utils'); // Test proves nothing

// Good: Mock boundaries only jest.mock('../externalPaymentApi');

CI/CD Integration

.github/workflows/test.yml

name: Tests on: [push, pull_request]

jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'

  - run: npm ci
  - run: npm run lint
  - run: npm run typecheck
  - run: npm test -- --coverage
  
  - name: Upload coverage
    uses: codecov/codecov-action@v3
    with:
      files: ./coverage/lcov.info

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

skill-writer

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-native-expert

No summary provided by upstream source.

Repository SourceNeeds Review
General

rn-visual-testing

No summary provided by upstream source.

Repository SourceNeeds Review