Terraform: инфраструктура на уровне кода — страница 51 из 65

        }

        // (...)

}

Обратите внимание на новый вспомогательный метод из пакета random, входящего в состав Terratest. Вам нужно еще раз выполнить depensure. Этот код присваивает переменной alb_name значение test-, где RANDOM_ID — случайный идентификатор, возвращаемый вспомогательным методом ran­dom.UniqueId() из Terratest. Данный метод возвращает рандомизированную строку из шести символов в кодировке base-62. Это короткий идентификатор, который можно добавлять к именам большинства ресурсов, не превышая максимальную длину, и который достаточно случайный для того, чтобы сделать конфликты крайне маловероятными (626 — это более 56 миллиардов комбинаций). Благодаря этому вы сможете запускать параллельно огромное количество тестов ALB, не заботясь о конфликтах имен.

Аналогично модифицируйте пример Hello, World. Для начала добавьте новую входную переменную в файл examples/hello-world-app/variables.tf:

variable "environment" {

  description = "The name of the environment we're deploying to"

  type        = string

  default     = "example"

}

Затем передайте эту переменную модулю hello-world-app:

module "hello_world_app" {

  source = "../../../modules/services/hello-world-app"

  server_text = "Hello, World"

  environment = var.environment

  mysql_config = var.mysql_config

  instance_type      = "t2.micro"

  min_size           = 2

  max_size           = 2

  enable_autoscaling = false

}

И присвойте переменной environment в файле test/hello_world_app_example_test.go значение, которое включает в себя random.UniqueId():

func TestHelloWorldAppExample(t *testing.T) {

        t.Parallel()

        opts := &terraform.Options{

                // Сделайте так, чтобы этот относительный путь вел

                // к папке с примерами для hello-world-app!

                TerraformDir: "../examples/hello-world-app/standalone",

                Vars: map[string]interface{}{

                        "mysql_config": map[string]interface{}{

                                "address": "mock-value-for-test",

                                "port": 3306,

                        },

                        "environment": fmt.Sprintf("test-%s", random.UniqueId()),

                },

        }

        // (...)

}

После внесения этих изменений параллельный запуск ваших тестов должен быть безопасным:

$ go test -v -timeout 30m

TestAlbExample 2019-05-26T17:57:21+01:00 (...)

TestHelloWorldAppExample 2019-05-26T17:57:21+01:00 (...)

TestAlbExample 2019-05-26T17:57:21+01:00 (...)

TestHelloWorldAppExample 2019-05-26T17:57:21+01:00 (...)

TestHelloWorldAppExample 2019-05-26T17:57:21+01:00 (...)

(...)

PASS

ok  terraform-up-and-running  216.090s

Вы должны увидеть, что оба теста выполняются одновременно. Таким образом, общее время тестирования должно занимать примерно столько времени, сколько нужно для завершения самого медленного теста в вашем наборе (а не совокупное время работы всех тестов, как в случае с последовательным запуском).


Параллельное выполнение тестов в одной и той же папке

Еще один аспект параллелизма, который следует учитывать,  — ситуация, когда несколько автоматических тестов запускаются для одной и той же папки Terraform. Например, вы могли бы запустить несколько разных тестов для examples/hello-world-app, каждый из которых перед выполнением terraform apply присваивает свои собственные значения входным переменным. Если вы попробуете это сделать, возникнет проблема: ваши тесты начнут конфликтовать, поскольку все они выполняют terraform init и тем самым переопределяют файлы состояния Terraform в папке .terraform.

Если вы хотите параллельно запускать несколько тестов для одной и той же папки, самое простое решение — сделать так, чтобы каждый тест копировал состояние в уникальную папку и запускал Terraform из нее, чтобы избежать конфликтов. У Terratest для этого есть встроенный вспомогательный метод, который даже следит за корректным размещением относительных путей для модулей Terraform: подробности ищите в документации к методу test_structure.CopyTerraformFolderToTemp.


Интеграционные тесты

Подготовив несколько модульных тестов, мы можем переходить к модульному тестированию. Чтобы сформировать общее понимание, которое позже можно будет применить к коду Terraform, лучше начать с примера веб-сервера, написанного на Ruby. Для интеграционного тестирования такого сервера нужно выполнить следующее.

1. Запустить веб-сервер на локальном компьютере так, чтобы он прослушивал порт.

2. Отправить ему HTTP-запросы.

Создадим в файле web-server-test.rb метод, который выполняет эти шаги:

