DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 24: Advanced Testing Techniques

Advanced Testing Techniques

Introduction

As React applications grow in complexity, traditional testing approaches may not be sufficient to ensure quality and reliability. Advanced testing techniques provide deeper insights into application behavior, performance, and user experience. This chapter explores sophisticated testing methodologies that go beyond basic unit, integration, and E2E testing.

We’ll cover visual regression testing, performance testing, accessibility testing, API mocking strategies, test automation patterns, and advanced debugging techniques that will elevate your testing game to a professional level.

Visual Regression Testing

Visual regression testing ensures that UI changes don’t inadvertently break the visual appearance of your application. It compares screenshots of your application before and after changes to detect visual differences.

Setting Up Visual Regression Testing with Percy

Percy is a popular visual testing platform that integrates well with React applications:

npm install --save-dev @percy/cli @percy/selenium-webdriver

// tests/visual-regression.test.js
const { Builder } = require('selenium-webdriver');
const percySnapshot = require('@percy/selenium-webdriver');

describe('Visual Regression Tests', () => {
  let driver;

  beforeAll(async () => {
    driver = await new Builder().forBrowser('chrome').build();
  });

  afterAll(async () => {
    await driver.quit();
  });

  test('should capture homepage visual state', async () => {
    await driver.get('http://localhost:3000');
   
    // Wait for page to fully load
    await driver.sleep(2000);
   
    // Take Percy snapshot
    await percySnapshot(driver, 'Homepage');
  });

  test('should capture different viewport sizes', async () => {
    await driver.get('http://localhost:3000');
   
    // Desktop view
    await driver.manage().window().setRect({ width: 1280, height: 720 });
    await percySnapshot(driver, 'Homepage - Desktop', {
      widths: [1280]
    });
   
    // Tablet view
    await driver.manage().window().setRect({ width: 768, height: 1024 });
    await percySnapshot(driver, 'Homepage - Tablet', {
      widths: [768]
    });
   
    // Mobile view
    await driver.manage().window().setRect({ width: 375, height: 667 });
    await percySnapshot(driver, 'Homepage - Mobile', {
      widths: [375]
    });
  });

  test('should capture component states', async () => {
    await driver.get('http://localhost:3000/components/button');
   
    // Default state
    await percySnapshot(driver, 'Button - Default');
   
    // Hover state
    const button = await driver.findElement({ css: '[data-testid="primary-button"]' });
    await driver.actions().move({ origin: button }).perform();
    await percySnapshot(driver, 'Button - Hover');
   
    // Disabled state
    await driver.executeScript(
      'document.querySelector("[data-testid=\\"primary-button\\"]").disabled = true'
    );
    await percySnapshot(driver, 'Button - Disabled');
  });
});

Custom Visual Testing with Puppeteer

For more control, you can implement custom visual regression testing:

// tests/custom-visual-testing.js
const puppeteer = require('puppeteer');
const pixelmatch = require('pixelmatch');
const PNG = require('pngjs').PNG;
const fs = require('fs');
const path = require('path');

class VisualTester {
  constructor() {
    this.browser = null;
    this.page = null;
    this.screenshotsDir = path.join(__dirname, 'screenshots');
    this.baselineDir = path.join(this.screenshotsDir, 'baseline');
    this.actualDir = path.join(this.screenshotsDir, 'actual');
    this.diffDir = path.join(this.screenshotsDir, 'diff');
  }

  async setup() {
    // Ensure directories exist
    [this.baselineDir, this.actualDir, this.diffDir].forEach(dir => {
      if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
      }
    });

