Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Unit Testing React Components with Mocha

Three approaches for mocking LaunchDarkly React SDK hooks in Mocha tests.

Background

Mocha doesn’t include built-in module mocking like Jest. To test React components that use LaunchDarkly hooks (useFlags, useLDClient), you need to either:

  1. Mock the module using a library
  2. Inject dependencies as props
  3. Use a mock context provider

Each approach has tradeoffs in terms of code changes required and test complexity.

Approach 1: Monkey Patch Dependencies

Replace the LaunchDarkly module at import time using a mocking library. This allows testing components without any code modifications.

Using ESMock (ES Modules)

import esmock from "esmock";
import { render, screen, cleanup } from "@testing-library/react";
import { expect } from "chai";

describe("App with ESMock", () => {
  afterEach(() => {
    cleanup();
  });

  it("renders new header when simpleToggle is true", async () => {
    const { default: App } = await esmock("../src/App.jsx", {
      "launchdarkly-react-client-sdk": {
        useFlags: () => ({ simpleToggle: true })
      }
    });

    render(<App />);
    expect(screen.getByTestId("header").textContent).to.equal("New Header Experience");
  });

  it("renders legacy header when simpleToggle is false", async () => {
    const { default: App } = await esmock("../src/App.jsx", {
      "launchdarkly-react-client-sdk": {
        useFlags: () => ({ simpleToggle: false })
      }
    });

    render(<App />);
    expect(screen.getByTestId("header").textContent).to.equal("Legacy Header Experience");
  });
});

Component (unchanged):

import React from "react";
import { useFlags } from "launchdarkly-react-client-sdk";

export default function App() {
  const { simpleToggle } = useFlags();

  return (
    <div>
      <h1 data-testid="header">
        {simpleToggle ? "New Header Experience" : "Legacy Header Experience"}
      </h1>
    </div>
  );
}

Using ProxyRequire (CommonJS)

const proxyquire = require("proxyquire");
const { render, screen, cleanup } = require("@testing-library/react");
const { expect } = require("chai");
const React = require("react");

describe("App with ProxyRequire", () => {
  afterEach(() => {
    cleanup();
  });

  it("renders new header when simpleToggle is true", () => {
    const App = proxyquire("../src/App", {
      "launchdarkly-react-client-sdk": {
        useFlags: () => ({ simpleToggle: true })
      }
    });

    render(React.createElement(App));
    expect(screen.getByTestId("header").textContent).to.equal("New Header Experience");
  });

  it("renders legacy header when simpleToggle is false", () => {
    const App = proxyquire("../src/App", {
      "launchdarkly-react-client-sdk": {
        useFlags: () => ({ simpleToggle: false })
      }
    });

    render(React.createElement(App));
    expect(screen.getByTestId("header").textContent).to.equal("Legacy Header Experience");
  });
});

Component (unchanged):

const React = require("react");
const { useFlags } = require("launchdarkly-react-client-sdk");

function App() {
  const { simpleToggle } = useFlags();

  return React.createElement(
    "div",
    null,
    React.createElement(
      "h1",
      { "data-testid": "header" },
      simpleToggle ? "New Header Experience" : "Legacy Header Experience"
    )
  );
}

module.exports = App;

Important: When mocking the entire module, you must provide all exports used in your component (e.g., both useFlags and useLDClient if both are used).

Pros:

  • No code changes required
  • Test-runner agnostic
  • Component remains production-focused

Cons:

  • Async-only (must use await with ESMock)
  • Requires --import=esmock loader flag (can be slow)
  • Must mock all used exports from the module

Approach 2: Hook/Flag Map Injection

Pass the hook function or flag values as props instead of importing directly.

Component with dependency injection:

import React from "react";
import { useFlags } from "launchdarkly-react-client-sdk";