def do_integration_test(path, check_response)

  port = 8000

  server = WEBrick::HTTPServer.new :Port => port

  server.mount '/', WebServer

  begin

    # Запускаем веб-сервер в отдельном потоке, чтобы он не блокировал тест

    thread = Thread.new do

      server.start

    end

    # Отправляем HTTP-запрос по определенному пути веб-сервера

    uri = URI("http://localhost:#{port}#{path}")

    response = Net::HTTP.get_response(uri)

    # Используем для проверки ответа заданную лямбда-функцию check_response

    check_response.call(response)

  ensure

    # В конце теста останавливаем сервер и поток

    server.shutdown

    thread.join

  end

end

Метод do_integration_test конфигурирует веб-сервер на порте 8000, запускает его в фоновом потоке (чтобы он не блокировал выполнение теста), шлет HTTP-запрос типа GET по заданному пути, передает HTTP-ответ на проверку функции check_response и в конце останавливает веб-сервер. Вот как с помощью этого метода можно написать интеграционный тест для точки входа /:

def test_integration_hello

  do_integration_test('/', lambda { |response|

    assert_equal(200, response.code.to_i)

    assert_equal('text/plain', response['Content-Type'])

    assert_equal('Hello, World', response.body)

  })

end

Этот код вызывает метод do_integration_test с путем / и передает ему лямбду (встраиваемую функцию), которая проверяет, равны ли код и тело ответа 200OK и соответственно Hello,World. Похожи интеграционные тесты и для других точек входа, хотя для /web-service выполняется более общая проверка (то есть assert_include вместо assert_equal), чтобы минимизировать потенциальные проблемы при изменении точки входа example.org:

def test_integration_api

  do_integration_test('/api', lambda { |response|

    assert_equal(201, response.code.to_i)

    assert_equal('application/json', response['Content-Type'])

    assert_equal('{"foo":"bar"}', response.body)

  })

end

def test_integration_404

  do_integration_test('/invalid-path', lambda { |response|

    assert_equal(404, response.code.to_i)

    assert_equal('text/plain', response['Content-Type'])

    assert_equal('Not Found', response.body)

  })

end

def test_integration_web_service

  do_integration_test('/web-service', lambda { |response|

    assert_equal(200, response.code.to_i)

    assert_include(response['Content-Type'], 'text/html')

    assert_include(response.body, 'Example Domain')

  })

end

Запустим все наши тесты:

$ ruby web-server-test.rb

(...)

Finished in 0.221561 seconds.

--------------------------------------------

8 tests, 24 assertions, 0 failures, 0 errors

100% passed

--------------------------------------------

Обратите внимание, что при модульном тестировании на выполнение всех тестов уходило 0,000572 секунды. Теперь же это время выросло примерно в 387 раз, до 0,221561 секунды. Конечно, выполнение по-прежнему происходит молниеносно, но это связано лишь с тем, что код веб-сервера на Ruby мало что умеет. Этот пример специально сделан как можно более компактным. Здесь важны не абсолютные показатели, а относительная тенденция: интеграционные тесты обычно работают медленней, чем модульные. Позже мы еще к этому вернемся.

Теперь сосредоточимся на интеграционном тестировании кода Terraform. Если раньше мы тестировали отдельные модули, то теперь нужно проверить, как они работают вместе. Для этого мы должны развернуть их одновременно и убедиться в том, что они ведут себя корректно. Ранее мы развернули демонстрационное приложение Hello, World с фиктивными данными вместо настоящей БД MySQL. Теперь развернем реальный модуль MySQL и посмотрим, как с ним интегрируется приложение Hello, World. У вас уже должен быть подходящий код в папках live/stage/data-stores/mysql и live/stage/services/hello-world-app. Таким образом, вы можете создать интеграционный тест для своей среды финального тестирования (точнее, для ее части).

Конечно, как уже упоминалось в этой главе, все автоматические тесты должны выполняться в изолированной учетной записи AWS. Поэтому при проверке кода для среды финального тестирования все тесты следует запускать от имени изолированного тестового пользователя. Если в ваших модулях есть код, который специально прописан для среды финального тестирования, самое время сделать его конфигурируемым, чтобы вы могли внедрять тестовые значения. Позвольте менять в файле live/stage/data-stores/mysql/variables.tf название базы данных, используя новую входную переменную db_name:

variable "db_name" {

  description = "The name to use for the database"

  type        = string

  default     = "example_database_stage"

}

Передайте это значение модулю mysql в файле live/stage/data-stores/mysql/main.tf:

module "mysql" {

  source = "../../../../modules/data-stores/mysql"