    this.browser = await puppeteer.launch({
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
    this.page = await this.browser.newPage();
  }

  async teardown() {
    if (this.browser) {
      await this.browser.close();
    }
  }

  async captureScreenshot(url, name, options = {}) {
    await this.page.goto(url, { waitUntil: 'networkidle0' });
   
    if (options.viewport) {
      await this.page.setViewport(options.viewport);
    }

    if (options.selector) {
      const element = await this.page.$(options.selector);
      return await element.screenshot({
        path: path.join(this.actualDir, `${name}.png`)
      });
    } else {
      return await this.page.screenshot({
        path: path.join(this.actualDir, `${name}.png`),
        fullPage: options.fullPage || false
      });
    }
  }

  async compareScreenshots(name, threshold = 0.1) {
    const baselinePath = path.join(this.baselineDir, `${name}.png`);
    const actualPath = path.join(this.actualDir, `${name}.png`);
    const diffPath = path.join(this.diffDir, `${name}.png`);

    if (!fs.existsSync(baselinePath)) {
      // First run - copy actual to baseline
      fs.copyFileSync(actualPath, baselinePath);
      return { isMatch: true, diffPixels: 0, message: 'Baseline created' };
    }

    const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
    const actual = PNG.sync.read(fs.readFileSync(actualPath));
    const diff = new PNG({ width: baseline.width, height: baseline.height });

    const diffPixels = pixelmatch(
      baseline.data,
      actual.data,
      diff.data,
      baseline.width,
      baseline.height,
      { threshold }
    );

    fs.writeFileSync(diffPath, PNG.sync.write(diff));

    const totalPixels = baseline.width * baseline.height;
    const diffPercentage = (diffPixels / totalPixels) * 100;

    return {
      isMatch: diffPercentage < threshold * 100,
      diffPixels,
      diffPercentage,
      message: `${diffPercentage.toFixed(2)}% pixels different`
    };
  }
}

// Usage in tests
describe('Custom Visual Regression Tests', () => {
  let visualTester;

  beforeAll(async () => {
    visualTester = new VisualTester();
    await visualTester.setup();
  });

  afterAll(async () => {
    await visualTester.teardown();
  });

  test('should not have visual regressions on homepage', async () => {
    await visualTester.captureScreenshot(
      'http://localhost:3000',
      'homepage',
      { viewport: { width: 1280, height: 720 } }
    );

    const result = await visualTester.compareScreenshots('homepage');
    expect(result.isMatch).toBe(true);
  });

  test('should not have visual regressions on modal component', async () => {
    await visualTester.captureScreenshot(
      'http://localhost:3000/modal-test',
      'modal-open',
      { selector: '[data-testid="modal"]' }
    );

    const result = await visualTester.compareScreenshots('modal-open');
    expect(result.isMatch).toBe(true);
  });
});

Performance Testing

Performance testing ensures your React application meets speed and responsiveness requirements under various conditions.

Lighthouse Performance Testing

// tests/performance.test.js
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

class PerformanceTester {
  constructor() {
    this.chrome = null;
    this.options = {
      logLevel: 'info',
      output: 'json',
      onlyCategories: ['performance'],
      port: undefined
    };
  }

  async setup() {
    this.chrome = await chromeLauncher.launch({
      chromeFlags: ['--headless', '--no-sandbox']
    });
    this.options.port = this.chrome.port;
  }

  async teardown() {
    if (this.chrome) {
      await this.chrome.kill();
    }
  }

  async auditPerformance(url) {
    const runnerResult = await lighthouse(url, this.options);
   
    return {
      score: runnerResult.lhr.categories.performance.score * 100,
      metrics: {
        firstContentfulPaint: runnerResult.lhr.audits['first-contentful-paint'].displayValue,
        largestContentfulPaint: runnerResult.lhr.audits['largest-contentful-paint'].displayValue,
        speedIndex: runnerResult.lhr.audits['speed-index'].displayValue,
        timeToInteractive: runnerResult.lhr.audits['interactive'].displayValue,
        totalBlockingTime: runnerResult.lhr.audits['total-blocking-time'].displayValue,
        cumulativeLayoutShift: runnerResult.lhr.audits['cumulative-layout-shift'].displayValue
      },
      opportunities: runnerResult.lhr.categories.performance.auditRefs
        .filter(audit => audit.group === 'load-opportunities')
        .map(audit => ({
          id: audit.id,
          title: runnerResult.lhr.audits[audit.id].title,
          description: runnerResult.lhr.audits[audit.id].description,
          score: runnerResult.lhr.audits[audit.id].score
        }))
    };
  }
}

describe('Performance Tests', () => {
  let performanceTester;

  beforeAll(async () => {
    performanceTester = new PerformanceTester();
    await performanceTester.setup();
  });

  afterAll(async () => {
    await performanceTester.teardown();
  });

  test('homepage should meet performance benchmarks', async () => {
    const results = await performanceTester.auditPerformance('http://localhost:3000');
   
    console.log('Performance Results:', results);
   
    // Assert performance thresholds
    expect(results.score).toBeGreaterThanOrEqual(75); // Lighthouse score >= 75
   
    // Check specific metrics
    const fcp = parseFloat(results.metrics.firstContentfulPaint);
    expect(fcp).toBeLessThanOrEqual(2.0); // FCP <= 2 seconds
   
    const lcp = parseFloat(results.metrics.largestContentfulPaint);
    expect(lcp).toBeLessThanOrEqual(4.0); // LCP <= 4 seconds
  });

  test('should not have major performance regressions', async () => {
    const beforeResults = await performanceTester.auditPerformance('http://localhost:3000');
   
    // Simulate some changes or different page
    const afterResults = await performanceTester.auditPerformance('http://localhost:3000/heavy-page');
   
    // Performance shouldn't degrade by more than 10 points
    const scoreDifference = beforeResults.score - afterResults.score;
    expect(scoreDifference).toBeLessThanOrEqual(10);
  });
});


Custom Performance Metrics

// tests/custom-performance.test.js
const puppeteer = require('puppeteer');

class CustomPerformanceTester {
  constructor() {
    this.browser = null;
    this.page = null;
  }

  async setup() {
    this.browser = await puppeteer.launch({ headless: true });
    this.page = await this.browser.newPage();
  }

  async teardown() {
    if (this.browser) {
      await this.browser.close();
    }
  }

  async measurePageLoad(url) {
    const metrics = await this.page.goto(url, { waitUntil: 'networkidle0' });
   
    const performanceMetrics = await this.page.evaluate(() => {
      const navigation = performance.getEntriesByType('navigation')[0];
      const paint = performance.getEntriesByType('paint');
     
      return {
        domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
        loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
        firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
        firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
        networkTime: navigation.responseEnd - navigation.requestStart,
        renderTime: navigation.loadEventEnd - navigation.responseEnd
      };
    });

    return performanceMetrics;
  }

