# Terratest: интеграционные тесты Terraform на Go _Тестирование · TerraformLab Knowledge Base_ **TL;DR:** Terratest, Go-библиотека от Gruntwork. Поднимает Terraform, ходит по AWS-API проверять что ресурсы реально созданы такими, как ожидаем, затем сносит. Тяжелее нативного `.tftest.hcl`, но даёт то, чего тот не даст: проверки облачного состояния, HTTP-запросы к подъятому сервису, table-driven тесты, retry-логика. Тесты на Go в `_test.go` файлах рядом с модулями. ## Когда Terratest Нативный `.tftest.hcl` отлично покрывает: «модуль принимает X, в plan видно Y». Не покрывает: «после apply реально создался бакет с именем Y, ARN совпадает с ожиданием, HTTPS endpoint отвечает 200». Это, Terratest. Это Go-библиотека. Тесты, `_test.go` файлы рядом с модулем. Запускается через `go test`. Внутри теста: `terraform.Init`, `terraform.Apply`, любые проверки через AWS SDK (или curl, или kubectl, что нужно), `defer terraform.Destroy`. Цена: писать Go, держать Go-окружение в CI, тесты идут минутами. ## Минимальный тест Структура: ``` examples/ └── s3-public-bucket/ └── main.tf # пример вызова твоего модуля modules/ └── s3-bucket/ └── main.tf test/ └── s3_bucket_test.go go.mod go.sum ``` `test/s3_bucket_test.go`: ```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 TestS3BucketCreated(t *testing.T) { t.Parallel() uniqueID := random.UniqueId() bucketName := fmt.Sprintf("test-bucket-%s", uniqueID) awsRegion := "us-east-1" tfOpts := &terraform.Options{ TerraformDir: "../examples/s3-public-bucket", Vars: map[string]interface{}{ "name": bucketName, "region": awsRegion, }, NoColor: true, } defer terraform.Destroy(t, tfOpts) terraform.InitAndApply(t, tfOpts) gotARN := terraform.Output(t, tfOpts, "bucket_arn") assert.Contains(t, gotARN, bucketName) aws.AssertS3BucketExists(t, awsRegion, bucketName) } ``` Что происходит по строкам: 1. `random.UniqueId()`, суффикс, чтобы два параллельных теста не дрались за одно глобально-уникальное имя бакета. 2. `defer terraform.Destroy` ставится **до** apply. Если apply упадёт destroy всё равно выполнится. 3. `terraform.InitAndApply`, init + apply -auto-approve. 4. `terraform.Output`, читает один output модуля. 5. `aws.AssertS3BucketExists`, реальный AWS-API вызов через SDK. Запуск: ```bash cd test go test -timeout 30m -v ``` Долгий timeout не зря, поднять+снести инфру это минуты. ## Table-driven Стандартный go-паттерн: один тест прогоняет N сценариев из таблицы: ```go func TestBucketVariations(t *testing.T) { t.Parallel() tests := []struct { name string versioning bool encryption bool }{ {"plain", false, false}, {"versioned", true, false}, {"encrypted", false, true}, {"all_features", true, true}, } for _, tc := range tests { tc := tc // closure capture t.Run(tc.name, func(t *testing.T) { t.Parallel() tfOpts := &terraform.Options{ TerraformDir: "../examples/s3-public-bucket", Vars: map[string]interface{}{ "name": fmt.Sprintf("test-%s-%s", tc.name, random.UniqueId()), "versioning": tc.versioning, "encryption": tc.encryption, }, } defer terraform.Destroy(t, tfOpts) terraform.InitAndApply(t, tfOpts) // assertions per-variation }) } } ``` Запуск конкретного варианта: `go test -run TestBucketVariations/versioned`. ## Retry на eventual consistency AWS-API не всегда мгновенно консистентен. Бакет «создан», но `GetBucketAcl` отвечает 404 ещё пару секунд. Terratest даёт retry-обёртки: ```go retry.DoWithRetry(t, "Wait for ACL", 10, 5*time.Second, func() (string, error) { acl := aws.GetS3BucketACL(t, awsRegion, bucketName) if acl == "" { return "", fmt.Errorf("ACL not yet readable") } return acl, nil }) ``` 10 попыток с 5-сек паузой. Без этого первый CI-run на чистом окружении упадёт случайно, flaky test, ад для команды. ## Стейджи: чтобы не разворачивать каждый раз Долгие тесты, это боль. Терратест умеет «зашить» apply, потом N тестов на том же state'е, потом destroy: ```go func TestStaged(t *testing.T) { tfOpts := &terraform.Options{ TerraformDir: "../examples/big" } defer test_structure.RunTestStage(t, "destroy", func() { terraform.Destroy(t, tfOpts) }) test_structure.RunTestStage(t, "deploy", func() { terraform.InitAndApply(t, tfOpts) }) test_structure.RunTestStage(t, "validate", func() { // bunch of asserts }) } ``` Запуск: `SKIP_destroy=true go test`, оставит инфру, прогонит deploy+validate. В следующий раз `SKIP_deploy=true SKIP_destroy=true go test`, только валидируем. Деплой одной командой, развалидация ста, отдельно. ## Что есть в terratest кроме aws и terraform | Пакет | Зачем | |---|---| | `modules/aws` | S3, EC2, IAM, RDS, EKS, ..., assertions и helpers | | `modules/k8s` | Создать kubeconfig, kubectl apply, wait for pod | | `modules/http-helper` | HTTP-вызовы с retry, проверить что endpoint отвечает | | `modules/docker` | docker run/exec, для тестов с локальным контейнером | | `modules/shell` | RunCommand с capture, что не покрыли остальные | | `modules/random` | UniqueId, Random*, стабильные суффиксы для имён | | `modules/retry` | DoWithRetry/DoWithRetryE для flaky-ситуаций | ## Terratest vs tftest | Аспект | tftest (`.tftest.hcl`) | Terratest (Go) | |---|---|---| | Язык теста | HCL | Go | | Окружение | Нужен только terraform | Нужен Go + AWS-creds (или LocalStack) | | Что проверяет | Атрибуты Terraform-state | State + реальные API + HTTP + любые SDK | | Скорость | Секунды | Минуты | | Mock support | Встроен (`mock_provider`) | Нет, реальные API | | Кривая обучения | HCL знаешь, пишешь | Go или с нуля, или вспоминать | | Когда использовать | Unit-тесты модуля | Integration на LocalStack/AWS | Реалистично, оба. tftest гоняешь на каждом PR (быстрый), Terratest, раз в день или на merge в main. ## Подводные камни - **Terratest требует крылатой инфры для CI.** Go + AWS-creds + достаточно quota. LocalStack помогает, но не покрывает всё, некоторые сервисы AWS Pro-only. - **Defer destroy не гарантирует destroy.** Если процесс убили kill -9 или у тест-runner истёк timeout, defer не выполнится. Чистка ресурсов остаётся на cron-job (например aws-nuke по тэгу). - **`t.Parallel()` без unique names = коллизия.** Два теста с одинаковыми bucket-name дерутся за глобальное пространство. Всегда `random.UniqueId()` в каждом имени. - **Большие тесты, большие AWS-bill'ы.** Один CI-runner поднял EKS-кластер, зафейлился до destroy, EKS живёт сутки = $$. Поставь cloud-quota alert и/или используй LocalStack где возможно. - **Terratest стабилизировался, но не «standard».** Конкуренты: kitchen-terraform (Ruby, устаревает), terraform-compliance (BDD, см. отдельную KB), собственные bash-скрипты. У Terratest самая большая community и лучший AWS-coverage, для интеграционных тестов это де-факто выбор. ## Команды ```bash go mod init testing ``` Инициализировать Go-модуль для тестов. Сделать один раз. ```bash go get github.com/gruntwork-io/terratest/modules/terraform ``` Подтянуть основной пакет. ```bash go test -timeout 30m -v ./... ``` Запустить все тесты. Timeout важен: без него умолчание 10 минут. ```bash SKIP_destroy=true go test -run TestSpecific ``` Не сносить инфру после теста: оставить для debug. ## См. также - [Mock-провайдеры: mock_provider, override_resource, override_data](/terraform/kb/tf-test-mocks.md) - [Что тестировать в Terraform, а что: не надо](/terraform/kb/iac-testing-theory.md)