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

  1. Define Objectives - System validation criteria
  2. Planning - Scenarios, test cases, environments
  3. Design - Scripts, tools, implementation
  4. Execution - Launch tests and collect results
  5. Analysis - Evaluate results and identify defects
  6. 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!
})

🎯 Prioritization

  1. Unit Tests (70%) - Fast, reliable, good ROI
  2. Integration Tests (20%) - Critical points
  3. 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

  1. Start Simple - Unit tests on pure functions
  2. Add Integration - Tests of APIs and components
  3. Automate - CI/CD with mandatory tests
  4. Optimize - Parallelization, cache, test selection
  5. Monitor - Quality metrics and execution time

Resources for Further Learning

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.