  async measureComponentRender(selector, iterations = 10) {
    const renderTimes = [];

    for (let i = 0; i < iterations; i++) {
      const startTime = await this.page.evaluate(() => performance.now());
     
      // Trigger component re-render
      await this.page.click(selector);
     
      const endTime = await this.page.evaluate(() => performance.now());
      renderTimes.push(endTime - startTime);
     
      // Reset state if needed
      await this.page.reload({ waitUntil: 'networkidle0' });
    }

    return {
      average: renderTimes.reduce((a, b) => a + b) / renderTimes.length,
      min: Math.min(...renderTimes),
      max: Math.max(...renderTimes),
      median: renderTimes.sort((a, b) => a - b)[Math.floor(renderTimes.length / 2)]
    };
  }

  async measureMemoryUsage() {
    const metrics = await this.page.metrics();
   
    return {
      jsHeapUsedSize: metrics.JSHeapUsedSize,
      jsHeapTotalSize: metrics.JSHeapTotalSize,
      jsHeapUsedSizeFormatted: `${(metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2)} MB`,
      jsHeapTotalSizeFormatted: `${(metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2)} MB`
    };
  }
}

describe('Custom Performance Tests', () => {
  let performanceTester;

  beforeAll(async () => {
    performanceTester = new CustomPerformanceTester();
    await performanceTester.setup();
  });

  afterAll(async () => {
    await performanceTester.teardown();
  });

  test('should load page within acceptable time limits', async () => {
    const metrics = await performanceTester.measurePageLoad('http://localhost:3000');
   
    console.log('Page Load Metrics:', metrics);
   
    expect(metrics.domContentLoaded).toBeLessThan(1000); // < 1 second
    expect(metrics.firstContentfulPaint).toBeLessThan(2000); // < 2 seconds
  });

  test('should render components efficiently', async () => {
    await performanceTester.page.goto('http://localhost:3000/performance-test');
   
    const renderMetrics = await performanceTester.measureComponentRender(
      '[data-testid="heavy-component-trigger"]',
      5
    );
   
    console.log('Component Render Metrics:', renderMetrics);
   
    expect(renderMetrics.average).toBeLessThan(100); // < 100ms average
    expect(renderMetrics.max).toBeLessThan(200); // < 200ms worst case
  });

  test('should not have memory leaks', async () => {
    await performanceTester.page.goto('http://localhost:3000');
   
    const initialMemory = await performanceTester.measureMemoryUsage();
   
    // Perform operations that might cause memory leaks
    for (let i = 0; i < 10; i++) {
      await performanceTester.page.click('[data-testid="create-components"]');
      await performanceTester.page.click('[data-testid="destroy-components"]');
    }
   
    // Force garbage collection
    await performanceTester.page.evaluate(() => {
      if (window.gc) window.gc();
    });
   
    const finalMemory = await performanceTester.measureMemoryUsage();
   
    console.log('Memory Usage:', { initial: initialMemory, final: finalMemory });
   
    // Memory shouldn't increase by more than 50%
    const memoryIncrease = (finalMemory.jsHeapUsedSize - initialMemory.jsHeapUsedSize) / initialMemory.jsHeapUsedSize;
    expect(memoryIncrease).toBeLessThan(0.5);
  });
});

Accessibility Testing

Automated accessibility testing ensures your React application is usable by people with disabilities.

Setting Up Accessibility Testing with axe-core

npm install --save-dev @axe-core/webdriverjs
// tests/accessibility.test.js
const { Builder } = require('selenium-webdriver');
const AxeBuilder = require('@axe-core/webdriverjs');

class AccessibilityTester {
  constructor() {
    this.driver = null;
  }

  async setup() {
    this.driver = await new Builder().forBrowser('chrome').build();
  }

  async teardown() {
    if (this.driver) {
      await this.driver.quit();
    }
  }

  async runAccessibilityAudit(url, options = {}) {
    await this.driver.get(url);
   
    let axeBuilder = new AxeBuilder(this.driver);
   
    if (options.include) {
      axeBuilder = axeBuilder.include(options.include);
    }
   
    if (options.exclude) {
      axeBuilder = axeBuilder.exclude(options.exclude);
    }
   
    if (options.tags) {
      axeBuilder = axeBuilder.withTags(options.tags);
    }

    const results = await axeBuilder.analyze();
   
    return {
      violations: results.violations,
      passes: results.passes,
      incomplete: results.incomplete,
      summary: {
        violationCount: results.violations.length,
        passCount: results.passes.length,
        incompleteCount: results.incomplete.length
      }
    };
  }

  formatViolations(violations) {
    return violations.map(violation => ({
      id: violation.id,
      impact: violation.impact,
      description: violation.description,
      help: violation.help,
      helpUrl: violation.helpUrl,
      nodes: violation.nodes.map(node => ({
        html: node.html,
        target: node.target,
        failureSummary: node.failureSummary
      }))
    }));
  }
}

describe('Accessibility Tests', () => {
  let accessibilityTester;

  beforeAll(async () => {
    accessibilityTester = new AccessibilityTester();
    await accessibilityTester.setup();
  });

  afterAll(async () => {
    await accessibilityTester.teardown();
  });

  test('homepage should have no accessibility violations', async () => {
    const results = await accessibilityTester.runAccessibilityAudit('http://localhost:3000');
   
    if (results.violations.length > 0) {
      console.log('Accessibility Violations:',
        accessibilityTester.formatViolations(results.violations)
      );
    }
   
    expect(results.violations).toHaveLength(0);
  });

  test('should meet WCAG 2.1 AA standards', async () => {
    const results = await accessibilityTester.runAccessibilityAudit(
      'http://localhost:3000',
      { tags: ['wcag2a', 'wcag2aa', 'wcag21aa'] }
    );
   
    expect(results.violations).toHaveLength(0);
  });

  test('form should be accessible', async () => {
    const results = await accessibilityTester.runAccessibilityAudit(
      'http://localhost:3000/contact',
      { include: ['form'] }
    );
   
    // Check for specific form-related violations
    const formViolations = results.violations.filter(v =>
      v.id.includes('label') || v.id.includes('form') || v.id.includes('input')
    );
   
    expect(formViolations).toHaveLength(0);
  });

  test('should handle keyboard navigation', async () => {
    await accessibilityTester.driver.get('http://localhost:3000');
   
    // Test tab navigation
    const body = await accessibilityTester.driver.findElement({ css: 'body' });
   
    // Tab through interactive elements
    for (let i = 0; i < 10; i++) {
      await body.sendKeys('\t');
     
      const activeElement = await accessibilityTester.driver.executeScript(
        'return document.activeElement.tagName + (document.activeElement.id ? "#" + document.activeElement.id : "")'
      );
     
      console.log(`Tab ${i + 1}: ${activeElement}`);
    }
   
    // Test Enter key on buttons
    const button = await accessibilityTester.driver.findElement({
      css: '[data-testid="primary-button"]'
    });
    await button.sendKeys('\n');
   
    // Verify button action occurred
    // Add specific assertions based on your button behavior
  });
});

Custom Accessibility Helpers

// utils/accessibility-helpers.js
class AccessibilityHelpers {
  constructor(driver) {
    this.driver = driver;
  }

  async checkColorContrast() {
    const contrastResults = await this.driver.executeScript(`
      function getContrast(element) {
        const styles = window.getComputedStyle(element);
        const bgColor = styles.backgroundColor;
        const textColor = styles.color;
       
        // Simple contrast calculation (simplified)
        function getLuminance(rgb) {
          const [r, g, b] = rgb.match(/\\d+/g).map(Number);
          return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
        }
       
        const bgLum = getLuminance(bgColor);
        const textLum = getLuminance(textColor);
       
        const contrast = (Math.max(bgLum, textLum) + 0.05) / (Math.min(bgLum, textLum) + 0.05);
       
        return {
          element: element.tagName + (element.id ? '#' + element.id : ''),
          contrast: contrast,
          meetsAA: contrast >= 4.5,
          meetsAAA: contrast >= 7
        };
      }
     
      const textElements = document.querySelectorAll('p, span, div, h1, h2, h3, h4, h5, h6, a, button');
      return Array.from(textElements).map(getContrast);
    `);

    return contrastResults;
  }

  async checkAriaLabels() {
    const ariaResults = await this.driver.executeScript(`
      const interactiveElements = document.querySelectorAll(
        'button, a, input, select, textarea, [role="button"], [role="link"], [tabindex]'
      );
     
      return Array.from(interactiveElements).map(element => {
        const hasAriaLabel = element.hasAttribute('aria-label');
        const hasAriaLabelledBy = element.hasAttribute('aria-labelledby');
        const hasVisibleText = element.textContent.trim().length > 0;
        const hasAltText = element.hasAttribute('alt');
       
        return {
          element: element.tagName + (element.id ? '#' + element.id : ''),
          hasAccessibleName: hasAriaLabel || hasAriaLabelledBy || hasVisibleText || hasAltText,
          attributes: {
            'aria-label': element.getAttribute('aria-label'),
            'aria-labelledby': element.getAttribute('aria-labelledby'),
            'alt': element.getAttribute('alt'),
            'title': element.getAttribute('title')
          }
        };
      });
    `);

    return ariaResults;
  }

