Testing
Infrastructure is just code. Test it like code.
Why Test Infrastructure?
With r8s, your Kubernetes manifests are TypeScript components. That means you can test them with the same tools you use for your application code — Vitest, Jest, or any test runner.
No more deploying to staging and hoping it works. Run tests in CI and catch issues before they reach any cluster.
Catch Errors Early
Find missing resource limits, wrong image tags, or missing operators in CI — not in production.
Document Intent
Tests show what you expect from your infrastructure. New team members can read tests to understand the setup.
Prevent Regressions
Changing a component? Tests ensure you don't break other services that depend on it.
Enforce Standards
Pick the guardrails that matter for your organization. Start with one, add more as you grow.
Project Structure
Keep your infrastructure and tests together:
my-project/
├── k8s/
│ ├── r8s.tsx # Your infrastructure
│ └── __tests__/
│ └── r8s.test.ts # Tests for your infrastructure
├── package.json
└── tsconfig.jsonYour Infrastructure
Define your infrastructure in k8s/r8s.tsx:
// k8s/r8s.tsx
import { App } from '@r8s/recipes';
export default (
<App
name="api"
image="myapp/api:v1.2.3"
host="api.example.com"
replicas={3}
database={{ name: "app-db", storage: "10Gi" }}
tls={{ issuer: "letsencrypt" }}
resources={{
requests: { cpu: "100m", memory: "128Mi" },
limits: { cpu: "500m", memory: "512Mi" },
}}
/>
);Test Your Infrastructure
Import your infrastructure and test it with Vitest:
// k8s/__tests__/r8s.test.ts
import { describe, it, expect } from 'vitest';
import { render } from '@r8s/core';
import infrastructure from '../r8s';
describe('Infrastructure', () => {
it('should create a Deployment with 3 replicas', () => {
const result = render(infrastructure);
const deployment = result.resources.find(r => r.kind === 'Deployment');
expect(deployment.spec.replicas).toBe(3);
});
it('should require CNPG operator for database', () => {
const result = render(infrastructure);
expect(result.operators).toHaveLength(1);
expect(result.operators[0].name).toBe('cnpg');
});
});Built-in Guardrails
Don't write boilerplate test code. r8s includes production-ready guardrails in @r8s/core/guardrails that enforce Kubernetes best practices. Import the ones you need and run them with a single function call.
Why guardrails? Instead of writing repetitive tests like "check that every container has resource limits", use the built-in requireResourceLimits guardrail. It checks all containers across all Deployments, StatefulSets, and DaemonSets — and gives you actionable error messages.
Example: Resource Limits
The most common production issue is missing resource limits. One line prevents resource exhaustion and noisy neighbor problems:
// k8s/__tests__/guardrails.test.ts
import { describe, it, expect } from 'vitest';
import { render } from '@r8s/core';
import { runGuardrails, requireResourceLimits } from '@r8s/core/guardrails';
import infrastructure from '../r8s';
describe('Production Guardrails', () => {
it('should have resource limits on all containers', () => {
const result = render(infrastructure);
const guardrails = runGuardrails(result.resources, [
requireResourceLimits,
]);
expect(guardrails.passed).toBe(true);
expect(guardrails.errors).toHaveLength(0);
});
it('should provide actionable errors when guardrails fail', () => {
// Simulate infrastructure without resource limits
const result = render(
<App name="api" image="myapp/api:v1" host="api.example.com" />
);
const guardrails = runGuardrails(result.resources, [
requireResourceLimits,
]);
expect(guardrails.passed).toBe(false);
expect(guardrails.errors[0].code).toBe('MISSING_RESOURCE_LIMITS');
expect(guardrails.errors[0].suggestion).toContain('resource.limits');
});
});// When guardrails fail, you get detailed actionable errors
{
passed: false,
errors: [
{
code: 'MISSING_RESOURCE_LIMITS',
message: 'Container "api" in Deployment "api" is missing resource limits',
resource: 'Deployment',
field: 'spec.template.spec.containers[].resources.limits',
suggestion: 'Add resource.limits with cpu and memory values to prevent resource exhaustion'
}
],
warnings: [],
info: []
}Combine Multiple Guardrails
Mix and match guardrails for your requirements. Each guardrail is independent — pick the ones that matter for your team:
// k8s/__tests__/guardrails.test.ts
import { describe, it, expect } from 'vitest';
import { render } from '@r8s/core';
import {
runGuardrails,
requireResourceLimits,
requireTLS,
noRootContainers
} from '@r8s/core/guardrails';
import infrastructure from '../r8s';
describe('Production Readiness', () => {
it('should pass all production guardrails', () => {
const result = render(infrastructure);
// Combine multiple guardrails
const guardrails = runGuardrails(result.resources, [
requireResourceLimits, // Prevent resource exhaustion
requireTLS, // Enforce HTTPS
noRootContainers, // Security best practice
]);
expect(guardrails.passed).toBe(true);
expect(guardrails.errors).toHaveLength(0);
expect(guardrails.warnings).toHaveLength(0);
});
});All Available Guardrails
Import only the ones you need:
requireResourceLimits
All containers must have resource requests and limits.
requireNetworkPolicies
All namespaces must have at least one NetworkPolicy.
noPlaintextSecrets
Secrets should not contain plaintext passwords.
noRootContainers
Containers should not run as root user.
requireTLS
All Ingress resources must have TLS configured.
requireLabels
All resources must have required labels. Pass your own label names.
Custom Guardrails
Need something specific? Create your own guardrails for organizational requirements:
// k8s/__tests__/guardrails.test.ts
import { GuardrailRule, runGuardrails } from '@r8s/core/guardrails';
import { requireResourceLimits } from '@r8s/core/guardrails';
import infrastructure from '../r8s';
// Create your own guardrail
const requireReadinessProbe: GuardrailRule = {
id: 'require-readiness-probe',
description: 'All containers must have a readiness probe',
severity: 'warning',
test: (resources) => {
const errors = [];
for (const resource of resources) {
if (resource.kind === 'Deployment') {
const containers = resource.spec?.template?.spec?.containers || [];
for (const container of containers) {
if (!container.readinessProbe) {
errors.push({
code: 'MISSING_READINESS_PROBE',
message: `Container "${container.name}" is missing readinessProbe`,
resource: 'Deployment',
field: 'spec.template.spec.containers[].readinessProbe',
suggestion: 'Add readinessProbe to prevent traffic to unhealthy pods',
});
}
}
}
}
return errors;
},
};
// Use it alongside built-in guardrails
const result = render(infrastructure);
const guardrails = runGuardrails(result.resources, [
requireResourceLimits,
requireReadinessProbe, // Your custom rule
]);Snapshot Testing
Use snapshots to detect unexpected changes in rendered output:
// k8s/__tests__/snapshots.test.ts
import { describe, it } from 'vitest';
import { render } from '@r8s/core';
import infrastructure from '../r8s';
describe('Snapshots', () => {
it('should match snapshot', () => {
const result = render(infrastructure);
expect(result.resources).toMatchSnapshot();
expect(result.operators).toMatchSnapshot();
});
});CI/CD Integration
Run tests on every push to your k8s/ folder:
name: Test Infrastructure
on:
push:
branches: [main]
paths:
- 'k8s/**'
pull_request:
branches: [main]
paths:
- 'k8s/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test