code-test
Test DevOps code including Terraform, Kubernetes, Docker, and shell scripts using unit tests, integration tests, and compliance tests. Use when creating test strategies, writing infrastructure tests,
SKILL.md.claude/skills/code-test/SKILL.md
GitHubDevOps Code Testing Skill
You are a Senior DevOps Engineer specializing in testing infrastructure code. You implement testing strategies for Terraform, Kubernetes, Docker, and shell scripts using unit tests, integration tests, and compliance validation.
Core Testing Areas
Terraform Testing
- Unit tests with Terraform Test Framework
- Integration tests with Terratest
- Plan validation
- Cost estimation tests
Kubernetes Testing
- YAML validation with kubeconform
- Policy testing with OPA/Conftest
- Helm chart testing
- Dry-run validation
Docker Testing
- Image structure tests
- Container runtime tests
- Security scanning
- Multi-arch testing
CI/CD Testing
- Workflow validation
- Mock pipeline runs
- Security gate testing
Shell Script Testing
- Unit tests with Bats
- Static analysis with ShellCheck
- Integration testing
Terraform Testing
Terraform Test Framework
# tests/unit.tftest.hcl
variables {
environment = "test"
project = "test-project"
}
run "validate_vpc" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR block should be 10.0.0.0/16"
}
assert {
condition = aws_vpc.main.enable_dns_hostnames == true
error_message = "DNS hostnames should be enabled"
}
}
run "validate_security_group_rules" {
command = plan
assert {
condition = length(aws_security_group.web.ingress) == 1
error_message = "Web security group should have exactly one ingress rule"
}
assert {
condition = aws_security_group.web.ingress[0].from_port == 443
error_message = "Web security group should only allow HTTPS"
}
}
run "validate_tags" {
command = plan
variables {
tags = {
Environment = "test"
Project = "test-project"
}
}
assert {
condition = aws_vpc.main.tags["Environment"] == "test"
error_message = "VPC should have Environment tag"
}
}
Terratest (Go)
// tests/vpc_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/stretchr/testify/assert"
)
func TestVpcCreation(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../terraform/modules/vpc",
Vars: map[string]interface{}{
"environment": "test",
"vpc_cidr": "10.0.0.0/16",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcID := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcID)
// Verify VPC exists
vpc := aws.GetVpcById(t, vpcID, "us-east-1")
assert.Equal(t, "10.0.0.0/16", vpc.CidrBlock)
assert.Equal(t, "available", vpc.State)
}
func TestSecurityGroupRules(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../terraform/modules/security",
Vars: map[string]interface{}{
"allowed_cidr": []string{"10.0.0.0/8"},
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Test ingress rules
ingressRules := terraform.OutputListOfObjects(t, terraformOptions, "ingress_rules")
for _, rule := range ingressRules {
cidrBlocks := rule["cidr_blocks"].([]interface{})
assert.NotContains(t, cidrBlocks, "0.0.0.0/0")
}
}
Terraform Plan Validation
# .github/workflows/terraform-test.yml
name: Terraform Tests
on:
pull_request:
paths:
- 'terraform/**'
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Run Terraform Tests
run: |
cd terraform/modules/vpc
terraform init
terraform test
integration-tests:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Run Terratest
run: |
cd tests
go test -v -timeout 30m
Kubernetes Testing
Kubeconform
# .github/workflows/k8s-test.yml
name: Kubernetes Tests
on:
pull_request:
paths:
- 'k8s/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kubeconform
run: |
curl -L -o kubeconform.tar.gz https://github.com/yannh/kubeconform/releases/download/v0.6.4/kubeconform-linux-amd64.tar.gz
tar -xzf kubeconform.tar.gz
sudo mv kubeconform /usr/local/bin/
- name: Validate manifests
run: |
kubeconform -strict -schema-location default \
-schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master' \
k8s/
- name: Helm lint
run: |
helm lint charts/*
helm template myapp charts/myapp | kubeconform -strict
OPA/Conftest Tests
# tests/k8s/policy.rego
package main
deny[msg] {
input.kind == "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot
msg := "Deployment must run as non-root"
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container %s must have memory limits", [container.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
container.image
contains(container.image, ":latest")
msg := sprintf("Container %s must not use 'latest' tag", [container.name])
}
deny[msg] {
input.kind == "Service"
input.spec.type == "LoadBalancer"
not input.metadata.annotations["service.beta.kubernetes.io/aws-load-balancer-internal"]
msg := "LoadBalancer service must be internal"
}
warn[msg] {
input.kind == "Deployment"
input.spec.replicas < 2
msg := "Deployment should have at least 2 replicas for HA"
}
Helm Testing
# charts/myapp/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "myapp.fullname" . }}-test-connection"
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args:
- '--spider'
- '{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health'
restartPolicy: Never
# test-helm.yml
name: Helm Tests
on:
pull_request:
paths:
- 'charts/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Helm
uses: azure/setup-helm@v3
- name: Setup Kind
uses: helm/kind-action@v1
- name: Run Helm tests
run: |
helm install myapp charts/myapp --wait
helm test myapp
Docker Testing
Container Structure Tests
# tests/container-structure.yaml
schemaVersion: "2.0.0"
commandTests:
- name: "node version"
command: "node"
args: ["--version"]
expectedOutput: ["v20."]
- name: "npm version"
command: "npm"
args: ["--version"]
expectedOutput: ["10."]
- name: "health check endpoint"
command: "curl"
args: ["-f", "http://localhost:8080/health"]
expectedOutput: ["ok"]
fileExistenceTests:
- name: "app directory exists"
path: "/app"
shouldExist: true
- name: "tmp directory exists"
path: "/tmp"
shouldExist: true
fileContentTests:
- name: "package.json contains app name"
path: "/app/package.json"
expectedContents: ["myapp"]
metadataTest:
env:
- key: NODE_ENV
value: production
labels:
- key: maintainer
value: platform-team
exposedPorts: ["8080"]
user: "nodejs"
name: Docker Tests
on:
pull_request:
paths:
- 'Dockerfile'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:test .
- name: Container Structure Test
uses: brpaz/structure-tests-action@v1
with:
image: app:test
config: tests/container-structure.yaml
Docker Compose Testing
# docker-compose.test.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
- NODE_ENV=test
depends_on:
- db
- redis
command: npm test
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
redis:
image: redis:7-alpine
name: Docker Compose Tests
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: docker-compose -f docker-compose.test.yml up --abort-on-container-exit
Shell Script Testing
Bats Tests
#!/usr/bin/env bats
# tests/test_backup.bats
@test "backup script exists and is executable" {
[ -x "./scripts/backup.sh" ]
}
@test "backup script requires database argument" {
run ./scripts/backup.sh
[ "$status" -eq 1 ]
[[ "$output" == *"Usage:"* ]]
}
@test "backup script validates database name" {
run ./scripts/backup.sh "invalid; rm -rf /"
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid database name"* ]]
}
@test "backup script creates backup file" {
run ./scripts/backup.sh "test_db" "test-bucket"
[ "$status" -eq 0 ]
[ -f "/tmp/backup-*/backup.sql" ]
}
@test "backup script cleans up on failure" {
DB_HOST="invalid" run ./scripts/backup.sh "test_db" "test-bucket"
[ "$status" -eq 1 ]
[ ! -d "/tmp/backup-*" ]
}
name: Shell Tests
on:
pull_request:
paths:
- 'scripts/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bats
run: |
git clone https://github.com/bats-core/bats-core.git
cd bats-core
sudo ./install.sh /usr/local
- name: Run tests
run: bats tests/
Ansible Testing
Molecule
# molecule/default/converge.yml
---
- name: Converge
hosts: all
tasks:
- name: Include role
ansible.builtin.include_role:
name: common
# molecule/default/verify.yml
---
- name: Verify
hosts: all
gather_facts: false
tasks:
- name: Check SSH service
ansible.builtin.service:
name: ssh
state: started
register: ssh_status
failed_when: not ssh_status.status.ActiveState == "active"
- name: Verify timezone
ansible.builtin.command: timedatectl
register: timezone
changed_when: false
failed_when: "'UTC' not in timezone.stdout"
- name: Check fail2ban
ansible.builtin.service:
name: fail2ban
state: started
name: Ansible Tests
on:
pull_request:
paths:
- 'ansible/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install ansible molecule molecule-docker
- name: Run Molecule tests
run: |
cd ansible/roles/common
molecule test
Compliance Testing
name: Compliance Tests
on:
schedule:
- cron: '0 0 * * 0'
workflow_dispatch:
jobs:
cis-benchmarks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run CIS Benchmark
uses: aquasecurity/trivy-action@master
with:
scan-type: 'config'
scan-ref: '.'
format: 'sarif'
output: 'compliance-results.sarif'
- name: Check compliance
run: |
if [ $(grep -c "CRITICAL" compliance-results.sarif) -gt 0 ]; then
echo "Compliance violations found"
exit 1
fi
Test Organization
tests/
├── terraform/
│ ├── unit/
│ │ ├── vpc_test.go
│ │ └── security_test.go
│ └── integration/
│ └── full_stack_test.go
├── kubernetes/
│ ├── policy/
│ │ └── security.rego
│ └── helm/
│ └── values-test.yaml
├── docker/
│ └── container-structure.yaml
├── shell/
│ ├── test_backup.bats
│ └── test_deploy.bats
└── ansible/
└── molecule/
└── default/
├── converge.yml
├── molecule.yml
└── verify.yml
Test Guidelines
- Unit Tests: Fast, isolated, test single components
- Integration Tests: Test component interactions
- End-to-End Tests: Test complete workflows
- Compliance Tests: Verify security/policy adherence
Test Metrics
- Coverage: Minimum 80% for Terraform modules
- Execution Time: < 10 minutes for PR validation
- Reliability: < 1% flaky test rate
- Documentation: Every test has clear purpose documented