TL;DR
- Terratest prueba infraestructura real via APIs de la nube—no estado de Terraform—detectando bugs que el testing nativo no encuentra
- El patrón
defer terraform.Destroygarantiza limpieza incluso cuando las pruebas fallan, previniendo recursos huérfanos- Los test stages te permiten saltar pasos lentos durante desarrollo local (saltar build de AMI, reusar infra desplegada)
Ideal para: Equipos que necesitan validar comportamiento real en la nube, no solo corrección de configuración Omitir si: Solo necesitas validar sintaxis/lógica de Terraform (usa
terraform testnativo) Tiempo de lectura: 12 minutos
Aquí hay una verdad dura sobre el testing de infraestructura: el framework de test nativo de Terraform valida estado, no realidad. Si un bug del provider crea un recurso mal configurado pero reporta éxito, tus tests pasan mientras tu infraestructura falla.
Terratest resuelve esto consultando recursos reales en la nube a través de sus APIs nativas. Tu bucket S3 no solo está “creado” en el estado de Terraform—Terratest verifica que realmente existe, tiene el cifrado correcto y sirve contenido correctamente.
Prerrequisitos
Antes de comenzar, asegúrate de tener:
- Go 1.21+ instalado (
go version) - Terraform 1.0+ instalado (
terraform version) - AWS CLI configurado con credenciales (
aws sts get-caller-identity) - Conocimiento básico de Go (funciones, structs, manejo de errores)
- Una cuenta AWS dedicada o sandbox para testing
Paso 1: Configuración del Proyecto
Crea una estructura de directorios para tu módulo Terraform y tests:
mkdir -p terraform-s3-module/test
cd terraform-s3-module
Inicializa el módulo Go:
cd test
go mod init github.com/yourorg/terraform-s3-module/test
go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/gruntwork-io/terratest/modules/aws
go get github.com/stretchr/testify/assert
Tu go.mod debería ahora referenciar Terratest v0.53.0 o posterior.
Paso 2: Crear un Módulo Terraform para Probar
Crea main.tf en el directorio raíz:
variable "bucket_name" {
description = "Name of the S3 bucket"
type = string
}
variable "environment" {
description = "Environment tag"
type = string
default = "test"
}
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
}
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
bucket = aws_s3_bucket.this.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
output "bucket_id" {
value = aws_s3_bucket.this.id
}
output "bucket_arn" {
value = aws_s3_bucket.this.arn
}
Paso 3: Escribe Tu Primer Test con Terratest
Crea test/s3_bucket_test.go:
package test
import (
"fmt"
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestS3BucketCreation(t *testing.T) {
t.Parallel()
// Genera nombre único para evitar conflictos
uniqueID := random.UniqueId()
bucketName := fmt.Sprintf("terratest-example-%s", uniqueID)
awsRegion := "us-west-2"
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../",
Vars: map[string]interface{}{
"bucket_name": bucketName,
"environment": "test",
},
EnvVars: map[string]string{
"AWS_DEFAULT_REGION": awsRegion,
},
})
// CRÍTICO: Siempre limpia recursos
defer terraform.Destroy(t, terraformOptions)
// Despliega infraestructura
terraform.InitAndApply(t, terraformOptions)
// Obtiene outputs
bucketID := terraform.Output(t, terraformOptions, "bucket_id")
// Valida infraestructura REAL via AWS API
aws.AssertS3BucketExists(t, awsRegion, bucketID)
// Verifica que el versionado esté realmente habilitado (no solo en estado)
versioning := aws.GetS3BucketVersioning(t, awsRegion, bucketID)
assert.Equal(t, "Enabled", versioning)
// Verifica configuración de cifrado
encryption := aws.GetS3BucketEncryption(t, awsRegion, bucketID)
assert.Equal(t, "AES256", encryption)
}
Patrones clave en este test:
t.Parallel()— Ejecuta tests concurrentemente para mayor velocidadrandom.UniqueId()— Previene colisiones de nombres de recursosdefer terraform.Destroy()— Garantiza limpieza incluso en fallosaws.AssertS3BucketExists()— Valida recursos AWS reales, no estado
Paso 4: Ejecuta el Test
Ejecuta desde el directorio test:
go test -v -timeout 30m
Salida esperada:
=== RUN TestS3BucketCreation
=== PAUSE TestS3BucketCreation
=== CONT TestS3BucketCreation
TestS3BucketCreation 2026-01-11T10:15:30Z command.go:158: Running command terraform with args [init]
TestS3BucketCreation 2026-01-11T10:15:35Z command.go:158: Running command terraform with args [apply -auto-approve]
...
TestS3BucketCreation 2026-01-11T10:16:45Z command.go:158: Running command terraform with args [destroy -auto-approve]
--- PASS: TestS3BucketCreation (95.23s)
PASS
Paso 5: Test Stages para Iteración Más Rápida
Los tests del mundo real son lentos. Construir AMIs, desplegar clusters y ejecutar validaciones puede tomar más de 30 minutos. Los test stages te permiten saltar etapas completadas durante desarrollo:
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
)
func TestWithStages(t *testing.T) {
t.Parallel()
workingDir := test_structure.CopyTerraformFolderToTemp(t, "../", ".")
// Stage: Deploy
defer test_structure.RunTestStage(t, "teardown", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, workingDir)
terraform.Destroy(t, terraformOptions)
})
test_structure.RunTestStage(t, "deploy", func() {
uniqueID := random.UniqueId()
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: workingDir,
Vars: map[string]interface{}{
"bucket_name": fmt.Sprintf("terratest-%s", uniqueID),
},
})
test_structure.SaveTerraformOptions(t, workingDir, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
})
// Stage: Validate
test_structure.RunTestStage(t, "validate", func() {
terraformOptions := test_structure.LoadTerraformOptions(t, workingDir)
bucketID := terraform.Output(t, terraformOptions, "bucket_id")
aws.AssertS3BucketExists(t, "us-west-2", bucketID)
})
}
Salta stages durante desarrollo:
# Salta deploy, solo ejecuta validación (asume que la infra existe)
SKIP_deploy=true go test -v -run TestWithStages
# Salta teardown para mantener recursos para debugging
SKIP_teardown=true go test -v -run TestWithStages
Enfoques Asistidos por IA
En 2026, la IA acelera significativamente el desarrollo con Terratest.
Lo que la IA hace bien:
- Generar estructura boilerplate de tests a partir de módulos Terraform
- Escribir aserciones basadas en configuraciones de recursos
- Sugerir casos límite (¿qué pasa si el nombre del bucket tiene caracteres especiales?)
- Convertir outputs HCL a mappings de structs en Go
Lo que aún necesita humanos:
- Decidir qué comportamientos realmente importa probar
- Entender el radio de impacto si los tests fallan en cuentas de producción
- Diseñar estrategias de aislamiento de tests
- Revisar aserciones generadas por IA para falsos positivos
Prompt útil:
Dado este módulo Terraform que crea una instancia RDS:
resource "aws_db_instance" "main" {
identifier = var.db_identifier
engine = "postgres"
engine_version = "15.4"
instance_class = var.instance_class
allocated_storage = 20
storage_encrypted = true
deletion_protection = var.enable_deletion_protection
}
Genera un Terratest que:
1. Despliegue la instancia RDS con variables de test
2. Valide que el cifrado esté realmente habilitado via AWS API
3. Verifique que la versión correcta de Postgres esté corriendo
4. Pruebe que la protección contra eliminación esté configurada correctamente
5. Use limpieza apropiada con defer
Incluye manejo de errores y mensajes de aserción significativos.
Framework de Decisión
Cuándo Usar Terratest
Este enfoque funciona mejor cuando:
- Necesitas validar comportamiento real de recursos en la nube
- Pruebas interacciones entre múltiples servicios (VPC + EC2 + RDS)
- Tu equipo tiene experiencia en Go o disposición para aprender
- Los tests de integración son críticos para tu pipeline de despliegue
- Estás probando Kubernetes, Docker o Packer junto con Terraform
Considera alternativas cuando:
- Solo necesitas validar lógica de Terraform (usa
terraform test) - El feedback rápido es crítico y la curva de aprendizaje de Go es muy pronunciada
- Tu infraestructura es simple (<5 recursos)
- Las restricciones de costo limitan el despliegue de recursos efímeros
Terratest vs Terraform Test Nativo
| Aspecto | Terratest | Terraform Test |
|---|---|---|
| Qué prueba | Infraestructura real via APIs | Estado de Terraform |
| Lenguaje | Go | HCL |
| Curva de aprendizaje | Mayor | Menor |
| Detecta bugs de providers | Sí | No |
| Soporte multi-herramienta | Terraform, K8s, Docker, Packer | Solo Terraform/OpenTofu |
| Velocidad | Más lento (deploys reales) | Más rápido (basado en plan) |
Mi recomendación: Usa ambos. Tests nativos de Terraform para pruebas unitarias rápidas de lógica de módulos. Terratest para tests de integración que validan infraestructura real antes de despliegues a producción.
Midiendo el Éxito
| Métrica | Línea Base | Objetivo | Cómo Medir |
|---|---|---|---|
| Cobertura de tests | 0% | 80%+ de módulos críticos | Contar módulos testeados vs total |
| Tiempo de ejecución | N/A | < 30 min para suite completa | Métricas de pipeline CI |
| Recursos huérfanos | Desconocido | 0 | Tags en AWS Cost Explorer |
| Tasa de tests inestables | Alta | < 5% | Análisis de fallos CI |
| Tiempo medio de detección | Días | Horas | Tracking de incidentes |
Señales de alerta:
- Tests pasan pero deploys a producción fallan — tests no son suficientemente completos
- Recursos huérfanos acumulándose — la limpieza no funciona correctamente
- Tests toman > 1 hora — necesitas paralelizar o usar test stages
- Alta inestabilidad — falta lógica de retry o problemas de timing
Errores Comunes
1. Falta defer Destroy
// MAL: Si Apply falla, los recursos quedan huérfanos
terraform.InitAndApply(t, options)
terraform.Destroy(t, options) // Nunca se alcanza si Apply falla
// BIEN: La limpieza se ejecuta sin importar el resultado del test
defer terraform.Destroy(t, options)
terraform.InitAndApply(t, options)
2. Colisiones de Nombres de Recursos
// MAL: Conflictuará con otros tests o ejecuciones
bucketName := "my-test-bucket"
// BIEN: Único por ejecución de test
uniqueID := random.UniqueId()
bucketName := fmt.Sprintf("test-%s", uniqueID)
3. Regiones Hardcodeadas
// MAL: Falla si la región por defecto difiere
aws.AssertS3BucketExists(t, "us-east-1", bucketID)
// BIEN: Consistente con el despliegue de Terraform
awsRegion := terraform.Output(t, options, "region")
aws.AssertS3BucketExists(t, awsRegion, bucketID)
4. Timeouts Insuficientes
# MAL: El timeout por defecto de 10m mata tests de larga duración
go test -v
# BIEN: Permite tiempo para infraestructura real
go test -v -timeout 30m
Próximos Pasos
Comienza con un módulo crítico—tu VPC, base de datos principal o cluster de cómputo principal. Escribe un solo test que valide que realmente funciona, no solo que se despliega. Ejecútalo en CI antes de cada merge.
Una vez cómodo, expande para probar interacciones: ¿puede tu aplicación realmente conectarse a esa base de datos? ¿El load balancer rutea el tráfico correctamente?
El objetivo no es 100% de cobertura—es la confianza de que tu infraestructura funciona en la realidad, no solo en la visión de Terraform de la realidad.
Artículos relacionados:
- Mejores Prácticas de Testing en Pulumi
- Estrategias de Testing y Validación en Terraform
- Estrategia de la Pirámide de Automatización de Tests
- Testing de Templates de CloudFormation
Recursos externos: