Skip to main content

Testing Infrastructure

Where do your tests run — and what does that cost?

COMMIT → TYPECHECK → UNIT/INTEGRATION → PREVIEW DEPLOY → E2E → SHIP
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
Local <1s 5-30s Vercel builds 30-120s Production
Free Free DB required Free tier Browser Monitored

Testing infrastructure is the machinery that runs tests — CI pipelines, databases, runners, preview environments. Get this wrong and you pay in flaky tests, blocked terminals, or expensive CI bills. Get it right and every push gets verified automatically against a real deployment.

The Core Principle

Test against deployed artifacts, not dev servers.

Running E2E tests against localhost means port conflicts, RAM contention, and the false choice between "dev server running" or "run tests." Preview deploys eliminate this entire class of problems. A real URL. Real infrastructure. No local resource cost.

Pipeline Design

Two pipelines. Different triggers, different budgets.

Local Pipeline (every commit)

Fast, free, catches 90% of issues.

tsc --noEmit → vitest run → done
│ │
Types OK? Unit + Integration pass?
<10s <60s

Budget: Under 90 seconds total. If it exceeds this, tests will be skipped. Set a hard ceiling and enforce it.

Requirements:

  • Test database running (Docker container on a dedicated port)
  • No dev server needed — unit and integration tests don't use the browser
  • Memory-safe typecheck — use affected-file checking, not full project
CommandPurposeTrap to Avoid
tsc --noEmit or pnpm tcType verificationFull project typecheck can OOM on large monorepos. Use affected-only.
vitest run or jest --bailUnit + integration--bail stops on first failure. Fast feedback.

CI Pipeline (every PR)

Thorough. Runs against the preview deployment. Catches integration issues.

Install → Typecheck → Unit/Integration → Wait for Preview → E2E → Report

Vercel builds preview URL
from the PR branch

The key insight: Vercel gives you a preview URL on every push for free. The E2E tests just need to point at that URL instead of localhost.

Preview Deploy Testing

The single highest-leverage infrastructure change. Decouples test environment from development environment.

How It Works

git push → Vercel builds preview → CI runs Playwright against preview URL
│ │
Free on all plans Tests hit real infrastructure
Unique URL per commit No local server needed

Playwright Configuration

One config change makes Playwright work against any URL:

// playwright.config.ts
export default defineConfig({
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
},
// Only start local server when NOT in CI
webServer: process.env.CI ? undefined : {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: true,
},
});

Locally: Playwright starts the dev server. In CI: BASE_URL points to the Vercel preview. Same tests, different target.

Deployment Protection

If Vercel has deployment protection enabled, Playwright requests hit an auth page instead of the app. Solutions:

ApproachCostTrade-off
Disable protection for previewsFreePreviews are publicly accessible
Automation bypass headerPro plan ($20/mo)Previews stay protected, CI gets a bypass token
Password protection with env varPro planPlaywright sends the password in a setup step

For most teams, disabling protection on preview deployments is fine — they're ephemeral and the URLs are unguessable.

Test Database Infrastructure

Integration tests (L2) need a real database. Not a mock. Mocking the database hides the bugs that matter — constraint violations, transaction behavior, query performance.

Docker Test Database

Dedicated container, dedicated port, isolated from development:

ConcernApproach
IsolationSeparate port (e.g., 5433) from dev database
Startupdocker compose up with health check, wait for ready signal
CleanupEach test cleans its own data. No shared state between tests.
ResetDrop and recreate between test suites if needed
CIService container in GitHub Actions / self-hosted runner

Port Allocation

Dedicated ports prevent collisions between dev and test:

ServiceDev PortTest Port
Database54325433
App server30004300
API3001— (use preview URL)

Cost Management

Browser testing in CI is expensive. Playwright with 3 browsers for 5 minutes, 20 runs/day on GitHub Actions: ~$50/month. Here's how to reduce that.

Reduce What Runs

The testing strategy page covers the layer model. The infrastructure implication: every E2E test you convert to an integration test saves 30-120 seconds of CI browser time. At scale, this is the biggest cost lever.

ActionSavings
Convert server-action E2E → integration test~60s per test
Run only affected tests (nx affected)Skip unchanged projects entirely
Bail on first failure in PRsStop wasting time after a break
Parallelize with shardingSame wall-clock time, more runner-minutes, but faster feedback

Cheaper Runners

GitHub Actions minutes are premium-priced. Alternatives for browser-heavy workloads:

ProviderModelRelative Cost
GitHub ActionsPer-minute, managedBaseline (1x)
Self-hosted runnerYour machine, free computeFree (your electricity + RAM)
UbicloudDrop-in replacement, bare metal~0.3x
RunsOnYour AWS account, spot instances~0.1-0.15x
BlacksmithManaged, faster builds~0.5x

Cheapest viable path: Self-hosted runner on a spare machine for private repos. Ubicloud for open source.

Simplest path: Run E2E locally against preview URLs. Zero CI minutes for browser tests. The preview URL is the infrastructure — BASE_URL=https://your-preview.vercel.app npx playwright test.

NX and Caching

Monorepo build tools provide two cost-saving mechanisms:

MechanismWhat It Does
Affected commandsOnly test projects changed by the PR. Skip everything else.
Computation cachingIf inputs haven't changed, reuse previous test results.

In CI: nx affected -t test instead of nx run-many -t test. On a 10-project monorepo, this typically skips 60-80% of test runs.

Preflight Checklist

Before any test work:

  • Test database running and healthy
  • Correct environment identified (local vs CI vs preview)
  • Known issues checked (issue log, failing tests)
  • Test type identified → correct layer selected

Never Do

ActionWhyAlternative
Full-project typecheck on large monoreposOOM crashAffected-only typecheck
npm run dev in CIBlocks the runnerBuild + serve, or use preview URL
Skip database readiness checkTests fail silently with connection errorsAlways wait for ready signal
Claim tests pass without running themBreaks trustEvidence or nothing
Arbitrary sleep() in E2E testsFlaky, slowwaitForSelector(), waitForResponse()

Feedback Loop

Tests are instruments. They measure the gap between intent and reality.

TEST FAILURE

├─► Implementation gap → fix the code (engineering concern)

└─► Vision gap → update the spec (product concern)

Every failure is classified: is the code wrong, or is the spec wrong? This distinction routes feedback to the right team and prevents the cycle of fixing code to match a broken spec.

Log failures with evidence: test file, line number, expected vs actual. Structured feedback compounds — pattern recognition across failures reveals systemic issues that individual fixes miss.

Context