Testing Strategies
A testing strategy is a detailed plan that defines how tests should be designed, implemented, and executed to validate the proper functioning of a system or application. It defines the objectives, scenarios, test cases, and tools needed to ensure the system's quality.
Why a Testing Strategy?
Objectives
- Functionality Validation - Ensure the application works as expected
- Early Bug Detection - Identify problems before production
- Deployment Confidence - Enable automated deployments
- Living Documentation - Tests serve as specifications
- Regression Prevention - Avoid reintroducing bugs
📋 Main Steps
- Define Objectives - System validation criteria
- Planning - Scenarios, test cases, environments
- Design - Scripts, tools, implementation
- Execution - Launch tests and collect results
- Analysis - Evaluate results and identify defects
- Anomaly Management - Correction, retest, documentation
Types of Tests
General Classification
Static Tests (Without execution)
- Code Analysis - Linters, static analyzers
- Code Review - Peer review
- Quality Analysis - SonarQube, ESLint
Dynamic Tests (With execution)
- Functional Tests - Functionality validation
- Non-Functional Tests - Performance, security, compatibility
Test Pyramid
/\
/ \
/ E2E \ ← Few, slow, expensive
/______\
/ \
/Integration\ ← Moderately numerous
/____________\
/ \
/ Unit Tests \ ← Very numerous, fast, inexpensive
/________________\
🧪 Unit Tests
Tests of the smallest units of code (functions, methods, components).
// Example with Jest
describe('Calculator', () => {
test('should add two numbers correctly', () => {
const result = add(2, 3)
expect(result).toBe(5)
})
test('should handle negative numbers', () => {
const result = add(-2, 3)
expect(result).toBe(1)
})
test('should throw error for invalid input', () => {
expect(() => add('a', 2)).toThrow('Invalid input')
})
})
// React Testing Library
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Button from './Button'
test('should call onClick when clicked', async () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
const button = screen.getByRole('button', { name: /click me/i })
await userEvent.click(button)
expect(handleClick).toHaveBeenCalledTimes(1)
})
Integration Tests
Tests of the interaction between different modules or services.
// API Test with Supertest
import request from 'supertest'
import app from '../app'
describe('User API', () => {
test('POST /users should create a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
}
const response = await request(app)
.post('/users')
.send(userData)
.expect(201)
expect(response.body).toMatchObject({
id: expect.any(Number),
name: 'John Doe',
email: 'john@example.com'
})
})
})
// React Integration Test
test('should display user data after fetch', async () => {
// Mock API response
server.use(
rest.get('/api/users/1', (req, res, ctx) => {
return res(ctx.json({ id: 1, name: 'John Doe' }))
})
)
render(<UserProfile userId={1} />)
expect(await screen.findByText('John Doe')).toBeInTheDocument()
})
End-to-End (E2E) Tests
Complete tests of the user journey in the real application.
// Cypress
describe('User Login Flow', () => {
it('should allow user to login and access dashboard', () => {
cy.visit('/login')
cy.get('[data-testid=email]').type('user@example.com')
cy.get('[data-testid=password]').type('password123')
cy.get('[data-testid=login-button]').click()
cy.url().should('include', '/dashboard')
cy.get('[data-testid=welcome-message]')
.should('contain', 'Welcome back!')
})
})
// Playwright
import { test, expect } from '@playwright/test'
test('user can complete purchase', async ({ page }) => {
await page.goto('/products')
await page.click('[data-testid=add-to-cart]')
await page.click('[data-testid=cart-icon]')
await page.click('[data-testid=checkout-button]')
await page.fill('[data-testid=card-number]', '4242424242424242')
await page.click('[data-testid=pay-button]')
await expect(page.locator('[data-testid=success-message]'))
.toContainText('Payment successful')
})
Testing Approaches
Black Box Testing
Testing without knowledge of the internal code (API, user interface).
// API test without knowing the implementation
describe('Authentication API', () => {
test('should return 401 for invalid credentials', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: 'wrong@example.com',
password: 'wrongpassword'
})
expect(response.status).toBe(401)
expect(response.body.error).toBe('Invalid credentials')
})
})
White Box Testing
Testing with complete knowledge of the source code.
// Test with mock of internal dependencies
jest.mock('../services/emailService')
test('should call email service when user registers', async () => {
const mockSendEmail = jest.fn()
emailService.sendWelcomeEmail = mockSendEmail
await userService.registerUser({
email: 'test@example.com',
password: 'password123'
})
expect(mockSendEmail).toHaveBeenCalledWith('test@example.com')
})
Specialized Types of Tests
🎭 Property-Based Testing
// With fast-check
import fc from 'fast-check'
test('reverse of reverse should be identity', () => {
fc.assert(fc.property(
fc.array(fc.integer()),
(arr) => {
const reversed = reverse(reverse(arr))
expect(reversed).toEqual(arr)
}
))
})
💨 Smoke Testing
Basic tests to verify that the application starts.
describe('Smoke Tests', () => {
test('app should start without crashing', () => {
render(<App />)
expect(screen.getByTestId('app-container')).toBeInTheDocument()
})
test('main routes should be accessible', async () => {
const routes = ['/', '/about', '/contact']
for (const route of routes) {
const response = await request(app).get(route)
expect(response.status).toBe(200)
}
})
})
🧠 Sanity Testing
Quick tests after deployment to verify critical functionalities.
describe('Sanity Tests', () => {
test('critical user journey works', async () => {
// Login
await page.goto('/login')
await page.fill('[data-testid=email]', 'test@example.com')
await page.fill('[data-testid=password]', 'password')
await page.click('[data-testid=login-button]')
// Navigate to main feature
await page.click('[data-testid=main-feature]')
// Verify it loads
await expect(page.locator('[data-testid=feature-content]'))
.toBeVisible()
})
})
👥 User Acceptance Testing (UAT)
// Gherkin with Cucumber
Feature: User Registration
As a new user
I want to register an account
So that I can access the application
Scenario: Successful registration
Given I am on the registration page
When I fill in "Email" with "user@example.com"
And I fill in "Password" with "securepassword"
And I click "Register"
Then I should see "Registration successful"
And I should be redirected to the dashboard
🔥 Fuzz Testing
// Test with random data
describe('Fuzz Testing', () => {
test('API should handle malformed input gracefully', async () => {
const malformedInputs = [
null,
undefined,
'',
'<script>alert("xss")</script>',
'A'.repeat(10000),
{ malformed: { deeply: { nested: 'object' } } }
]
for (const input of malformedInputs) {
const response = await request(app)
.post('/api/data')
.send(input)
// Should not crash (500) but handle gracefully
expect(response.status).not.toBe(500)
}
})
})
Configuration and Tools
⚙️ Jest Configuration
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1'
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/index.js',
'!src/**/*.d.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
]
}
🎯 Cypress Configuration
// cypress.config.js
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.js',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
video: false,
screenshotOnRunFailure: true,
setupNodeEvents(on, config) {
// Plugins et tâches personnalisées
}
},
component: {
devServer: {
framework: 'react',
bundler: 'vite'
}
}
})
🎭 Playwright Configuration
// playwright.config.js
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './tests',
timeout: 30000,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 12'] } }
]
})
Advanced Strategies
📊 Code Coverage
// Configuration de seuils de couverture
{
"jest": {
"collectCoverage": true,
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
},
"./src/components/": {
"branches": 90,
"statements": 90
}
}
}
}
🔄 Test-Driven Development (TDD)
// 1. Write the test (which fails)
test('should calculate total price with tax', () => {
const items = [{ price: 100 }, { price: 200 }]
const total = calculateTotalWithTax(items, 0.2)
expect(total).toBe(360) // 300 + 20% tax
})
// 2. Write the minimum code to pass
function calculateTotalWithTax(items, taxRate) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0)
return subtotal * (1 + taxRate)
}
// 3. Refactor if necessary
🏭 Test Factories
// Factory to create test data
const userFactory = (overrides = {}) => ({
id: Math.random(),
name: 'John Doe',
email: 'john@example.com',
role: 'user',
...overrides
})
// Usage
test('admin user should have access', () => {
const admin = userFactory({ role: 'admin' })
expect(hasAccess(admin, 'admin-panel')).toBe(true)
})
🎯 Test Doubles (Mocks, Stubs, Spies)
// Complete mock
jest.mock('../services/apiService', () => ({
fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: 'John' }))
}))
// Spy on existing method
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
// Stub with return value
const mockFetch = jest.fn()
mockFetch.mockResolvedValueOnce({ ok: true, json: () => ({ data: 'test' }) })
Continuous Integration (CI)
🔄 GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
- name: Run E2E tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
📊 Quality Gates
// Scripts package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --testPathPattern=unit",
"test:integration": "jest --testPathPattern=integration",
"test:e2e": "cypress run",
"test:all": "npm run test:unit && npm run test:integration && npm run test:e2e",
"quality:check": "npm run lint && npm run test:coverage && npm run type-check",
"pre-commit": "lint-staged && npm run test:unit"
}
}
Tools and Ecosystem
🧪 Testing Frameworks
- Jest - Comprehensive framework for JavaScript
- Vitest - Fast alternative to Jest (Vite compatible)
- Mocha - Flexible framework with Chai/Sinon
- Jasmine - BDD framework
🌐 E2E Testing
- Cypress - Modern and intuitive framework
- Playwright - Multi-browser by Microsoft
- Puppeteer - Control of Chrome/Chromium
- Selenium - Historical standard
📊 Code Quality
- SonarQube - Comprehensive quality analysis
- ESLint - Linting JavaScript/TypeScript
- Prettier - Code formatting
- Husky - Git hooks for automation
🎯 Test Utilities
- React Testing Library - User-centric React tests
- Vue Test Utils - Utilities for testing Vue.js
- Supertest - HTTP API tests
- MSW - Mock Service Worker for APIs
Best Practices
✅ Do's
// ✅ Descriptive and readable tests
describe('UserService', () => {
describe('when creating a new user', () => {
test('should send welcome email to valid email address', () => {
// Test implementation
})
})
})
// ✅ Arrange, Act, Assert pattern
test('should calculate discount correctly', () => {
// Arrange
const price = 100
const discountPercent = 20
// Act
const finalPrice = applyDiscount(price, discountPercent)
// Assert
expect(finalPrice).toBe(80)
})
// ✅ Test edge cases
test('should handle edge cases', () => {
expect(divide(10, 0)).toBe(Infinity)
expect(divide(0, 10)).toBe(0)
expect(divide(-10, 2)).toBe(-5)
})
❌ Don'ts
// ❌ Tests that are too complex
test('should do everything', () => {
// 50 lines of code testing multiple things
})
// ❌ Fragile tests (implementation-dependent)
test('should call internal method', () => {
const spy = jest.spyOn(service, '_internalMethod')
service.publicMethod()
expect(spy).toHaveBeenCalled() // Fragile!
})
// ❌ Tests without assertions
test('should not crash', () => {
service.doSomething() // No assertion!
})
Recommended Strategy
🎯 Prioritization
- Unit Tests (70%) - Fast, reliable, good ROI
- Integration Tests (20%) - Critical points
- E2E Tests (10%) - Essential user journeys
📋 Quality Checklist
- Code Coverage > 80% on critical code
- Automated Tests in CI/CD
- Regression Tests for each bug fixed
- Performance Tests on critical APIs
- Security Tests on entry points
- Accessibility Tests on the user interface
🚀 Progressive Implementation
- Start Simple - Unit tests on pure functions
- Add Integration - Tests of APIs and components
- Automate - CI/CD with mandatory tests
- Optimize - Parallelization, cache, test selection
- Monitor - Quality metrics and execution time
Resources for Further Learning
- 📚 Jest Documentation
- 🌐 Cypress Documentation
- 🎭 Playwright Documentation
- 📖 React Testing Library
- 🔍 SonarQube
- 📊 ISTQB - Testing Standards
- 🥒 Cucumber/Gherkin
- 🎯 Test Pyramid
A good testing strategy is essential to maintain code quality and enable confident deployments. The goal is to find the right balance between coverage, execution speed, and test maintenance.