Complete MDX Components Test Guide
Learn how to test custom MDX components in JavaScript projects. Set up Vitest or Jest to verify your markdown components render correctly and handle edge cases reliably.
MDX lets you write JSX components inside Markdown files, giving content authors powerful layout tools without leaving familiar syntax. Testing these custom components ensures they render correctly across every article, regardless of the input they receive. A broken callout or malformed code block in production damages reader trust.
This guide uses Vitest as the test framework because of its fast startup and native ESM support, but the patterns apply equally to Jest.
Setting Up the Test Environment
First, install Vitest and jsdom for DOM simulation. The jsdom package provides a browser-like environment without opening a real browser:
npm install --save-dev vitest jsdomAdd "test": "vitest run" to the scripts field in package.json, then create a vitest.config.js file to set jsdom as the test environment. Vitest reads test settings from this config file, not from package.json:
// vitest.config.js
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
},
});With this setup, your tests run in a simulated DOM that supports document.querySelector, innerHTML, and other browser APIs.
Testing a Simple MDX Component
Start with the simplest possible component: a Callout box that renders a title and children. Here is the component implementation:
// Callout.jsx
export function Callout({ title, children }) {
return (
<div className="callout" data-testid="callout">
<strong>{title}</strong>
<div>{children}</div>
</div>
);
}The test file verifies three things: the component renders, the title appears, and children content renders inside the component. First, import the testing utilities and the component:
// Callout.test.jsx
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { Callout } from "./Callout.jsx";Then write a test that renders the component with typical props and asserts on the visible output in the DOM:
describe("Callout", () => {
it("renders the title and children", () => {
render(
<Callout title="Note">
<p>This is important information.</p>
</Callout>
);
expect(screen.getByText("Note")).toBeTruthy();
expect(screen.getByText("This is important information.")).toBeTruthy();
});
});Run the test with npm test. If the component renders correctly, both assertions pass. This is the basic pattern for every MDX component test: render with props, then assert on the output.
Testing Edge Cases
Real content throws edge cases at your components. A thorough test suite covers these scenarios. For the Callout component, test what happens when the optional title prop is missing entirely:
it("renders without a title", () => {
render(
<Callout>
<p>Content without a title.</p>
</Callout>
);
expect(screen.getByText("Content without a title.")).toBeTruthy();
});And test what happens when children are empty, which is a common edge case for content components that receive no body content:
it("handles empty children gracefully", () => {
render(<Callout title="Empty Note" />);
expect(screen.getByText("Empty Note")).toBeTruthy();
// Component should not crash with no children
});For components that accept configurable props, test each prop value that changes rendering behavior. If a code block component supports a language prop for syntax highlighting, test that passing "javascript", "python", and an empty string all produce valid output.
Snapshot Testing for Regression Protection
Snapshots capture the rendered HTML output and alert you when it changes unexpectedly. They complement the functional tests above by catching visual regressions. Add a snapshot test after your render tests using the same render import from @testing-library/react, then assert the output against a stored snapshot:
it("matches snapshot with typical props", () => {
const { container } = render(
<Callout title="Remember">
<p>Save your work before closing.</p>
</Callout>
);
expect(container.firstChild).toMatchSnapshot();
});The first run creates a snapshot file. Subsequent runs compare the current output against the snapshot, and Vitest reports a difference if anything changed.
Update snapshots with npm test -- --update when you deliberately change the component's rendering.
Testing MDX Integration
The real test of an MDX component is how it behaves inside actual MDX content. Set up a test that compiles a small MDX string and verifies the component renders within it, catching issues that unit tests miss, like incorrect JSX parsing or missing imports.
The test imports the MDX compiler and the React JSX runtime:
import { compile, run } from "@mdx-js/mdx";
import * as runtime from "react/jsx-runtime";Then define a small MDX string, compile it to a runnable function body, render it, and assert on the visible text output:
it("renders inside MDX content", async () => {
const mdxContent = `import { Callout } from "./Callout.jsx";\n\n<Callout title="Heads up">Rendered from MDX.</Callout>`;
const compiled = String(await compile(mdxContent, { outputFormat: "function-body" }));
const { default: MDXContent } = await run(compiled, runtime);
const { container } = render(<MDXContent />);
expect(container.textContent).toContain("Heads up");
});This integration test compiles actual MDX syntax and verifies the component appears in the output. It is the closest approximation to production behavior without deploying to a live site.
Running Tests in CI
Add a test step to your continuous integration pipeline. A simple GitHub Actions workflow runs the test suite on every push and pull request, checking out the code, installing Node.js, and running npm test:
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm testIf any test fails, the CI run fails and the pull request is blocked. This prevents broken MDX components from reaching production and gives reviewers confidence that content changes did not break rendering.
For more on the JavaScript module system that MDX depends on, see the ES6 modules guide. For testing fundamentals, the Vitest unit testing guide covers setup and patterns in more depth.
Rune AI
Key Insights
- Use Vitest or Jest with jsdom to test MDX components in a simulated DOM environment.
- Test basic rendering first: does the component output the expected HTML structure.
- Test props: required props, optional props, and edge cases like missing or invalid values.
- Snapshot tests catch unexpected visual changes when component implementation updates.
- Integrate MDX component tests into your CI pipeline with a simple npm test script.
Frequently Asked Questions
What is MDX and why test its components?
Can I test MDX without a full React setup?
What should I test in an MDX component?
Conclusion
Testing MDX components ensures your content renders reliably across every article and page. Start with basic render tests, add prop validation, and layer in snapshot testing for regression protection. Integrate these tests into your CI pipeline so broken MDX components never reach production. The same testing patterns apply whether you use Vitest, Jest, or any other JavaScript test framework.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.