  async checkFocusManagement() {
    const focusResults = await this.driver.executeScript(`
      const focusableElements = document.querySelectorAll(
        'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
     
      return Array.from(focusableElements).map((element, index) => {
        const computedStyle = window.getComputedStyle(element);
        const isVisible = computedStyle.display !== 'none' &&
                        computedStyle.visibility !== 'hidden' &&
                        computedStyle.opacity !== '0';
       
        return {
          index,
          element: element.tagName + (element.id ? '#' + element.id : ''),
          tabIndex: element.tabIndex,
          isVisible,
          isFocusable: !element.disabled && isVisible
        };
      });
    `);

    return focusResults;
  }
}

module.exports = AccessibilityHelpers;

Advanced API Mocking and Testing

Mock Service Worker (MSW) for Advanced API Mocking

npm install --save-dev msw
// mocks/handlers.js
import { rest } from 'msw';

export const handlers = [
  // Success responses
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, name: 'John Doe', email: 'john@example.com' },
        { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
      ])
    );
  }),

  // Error responses
  rest.post('/api/users', (req, res, ctx) => {
    const { name, email } = req.body;
   
    if (!name || !email) {
      return res(
        ctx.status(400),
        ctx.json({ error: 'Name and email are required' })
      );
    }

    return res(
      ctx.status(201),
      ctx.json({ id: 3, name, email })
    );
  }),

  // Delayed responses for testing loading states
  rest.get('/api/slow-endpoint', (req, res, ctx) => {
    return res(
      ctx.delay(2000),
      ctx.status(200),
      ctx.json({ message: 'This response was delayed' })
    );
  }),

  // Dynamic responses based on request parameters
  rest.get('/api/users/:id', (req, res, ctx) => {
    const { id } = req.params;
   
    if (id === '404') {
      return res(
        ctx.status(404),
        ctx.json({ error: 'User not found' })
      );
    }

    return res(
      ctx.status(200),
      ctx.json({ id: parseInt(id), name: `User ${id}`, email: `user${id}@example.com` })
    );
  }),

  // Simulate network errors
  rest.get('/api/network-error', (req, res, ctx) => {
    return res.networkError('Failed to connect');
  })
];

// mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

// tests/api-integration.test.js
import { server } from '../mocks/server';
import { rest } from 'msw';

// Enable API mocking before tests
beforeAll(() => server.listen());

// Reset any request handlers that we may add during the tests
afterEach(() => server.resetHandlers());

// Clean up after the tests are finished
afterAll(() => server.close());

describe('API Integration Tests', () => {
  test('should handle successful API responses', async () => {
    // Test implementation using your React component
    // that makes API calls
  });

  test('should handle API errors gracefully', async () => {
    // Override the default handler for this test
    server.use(
      rest.get('/api/users', (req, res, ctx) => {
        return res(
          ctx.status(500),
          ctx.json({ error: 'Internal server error' })
        );
      })
    );

    // Test how your component handles the error
  });

  test('should handle loading states correctly', async () => {
    // Use the slow endpoint to test loading states
    server.use(
      rest.get('/api/users', (req, res, ctx) => {
        return res(
          ctx.delay(1000),
          ctx.json([])
        );
      })
    );

    // Test loading state behavior
  });
});

Test Automation Patterns

Advanced Test Setup and Teardown

// utils/test-environment.js
class TestEnvironment {
  constructor() {
    this.resources = [];
    this.cleanup = [];
  }

  async setupDatabase() {
    // Database setup logic
    const dbConnection = await this.createTestDatabase();
    this.resources.push(dbConnection);
    this.cleanup.push(() => dbConnection.close());
    return dbConnection;
  }

  async setupTestData() {
    // Create test data
    const testUsers = await this.createTestUsers();
    this.cleanup.push(() => this.cleanupTestUsers(testUsers));
    return testUsers;
  }

  async setupBrowser(options = {}) {
    const browser = await puppeteer.launch({
      headless: options.headless !== false,
      devtools: options.devtools || false,
      slowMo: options.slowMo || 0
    });
   
    this.resources.push(browser);
    this.cleanup.push(() => browser.close());
    return browser;
  }

  async teardownAll() {
    // Run cleanup in reverse order
    for (const cleanupFn of this.cleanup.reverse()) {
      try {
        await cleanupFn();
      } catch (error) {
        console.warn('Cleanup error:', error);
      }
    }
   
    this.resources.length = 0;
    this.cleanup.length = 0;
  }
}

// Global test setup
let testEnvironment;

beforeAll(async () => {
  testEnvironment = new TestEnvironment();
  // Setup shared resources
});

afterAll(async () => {
  if (testEnvironment) {
    await testEnvironment.teardownAll();
  }
});

Data-Driven Testing
// tests/data-driven.test.js
const testCases = [
  {
    name: 'valid email format',
    input: { email: 'test@example.com', password: 'password123' },
    expected: { success: true }
  },
  {
    name: 'invalid email format',
    input: { email: 'invalid-email', password: 'password123' },
    expected: { success: false, error: 'Invalid email format' }
  },
  {
    name: 'short password',
    input: { email: 'test@example.com', password: '123' },
    expected: { success: false, error: 'Password must be at least 8 characters' }
  },
  {
    name: 'empty fields',
    input: { email: '', password: '' },
    expected: { success: false, error: 'All fields are required' }
  }
];

