Чтобы быстро проверить, верно ли сконфигурировано ваше окружение, создайте в своей новой папке файл go_sanity_test.go следующего содержания:
package test
import (
"fmt"
"testing"
)
func TestGoIsWorking(t *testing.T) {
fmt.Println()
fmt.Println("If you see this text, it's working!")
fmt.Println()
}
Выполните этот тест с помощью команды gotest и убедитесь, что она возвращает следующий вывод:
$ go test -v
If you see this text, it's working!
PASS
ok terraform-up-and-running 0.004s
Флаг -v означает verbose («подробно»). Он делает так, чтобы тест показывал весь журнальный вывод.
Если все работает, можете удалить go_sanity_test.go и приступить к написанию модульного теста для модуля alb. Создайте в папке test файл alb_example_test.go со следующим каркасом своего теста:
package test
import (
"testing"
)
func TestAlbExample(t *testing.T) {
}
Для начала вы должны указать Terratest, где находится ваш код Terraform. Используйте для этого тип terraform.Options:
package test
import (
"github.com/gruntwork-io/terratest/modules/terraform"
"testing"
)
func TestAlbExample(t *testing.T) {
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь
// вел к папке с примерами для alb!
TerraformDir: "../examples/alb",
}
}
Следует отметить, что для проверки модуля alb вам действительно нужно протестировать код примера в папке examples (обновите относительный путь в TerraformDir, чтобы он вел к нужной папке). Это означает, что демонстрационный код теперь имеет тройное назначение: он служит исполняемой документацией, средством ручного тестирования и инструментом для выполнения автоматических тестов ваших модулей.
Обратите также внимание на новую инструкцию импорта для библиотеки Terratest вверху файла. Чтобы загрузить эту зависимость на ваш компьютер, выполните depensure:
$ dep ensure
Команда depensure просканирует ваш код на Go, найдет все новые инструкции импорта, автоматически загрузит их вместе со всеми зависимостями в папку vendor и пропишет их в файл Gopkg.lock. Если для вас в этом слишком много магии, можете использовать команду depensure-add, чтобы перечислить все нужные вам зависимости вручную:
$ dep ensure -add github.com/gruntwork-io/terratest/modules/terraform
Следующий этап автоматического тестирования — выполнение команд terraforminit и terraformapply, которые развернут ваш код. У Terratest для этого есть вспомогательные средства:
func TestAlbExample(t *testing.T) {
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь
// вел к папке с примерами для alb!
TerraformDir: "../examples/alb",
}
terraform.Init(t, opts)
terraform.Apply(t, opts)
}
Выполнение команд init и apply в Terratest — настолько рутинная операция, что для этого предусмотрен удобный вспомогательный метод, который делает все одной командой:
func TestAlbExample(t *testing.T) {
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь
// вел к папке с примерами для alb!
TerraformDir: "../examples/alb",
}
// Развертываем пример
terraform.InitAndApply(t, opts)
}
Код выше уже представляет собой довольно функциональный модульный тест: он выполняет terraforminit и terraformapply и проваливает тест, если эти команды не завершаются успешно (например, из-за проблем в вашем коде Terraform). Но вы можете пойти еще дальше и выполнить HTTP-запросы к развернутому балансировщику нагрузки, чтобы убедиться, что он возвращает нужные вам данные. Для этого вам надо как-то получить доменное имя развернутого балансировщика. К счастью, пример alb возвращает его в виде выходной переменной:
output "alb_dns_name" {
value = module.alb.alb_dns_name
description = "The domain name of the load balancer"
}
У Terratest есть встроенные вспомогательные средства для чтения вывода из кода Terraform:
func TestAlbExample(t *testing.T) {
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь
// вел к папке с примерами для alb!
TerraformDir: "../examples/alb",
}
// Развертываем пример
terraform.InitAndApply(t, opts)
// Получаем URL-адрес ALB
albDnsName := terraform.OutputRequired(t, opts, "alb_dns_name")
url := fmt.Sprintf("http://%s", albDnsName)
}
Функция OutputRequired возвращает вывод с заданным именем или проваливает тест, если этот вывод пустой или не существует. Предыдущий листинг формирует на основе этого вывода URL-адрес, используя встроенную в Go функцию fmt.Sprintf (не забудьте импортировать пакет fmt). Следующим шагом будет выполнение HTTP-запросов по этому URL-адресу:
package test
import (
"fmt"
"crypto/tls"
"github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/terraform"
"testing"
)
func TestAlbExample(t *testing.T) {
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь
// вел к папке с примерами для alb!
TerraformDir: "../examples/alb",
}
// Развертываем пример
terraform.InitAndApply(t, opts)
// Получаем URL-адрес ALB
albDnsName := terraform.OutputRequired(t, opts, "alb_dns_name")
url := fmt.Sprintf("http://%s", albDnsName)
// Проверяем в ALB действие по умолчанию, которое должно вернуть 404
expectedStatus := 404
expectedBody := "404: page not found"
http_helper.HttpGetWithValidation(t, url, expectedStatus, expectedBody)
}
Этот код импортирует из Terratest новый пакет, http_helper. Чтобы его загрузить, нужно еще раз выполнить rundep. Метод http_helper.HttpGetWithValidation сделает HTTP-запрос типа GET по переданному вами URL-адресу и провалит тест, если код состояния и тело ответа не совпадают с теми, которые вы указали.
У этого кода есть одна проблема: между завершением команды terraformapply и тем, когда доменное имя балансировщика нагрузки становится доступным (то есть распространилось по системе), проходит время. Если вызвать http_helper.HttpGetWithValidation незамедлительно, он, вполне вероятно, окажется неудачным, хотя через 30 секунд или минуту ALB заработает в нормальном режиме. Как уже обсуждалось в подразделе «Отложенная согласованность согласуется… с отлагательством» на с. 211, такого рода асинхронное поведение с отложенной согласованностью является для AWS (а точнее, для большинства распределенных систем) нормальным и решением будет повторное выполнение вызовов. У Terratest есть вспомогательный метод и для этого:
func TestAlbExample(t *testing.T) {
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь
// вел к папке с примерами для alb!
TerraformDir: "../examples/alb",
}
// Развертываем пример
terraform.InitAndApply(t, opts)
// Получаем URL-адрес ALB
albDnsName := terraform.OutputRequired(t, opts, "alb_dns_name")
url := fmt.Sprintf("http://%s", albDnsName)
// Проверяем в ALB действие по умолчанию, которое должно вернуть 404
expectedStatus := 404
expectedBody := "404: page not found"
maxRetries := 10
timeBetweenRetries := 10 * time.Second
http_helper.HttpGetWithRetry(
t,
url,
&tls.Config{},
expectedStatus,
expectedBody,
maxRetries,
timeBetweenRetries,
)
}
Метод http_helper.HttpGetWithRetry почти не отличается от метода http_helper.HttpGetWithValidation, только при отсутствии ответа с нужными кодом состояния или телом он повторит попытку. Максимальное количество повторений (десять) и длину интервалов между ними (десять секунд) можно настроить. Если в какой-то момент он получит ожидаемый ответ, тест будет пройден; если после истечения максимального количества попыток ожидаемый ответ так и не пришел, тест считается проваленным.
В конце теста нужно выполнить команду terraformdestroy, чтобы очистить ресурсы. Для этого у Terratest есть вспомогательный метод: terraform.Destroy. Но, если вызывать его в самом конце, из-за любой программной ошибки выше по коду (например, HttpGetWithRetry даст сбой из-за неправильной конфигурации ALB) тест может завершиться, не доходя до него, в результате чего развернутая инфраструктура не будет удалена.
Таким образом, вам нужно убедиться в том, что terraform.Destroy выполняется всегда, даже если тест проваливается. Во многих языках программирования для этого предусмотрены конструкции try/finally или try/ensure. Но в Go это делается с помощью выражения defer, которое гарантирует, что переданный в него код будет выполнен при завершении окружающей его функции (каким бы оно ни было):
func TestAlbExample(t *testing.T) {
opts := &terraform.Options{
// Сделайте так, чтобы этот относительный путь
// вел к папке с примерами для alb!