Environment variables are global state. Testing them usually means mutating process.env and hoping you clean up after. CtroEnv gives you better tools.
Testing with objectSource
objectSource() wraps a plain object as an environment source — no globals involved:
import { describe, expect, it } from "vitest"
import { defineEnv, objectSource, string, number } from "@ctroenv/core"
function makeEnv(overrides: Record<string, string> = {}) {
return defineEnv(
{
DATABASE_URL: string().url(),
PORT: number().port().default(3000),
JWT_SECRET: string().min(32).secret(),
},
{ source: objectSource(overrides) },
)
}
it("parses valid env vars", () => {
const env = makeEnv({
DATABASE_URL: "postgresql://localhost:5432/db",
JWT_SECRET: "a".repeat(32),
})
expect(env.DATABASE_URL).toBe("postgresql://localhost:5432/db")
expect(env.PORT).toBe(3000) // default
})
it("throws on missing required vars", () => {
expect(() => makeEnv({ PORT: "4000" })).toThrow()
})
No beforeEach/afterEach cleanup. No global mutation. Each test gets a fresh environment.
Testing Validation Errors
Catch and inspect specific errors:
import { CtroEnvError } from "@ctroenv/core"
it("reports invalid URL", () => {
try {
makeEnv({
DATABASE_URL: "not-a-url",
JWT_SECRET: "a".repeat(32),
})
} catch (e) {
expect(e).toBeInstanceOf(CtroEnvError)
expect((e as CtroEnvError).errors[0].code).toBe("invalid_value")
expect((e as CtroEnvError).errors[0].key).toBe("DATABASE_URL")
}
})
it("collects all errors at once", () => {
try {
makeEnv({}) // both required vars missing
} catch (e) {
expect((e as CtroEnvError).errors).toHaveLength(2)
}
})
No fix-one-find-another cycle. Every error surfaces in one throw.
Testing Secret Masking
it("masks secret values", () => {
const env = makeEnv({
DATABASE_URL: "postgresql://localhost:5432/db",
JWT_SECRET: "x".repeat(32),
})
expect(env.JWT_SECRET).toBe("********")
expect(env.meta.get("JWT_SECRET")).toBe("x".repeat(32))
})
it("supports custom mask", () => {
const env = defineEnv(
{ KEY: string().secret() },
{ source: objectSource({ KEY: "value" }), maskWith: "***" },
)
expect(env.KEY).toBe("***")
})
Debugging with Error Codes
CtroEnv has four error codes. Each tells you exactly what went wrong:
| Code | Meaning | Example |
|---|---|---|
missing_required |
Variable not in source |
DATABASE_URL not set |
type_mismatch |
Wrong JavaScript type | String for number validator |
invalid_value |
Failed refinement | Invalid URL, port out of range |
validation_failed |
Custom .validate() rejected |
API key format wrong |
formatErrors
import { formatErrors } from "@ctroenv/core"
try {
defineEnv(schema)
} catch (e) {
if (e instanceof CtroEnvError) {
process.stderr.write(formatErrors(e.errors))
// Groups errors by type with colors
}
}
CI/CD Integration
CtroEnv's CLI runs in CI without importing your schema — it parses config files directly.
Fast Key Check
npx ctroenv check --source .env.example
Compares keys only. Runs in under a second. Perfect for every PR.
Strict Validation
npx ctroenv check --source .env.staging --strict
Validates values against their validators — catches invalid URLs, out-of-range ports.
Unknown Key Warnings
npx ctroenv check --source .env --warn-unknown
Detects keys in your .env that aren't in the schema. Uses Levenshtein distance for suggestions:
⚠ Unknown key: "DATABSE_URL"
→ Did you mean "DATABASE_URL"?
JSON Output
npx ctroenv check --source .env --json
{
"clean": false,
"summary": { "missing": 1, "unused": 0, "matched": 4 },
"validationErrors": null
}
Pipe into Slack notifications, monitoring dashboards, or your own tooling.
GitHub Actions
name: Env Check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx ctroenv check --source .env.example
Build-Time Validation
Framework plugins validate during the build:
// Vite
ctroenvPlugin({ schema: "./src/env.ts", failOnError: true })
// Next.js
export default withCtroEnv(schema, nextConfig)
If validation fails, the build exits with code 1. No deployment reaches production with a missing DATABASE_URL.
Previous: Framework-Specific Env Patterns
Next: Shipping Reusable Env Schemas
Top comments (0)