describe('Login Form Validation', () => {
  let driver;

  beforeAll(async () => {
    driver = await new Builder().forBrowser('chrome').build();
  });

  afterAll(async () => {
    await driver.quit();
  });

  testCases.forEach(({ name, input, expected }) => {
    test(`should handle ${name}`, async () => {
      await driver.get('http://localhost:3000/login');
     
      // Fill form
      const emailInput = await driver.findElement(By.css('[data-testid="email-input"]'));
      await emailInput.clear();
      await emailInput.sendKeys(input.email);
     
      const passwordInput = await driver.findElement(By.css('[data-testid="password-input"]'));
      await passwordInput.clear();
      await passwordInput.sendKeys(input.password);
     
      // Submit form
      const submitButton = await driver.findElement(By.css('[data-testid="submit-button"]'));
      await submitButton.click();
     
      if (expected.success) {
        // Wait for success redirect or message
        await driver.wait(
          until.urlContains('/dashboard'),
          5000
        );
      } else {
        // Wait for error message
        const errorElement = await driver.wait(
          until.elementLocated(By.css('[data-testid="error-message"]')),
          3000
        );
        const errorText = await errorElement.getText();
        expect(errorText).toContain(expected.error);
      }
    });
  });
});

// CSV-based data-driven testing
const fs = require('fs');
const csv = require('csv-parser');

async function loadTestDataFromCSV(filePath) {
  return new Promise((resolve, reject) => {
    const results = [];
    fs.createReadStream(filePath)
      .pipe(csv())
      .on('data', (data) => results.push(data))
      .on('end', () => resolve(results))
      .on('error', reject);
  });
}

describe('CSV Data-Driven Tests', () => {
  let testData;

  beforeAll(async () => {
    testData = await loadTestDataFromCSV('./test-data/user-scenarios.csv');
  });

  testData?.forEach((scenario, index) => {
    test(`CSV Scenario ${index + 1}: ${scenario.description}`, async () => {
      // Execute test based on CSV data
      // scenario.username, scenario.password, scenario.expectedResult, etc.
    });
  });
});

Property-Based Testing

Property-based testing generates random test data to verify that certain properties hold true across a wide range of inputs:

// tests/property-based.test.js
const fc = require('fast-check');

// Test utility functions
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

function sanitizeInput(input) {
  return input.trim().replace(/[<>]/g, '');
}

describe('Property-Based Tests', () => {
  test('email validation should reject invalid formats', () => {
    fc.assert(
      fc.property(fc.string(), (randomString) => {
        // If the random string doesn't look like an email, it should be invalid
        if (!randomString.includes('@') || !randomString.includes('.')) {
          expect(isValidEmail(randomString)).toBe(false);
        }
      })
    );
  });

  test('input sanitization should remove dangerous characters', () => {
    fc.assert(
      fc.property(fc.string(), (input) => {
        const sanitized = sanitizeInput(input);
        expect(sanitized).not.toContain('<');
        expect(sanitized).not.toContain('>');
      })
    );
  });

  test('React component should handle any valid props', () => {
    fc.assert(
      fc.property(
        fc.record({
          title: fc.string({ minLength: 1, maxLength: 100 }),
          count: fc.integer({ min: 0, max: 1000 }),
          isVisible: fc.boolean()
        }),
        (props) => {
          // Test that component renders without error
          const { render } = require('@testing-library/react');
          const MyComponent = require('../components/MyComponent');
         
          expect(() => {
            render(<MyComponent {...props} />);
          }).not.toThrow();
        }
      )
    );
  });
});


Test Reporting and Analytics

Custom Test Reporter

// utils/custom-reporter.js
class CustomTestReporter {
  constructor() {
    this.results = {
      startTime: Date.now(),
      tests: [],
      summary: {
        total: 0,
        passed: 0,
        failed: 0,
        skipped: 0
      }
    };
  }

  onTestStart(test) {
    console.log(`🏃 Running: ${test.title}`);
  }

  onTestPass(test, duration) {
    this.results.tests.push({
      title: test.title,
      status: 'passed',
      duration,
      error: null
    });
    this.results.summary.passed++;
    console.log(`✅ Passed: ${test.title} (${duration}ms)`);
  }

  onTestFail(test, error, duration) {
    this.results.tests.push({
      title: test.title,
      status: 'failed',
      duration,
      error: error.message,
      stack: error.stack
    });
    this.results.summary.failed++;
    console.log(`❌ Failed: ${test.title} (${duration}ms)`);
    console.log(`   Error: ${error.message}`);
  }

  onTestSkip(test) {
    this.results.tests.push({
      title: test.title,
      status: 'skipped',
      duration: 0,
      error: null
    });
    this.results.summary.skipped++;
    console.log(`⏭️  Skipped: ${test.title}`);
  }

  onTestComplete() {
    this.results.endTime = Date.now();
    this.results.totalDuration = this.results.endTime - this.results.startTime;
    this.results.summary.total = this.results.tests.length;

    this.generateReport();
  }

  generateReport() {
    const report = {
      timestamp: new Date().toISOString(),
      duration: this.results.totalDuration,
      summary: this.results.summary,
      tests: this.results.tests,
      coverage: this.getCoverageData(),
      performance: this.getPerformanceMetrics()
    };

    // Save to file
    const fs = require('fs');
    fs.writeFileSync(
      `./reports/test-report-${Date.now()}.json`,
      JSON.stringify(report, null, 2)
    );

    // Generate HTML report
    this.generateHTMLReport(report);

    // Print summary
    this.printSummary();
  }

