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:
- Mock the module using a library
- Inject dependencies as props
- 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
awaitwith ESMock) - Requires
--import=esmockloader 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
| Criterion | Monkey Patch | Hook Injection | Mock Context |
|---|---|---|---|
| Code changes required | None | Minimal | Moderate |
| Test complexity | Moderate | Low | Moderate |
| Test speed | Slow (ESMock) | Fast | Fast |
| External dependencies | Yes | No | No |
| Production code impact | None | Minor (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