export default function App({ useFlags: useFlags_prop }) {
  // Use injected hook for testing, or real hook for production
  const useFlagsHook = useFlags_prop || useFlags;
  const { simpleToggle } = useFlagsHook();

  return (
    <div>
      <h1 data-testid="header">
        {simpleToggle ? "New Header Experience" : "Legacy Header Experience"}
      </h1>
    </div>
  );
}

Test:

import React from "react";
import { render, screen, cleanup } from "@testing-library/react";
import { expect } from "chai";
import App from "./hook-injection.jsx";

describe("App with Hook Injection", () => {
  afterEach(() => {
    cleanup();
  });

  it("renders new header when simpleToggle is true", () => {
    const mockUseFlags = () => ({ simpleToggle: true });
    render(<App useFlags={mockUseFlags} />);
    expect(screen.getByTestId("header").textContent).to.equal("New Header Experience");
  });

  it("renders legacy header when simpleToggle is false", () => {
    const mockUseFlags = () => ({ simpleToggle: false });
    render(<App useFlags={mockUseFlags} />);
    expect(screen.getByTestId("header").textContent).to.equal("Legacy Header Experience");
  });
});

Pros:

  • No mocking library required
  • Clear dependency injection pattern
  • Fast test execution

Cons:

  • Requires component code changes
  • Additional props to pass through
  • Props only used for testing

Approach 3: Mock Context Provider

Create a mock React context that matches the LaunchDarkly structure. Requires modifying components to accept an optional test context.

Mock Provider:

import React, { createContext, useContext } from "react";

const MockLDContext = createContext({ flags: {} });

function MockLDProvider({ flags = {}, children }) {
  const value = { flags };
  return React.createElement(MockLDContext.Provider, { value }, children);
}

export { MockLDProvider, MockLDContext };

Component with optional context:

import React, { useContext } from "react";
import { useFlags } from "launchdarkly-react-client-sdk";

export default function App({ testContext } = {}) {
  let flags;

  if (testContext) {
    // In TEST: use the mock context
    const ctx = useContext(testContext);
    flags = ctx.flags;
  } else {
    // In PROD: use the real LaunchDarkly hook
    flags = useFlags();
  }

  const { simpleToggle } = flags;

  return (
    <div>
      <h1 data-testid="header">
        {simpleToggle ? "New Header Experience" : "Legacy Header Experience"}
      </h1>
    </div>
  );
}

Test:

import React from "react";
import { render, screen, cleanup } from "@testing-library/react";
import { expect } from "chai";
import App from "./mock-context.jsx";
import { MockLDProvider, MockLDContext } from "./MockLDProvider.jsx";

describe("App with Mock Context Provider", () => {
  afterEach(() => {
    cleanup();
  });

  it("renders new header when simpleToggle is true", () => {
    render(
      React.createElement(
        MockLDProvider,
        { flags: { simpleToggle: true } },
        React.createElement(App, { testContext: MockLDContext })
      )
    );
    expect(screen.getByTestId("header").textContent).to.equal("New Header Experience");
  });

  it("renders legacy header when simpleToggle is false", () => {
    render(
      React.createElement(
        MockLDProvider,
        { flags: { simpleToggle: false } },
        React.createElement(App, { testContext: MockLDContext })
      )
    );
    expect(screen.getByTestId("header").textContent).to.equal("Legacy Header Experience");
  });
});

Pros:

  • No mocking library required
  • Can test provider wrapping behavior
  • Reusable mock provider across tests

Cons:

  • Requires component modifications
  • Adds conditional logic to production code
  • More complex test setup

Choosing an Approach

CriterionMonkey PatchHook InjectionMock Context
Code changes requiredNoneMinimalModerate
Test complexityModerateLowModerate
Test speedSlow (ESMock)FastFast
External dependenciesYesNoNo
Production code impactNoneMinor (extra prop)Moderate (conditional logic)

Recommendations:

  • Monkey Patch: Choose when you cannot modify production code or need to test existing components
  • Hook Injection: Choose for new components where dependency injection is acceptable
  • Mock Context: Choose when you need to test multiple components with provider wrapping

Resources