  getCoverageData() {
    // Integration with coverage tools like Istanbul/NYC
    try {
      const coverage = global.__coverage__;
      return coverage ? this.processCoverageData(coverage) : null;
    } catch (error) {
      return null;
    }
  }

  getPerformanceMetrics() {
    const testDurations = this.results.tests.map(t => t.duration);
    return {
      averageTestDuration: testDurations.reduce((a, b) => a + b, 0) / testDurations.length,
      slowestTest: Math.max(...testDurations),
      fastestTest: Math.min(...testDurations)
    };
  }

  generateHTMLReport(report) {
    const html = `
<!DOCTYPE html>
<html>
<head>
    <title>Test Report - ${report.timestamp}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .summary { background: #f5f5f5; padding: 20px; border-radius: 5px; }
        .passed { color: green; }
        .failed { color: red; }
        .skipped { color: orange; }
        .test-item { margin: 10px 0; padding: 10px; border-left: 4px solid #ddd; }
        .test-item.passed { border-left-color: green; }
        .test-item.failed { border-left-color: red; }
        .test-item.skipped { border-left-color: orange; }
        .error { background: #fee; padding: 10px; margin: 10px 0; border-radius: 3px; }
    </style>
</head>
<body>
    <h1>Test Report</h1>
    <div class="summary">
        <h2>Summary</h2>
        <p>Total: ${report.summary.total}</p>
        <p class="passed">Passed: ${report.summary.passed}</p>
        <p class="failed">Failed: ${report.summary.failed}</p>
        <p class="skipped">Skipped: ${report.summary.skipped}</p>
        <p>Duration: ${report.duration}ms</p>
    </div>
   
    <h2>Test Results</h2>
    ${report.tests.map(test => `
        <div class="test-item ${test.status}">
            <h3>${test.title}</h3>
            <p>Status: <span class="${test.status}">${test.status}</span></p>
            <p>Duration: ${test.duration}ms</p>
            ${test.error ? `<div class="error">Error: ${test.error}</div>` : ''}
        </div>
    `).join('')}
</body>
</html>`;

    const fs = require('fs');
    fs.writeFileSync(`./reports/test-report-${Date.now()}.html`, html);
  }

  printSummary() {
    console.log('\n📊 Test Summary:');
    console.log(`   Total: ${this.results.summary.total}`);
    console.log(`   ✅ Passed: ${this.results.summary.passed}`);
    console.log(`   ❌ Failed: ${this.results.summary.failed}`);
    console.log(`   ⏭️  Skipped: ${this.results.summary.skipped}`);
    console.log(`   ⏱️  Duration: ${this.results.totalDuration}ms`);
  }
}

module.exports = CustomTestReporter;

Advanced Debugging Techniques

Debug Mode Testing

// utils/debug-helpers.js
class DebugHelpers {
  constructor(driver, options = {}) {
    this.driver = driver;
    this.debugMode = options.debugMode || process.env.DEBUG_TESTS;
    this.screenshotDir = options.screenshotDir || './debug-screenshots';
  }

  async debugLog(message) {
    if (this.debugMode) {
      console.log(`🐛 DEBUG: ${message}`);
    }
  }

  async takeDebugScreenshot(name) {
    if (this.debugMode) {
      const screenshot = await this.driver.takeScreenshot();
      const fs = require('fs');
      const path = require('path');
     
      if (!fs.existsSync(this.screenshotDir)) {
        fs.mkdirSync(this.screenshotDir, { recursive: true });
      }
     
      const filename = `${name}-${Date.now()}.png`;
      const filepath = path.join(this.screenshotDir, filename);
     
      fs.writeFileSync(filepath, screenshot, 'base64');
      console.log(`📸 Debug screenshot saved: ${filepath}`);
    }
  }

  async logPageState() {
    if (this.debugMode) {
      const pageInfo = await this.driver.executeScript(`
        return {
          url: window.location.href,
          title: document.title,
          readyState: document.readyState,
          elementCount: document.querySelectorAll('*').length,
          errors: window.jsErrors || []
        };
      `);
     
      console.log('📄 Page State:', pageInfo);
    }
  }

  async logNetworkRequests() {
    if (this.debugMode) {
      const requests = await this.driver.executeScript(`
        return window.networkRequests || [];
      `);
     
      console.log('🌐 Network Requests:', requests);
    }
  }

  async pauseForInspection(duration = 5000) {
    if (this.debugMode) {
      console.log(`⏸️  Pausing for inspection (${duration}ms)...`);
      await this.driver.sleep(duration);
    }
  }

  async enableConsoleCapture() {
    await this.driver.executeScript(`
      window.jsErrors = [];
      window.jsLogs = [];
     
      // Capture JavaScript errors
      window.addEventListener('error', function(e) {
        window.jsErrors.push({
          message: e.message,
          filename: e.filename,
          lineno: e.lineno,
          stack: e.error ? e.error.stack : null
        });
      });
     
      // Capture console logs
      const originalLog = console.log;
      console.log = function(...args) {
        window.jsLogs.push(args.join(' '));
        originalLog.apply(console, args);
      };
    `);
  }

