In-Source Testing with Vitest for Private Helpers Without Exporting Them
Keep private helper functions tested without exporting them by writing Vitest suites inside the implementation file and gating them with import.meta.vitest, with careful Vite build configuration to avoid shipping test code.
03 May 2026, 12:36 UTC

The concrete problem is a private helper that is too small to deserve a public export but too risky to leave untested. Exporting it pollutes the module API; creating a separate .test.ts file forces you to reach into internals or duplicate setup. In-source testing with Vitest lets you keep the test next to the implementation and gate it behind import.meta.vitest, so the test runs in Vitest but is intended to be removed from production builds.
The useful takeaway is cohesion without bundle cost, provided the Vite build is configured to replace the gate with undefined. If the gate is misconfigured, test code and its dependencies can leak into production.
When in-source testing fits
In-source testing is useful for small, private utilities that are only consumed inside one file. Keeping the validation next to the logic reduces file-system navigation and avoids exposing internals for testability.
It is less useful for public APIs, complex fixtures, or tests that need to be reused across modules. Those belong in dedicated test files where they can be discovered and maintained independently.
How the gate works
Vitest provides a compile-time flag via import.meta.vitest. In a Vitest run the property is truthy, so an if (import.meta.vitest) block executes. In a production Vite build the flag is replaced.
The replacement is done with Vite's define option, which performs textual substitution before bundling. Replacing import.meta.vitest with undefined makes the condition false, allowing Vite's tree-shaking to drop the unreachable block.
TypeScript needs to know the property exists. The types field in tsconfig.json should include Vitest types so import.meta.vitest is recognized without errors.
Worked example
Consider a private normalizer inside src/utils/id.ts.
// src/utils/id.ts
export function formatId(raw) {
return String(raw).trim().toLowerCase().replace(/\s+/g, '-')
}
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest
test('normalizes spaces and case', () => {
expect(formatId(' My Id ')).toBe('my-id')
})
}
Vite config to strip the block for production builds. Run this from the project root where vite.config.ts lives.
// vite.config.ts
export default {
define: {
'import.meta.vitest': 'undefined'
}
}
tsconfig hint for editors:
// tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
Verification steps to check behavior, not claimed as tested here:
- Run
npx vitestfrom the project root. Expect the in-source test to be discovered and executed. - Run the production build command, e.g.
npx vite build. Inspect the generated JS bundle indistand check that test identifiers such asdescribeor the test string are absent.
Risks: a missing or incorrect define leaves the block in the bundle. Adding test-only dependencies inside the block can increase bundle size if the block is not eliminated.
Trade-offs and limits
Bundle safety depends entirely on correct build configuration. If define is omitted, the condition remains and test code ships.
Readability suffers when source files grow with many tests. In-source testing is best limited to small, private helpers.
Tooling assumptions matter. The approach relies on Vite's replacement and tree-shaking behavior. Current verification is required for your exact Vitest and Vite versions.
Actionable closing
Use in-source testing for private helpers you would otherwise leave untested. Keep the block small, gate it with if (import.meta.vitest), ensure define replaces the flag in production, and confirm via bundle inspection that test code is absent. Reserve dedicated test files for public APIs and shared fixtures.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.