DEV Community

Cover image for Testing and Debugging Your Env Config
Odejobi Abiola Samuel
Odejobi Abiola Samuel

Posted on

Testing and Debugging Your Env Config

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()
})
Enter fullscreen mode Exit fullscreen mode

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)
  }
})
Enter fullscreen mode Exit fullscreen mode

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("***")
})
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Compares keys only. Runs in under a second. Perfect for every PR.

Strict Validation

npx ctroenv check --source .env.staging --strict
Enter fullscreen mode Exit fullscreen mode

Validates values against their validators — catches invalid URLs, out-of-range ports.

Unknown Key Warnings

npx ctroenv check --source .env --warn-unknown
Enter fullscreen mode Exit fullscreen mode

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"?
Enter fullscreen mode Exit fullscreen mode

JSON Output

npx ctroenv check --source .env --json
Enter fullscreen mode Exit fullscreen mode
{
  "clean": false,
  "summary": { "missing": 1, "unused": 0, "matched": 4 },
  "validationErrors": null
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Build-Time Validation

Framework plugins validate during the build:

// Vite
ctroenvPlugin({ schema: "./src/env.ts", failOnError: true })

// Next.js
export default withCtroEnv(schema, nextConfig)
Enter fullscreen mode Exit fullscreen mode

If validation fails, the build exits with code 1. No deployment reaches production with a missing DATABASE_URL.

Links: GitHub · Docs · npm

Previous: Framework-Specific Env Patterns
Next: Shipping Reusable Env Schemas

Top comments (0)