  async getConsoleLogs() {
    return await this.driver.executeScript(`
      return {
        errors: window.jsErrors || [],
        logs: window.jsLogs || []
      };
    `);
  }
}

// Usage in tests
describe('Debug-enabled Tests', () => {
  let driver;
  let debugHelper;

  beforeAll(async () => {
    driver = await new Builder().forBrowser('chrome').build();
    debugHelper = new DebugHelpers(driver, { debugMode: true });
    await debugHelper.enableConsoleCapture();
  });

  afterAll(async () => {
    await driver.quit();
  });

  test('should debug complex user interaction', async () => {
    await debugHelper.debugLog('Starting complex interaction test');
   
    await driver.get('http://localhost:3000/complex-form');
    await debugHelper.takeDebugScreenshot('initial-load');
    await debugHelper.logPageState();
   
    // Perform actions with debug info
    const emailInput = await driver.findElement(By.css('[data-testid="email"]'));
    await emailInput.sendKeys('test@example.com');
    await debugHelper.debugLog('Email entered');
   
    await debugHelper.takeDebugScreenshot('email-entered');
   
    const submitButton = await driver.findElement(By.css('[data-testid="submit"]'));
    await submitButton.click();
    await debugHelper.debugLog('Form submitted');
   
    await debugHelper.pauseForInspection(2000);
    await debugHelper.takeDebugScreenshot('after-submit');
   
    const consoleLogs = await debugHelper.getConsoleLogs();
    await debugHelper.debugLog(`Console logs: ${JSON.stringify(consoleLogs)}`);
   
    // Verify result
    const successMessage = await driver.wait(
      until.elementLocated(By.css('[data-testid="success"]')),
      5000
    );
   
    expect(await successMessage.getText()).toContain('Success');
  });
});

Parallel Testing and Test Optimization

Parallel Test Execution

// utils/parallel-runner.js
const { Worker } = require('worker_threads');
const os = require('os');

class ParallelTestRunner {
  constructor(options = {}) {
    this.maxWorkers = options.maxWorkers || Math.min(os.cpus().length, 4);
    this.testFiles = options.testFiles || [];
    this.results = [];
  }

  async runTests() {
    const chunks = this.chunkArray(this.testFiles, this.maxWorkers);
    const workers = [];

    for (let i = 0; i < chunks.length; i++) {
      const worker = new Worker('./test-worker.js', {
        workerData: {
          testFiles: chunks[i],
          workerId: i
        }
      });

      workers.push(this.runWorker(worker));
    }

    const results = await Promise.all(workers);
    return this.combineResults(results);
  }

  async runWorker(worker) {
    return new Promise((resolve, reject) => {
      worker.on('message', resolve);
      worker.on('error', reject);
      worker.on('exit', (code) => {
        if (code !== 0) {
          reject(new Error(`Worker stopped with exit code ${code}`));
        }
      });
    });
  }

  chunkArray(array, chunkSize) {
    const chunks = [];
    for (let i = 0; i < array.length; i += chunkSize) {
      chunks.push(array.slice(i, i + chunkSize));
    }
    return chunks;
  }

  combineResults(results) {
    return results.reduce((combined, result) => ({
      tests: [...combined.tests, ...result.tests],
      passed: combined.passed + result.passed,
      failed: combined.failed + result.failed,
      duration: Math.max(combined.duration, result.duration)
    }), { tests: [], passed: 0, failed: 0, duration: 0 });
  }
}

// test-worker.js
const { parentPort, workerData } = require('worker_threads');
const { runTestFiles } = require('./test-runner');

async function main() {
  try {
    const results = await runTestFiles(workerData.testFiles, workerData.workerId);
    parentPort.postMessage(results);
  } catch (error) {
    parentPort.postMessage({ error: error.message });
  }
}

main();

Summary

Advanced testing techniques provide comprehensive coverage and insights that go beyond basic testing approaches. This chapter covered:

Visual Regression Testing: Ensuring UI consistency across changes using tools like Percy and custom screenshot comparison solutions.

Performance Testing: Measuring and validating application performance using Lighthouse automation and custom metrics collection.

Accessibility Testing: Automated accessibility auditing with axe-core and custom accessibility validation helpers.

Advanced API Mocking: Sophisticated API testing scenarios using Mock Service Worker with dynamic responses and error simulation.

Test Automation Patterns: Reusable patterns for test setup, data-driven testing, and property-based testing approaches.

Test Reporting and Analytics: Custom test reporting solutions with detailed analytics and HTML report generation.

Advanced Debugging: Debug-enabled testing with screenshot capture, console log collection, and inspection utilities.

Parallel Testing: Performance optimization through parallel test execution and worker-based test distribution.

These advanced techniques enable you to build robust, maintainable test suites that provide confidence in your React application’s quality, performance, and user experience. The combination of these approaches creates a comprehensive testing strategy that catches issues early and ensures consistent application behavior across all scenarios.

In the next chapter, we’ll explore debugging React applications, including advanced debugging techniques and tools for identifying and resolving complex issues in React applications.

Related Articles

Scroll to Top