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

                TerraformDir: "../examples/alb",

        }

        // Удаляем все в конце теста

        defer terraform.Destroy(t, opts)

        // Развертываем пример

        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,

        )

}

Обратите внимание, что defer размещается в начальной части кода, еще до вызова terraform.InitAndApply. Это нужно для того, чтобы тест не был провален еще до выражения defer, иначе вызов terraform.Destroy может не попасть в очередь на выполнение.

Этот модульный тест готов к запуску. Поскольку он развертывает инфраструктуру в AWS, перед его выполнением следует привычным способом аутентифицировать свою учетную запись (см. врезку «Способы аутентификации» на с. 64). Как упоминалось в этой главе, ручное тестирование следует проводить в изолированной учетной записи. Это вдвойне справедливо для автоматических тестов, поэтому советую сделать аутентификацию от имени совершенно отдельного пользователя. По мере создания все новых автоматических тестов у вас могут создаваться сотни и тысячи ресурсов, поэтому крайне важно изолировать их от всего остального.

Я обычно рекомендую командам разработчиков выделить совершенно отдельную среду (например, в отдельной учетной записи AWS) специально для автоматических тестов — отдельно даже от изолированных окружений, которые используются для ручного тестирования. Так, можно безопасно удалять в этой среде все ресурсы старше нескольких часов (предполагается, что ни один тест не выполняется настолько долго).

После входа в учетную запись AWS, которую можно безопасно применять для тестирования, запустите тест:

$ go test -v -timeout 30m

TestAlbExample 2019-05-26T13:29:32+01:00 command.go:53:

Running command terraform with args [init -upgrade=false]

(...)

TestAlbExample 2019-05-26T13:29:33+01:00 command.go:53:

Running command terraform with args [apply -input=false -lock=false]

(...)

TestAlbExample 2019-05-26T13:32:06+01:00 command.go:121:

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

(...)

TestAlbExample 2019-05-26T13:32:06+01:00 command.go:53:

Running command terraform with args [output -no-color alb_dns_name]

(...)

TestAlbExample 2019-05-26T13:38:32+01:00 http_helper.go:27:

Making an HTTP GET call to URL

http://terraform-up-and-running-1892693519.us-east-2.elb.amazonaws.com

(...)

TestAlbExample 2019-05-26T13:38:32+01:00 command.go:53:

Running command terraform with args

[destroy -auto-approve -input=false -lock=false]

(...)

TestAlbExample 2019-05-26T13:39:16+01:00 command.go:121:

Destroy complete! Resources: 5 destroyed.

(...)

PASS

ok   terraform-up-and-running   229.492s

Обратите внимание на аргумент -timeout30m, который используется вместе с коман­дой gotest. Go по умолчанию ограничивает время работы тестов десятью минутами; когда это время заканчивается, тестовое выполнение принудительно завершается, в результате чего тест не просто проваливается, но даже не доходит до кода очистки (terraformdestroy). Эта проверка ALB должна занять около пяти минут, но при реализации тестов, которые развертывают реальную инфраструктуру, время ожидания лучше увеличить, чтобы тест не прервался на полпути и не оставил после себя разного рода инфраструктуру.

Этот тест сгенерирует длинный журнальный вывод, но, если внимательно почитать, можно заметить все его ключевые моменты.

1. Выполнение terraforminit.

2. Выполнение terraformapply.

3. Чтение выходной переменной с помощью terraformapply.

4. Неоднократная отправка HTTP-запросов к ALB.

5. Выполнение terraformdestroy.

Этот код работает несравнимо медленнее модульных тестов на Ruby, но теперь менее чем за пять минут вы автоматически узнаете, ведет ли себя ли ваш модуль alb так, как вы того ожидали. Для инфраструктуры AWS это максимально быстро, и в результате вы должны увериться в том, что ваш код работает как следует. Если в вашем примере есть какая-либо ошибка (например, код состояния для действия по умолчанию был случайно изменен на 401), вы довольно скоро об этом узнаете:

$ go test -v -timeout 30m

(...)

Validation failed for URL

http://terraform-up-and-running-931760451.us-east-2.elb.amazonaws.com.

Response status: 401. Response body: 404: page not found.

(...)

Sleeping for 10s and will try again.

(...)

Validation failed for URL

http://terraform-up-and-running-h2ezYz-931760451.us-east-2.elb.amazonaws.com.

Response status: 401. Response body: 404: page not found.

(...)

Sleeping for 10s and will try again.

(...)

--- FAIL: TestAlbExample (310.19s)

http_helper.go:94:

    HTTP GET to URL

http://terraform-up-and-running-931760451.us-east-2.elb.amazonaws.com

    unsuccessful after 10 retries

FAIL     terraform-up-and-running       310.204s


Внедрение зависимостей

Теперь посмотрим, сколько усилий потребуется, чтобы создать модульный тест для чуть более сложного кода. Еще раз вернемся к примеру с веб-сервером на Ruby и представим, что нам нужно добавить новую точку входа /web-service, которая шлет HTTP-вызовы к внешней зависимости:

class Handlers

  def handle(path)

    case path

    when "/"

      [200, 'text/plain', 'Hello, World']

    when "/api"

      [201, 'application/json', '{"foo":"bar"}']

    when "/web-service"

      # Новая точка входа, которая вызывает веб-сервис

      uri = URI("http://www.example.org")

      response = Net::HTTP.get_response(uri)

      [response.code.to_i, response['Content-Type'], response.body]

    else

      [404, 'text/plain', 'Not Found']

    end

  end

end

Обновленный класс Handlers обрабатывает URL-адрес /web-service, отправляя HTTP-запрос типа GET на example.org и передавая ответ. Если обратиться к этой точке входа с помощью curl, получится следующий результат:

$ curl localhost:8000/web-service

Example Domain

<-- (...) -->

Example Domain

      This domain is established to be used for illustrative

      examples in documents. You may use this domain in

      examples without prior coordination or asking for permission.

Как написать модульный тест для этого нового метода? Если проверять код как есть, тестирование будет зависеть от поведения внешнего компонента (в данном случае сервиса example.org). Есть ряд отрицательных последствий.

• Если у этой зависимости возникнут перебои в работе, ваш тест провалится, несмотря на то что с вашим кодом все в порядке.

• Если эта зависимость со временем поменяет свое поведение (например, начнет возвращать другое тело ответа), ваши тесты будут периодически проваливаться. Придется постоянно обновлять их код, несмотря на то что у вашей реализации нет никаких проблем.

• Если эта зависимость медленная, она притормозит ваши тесты, что нивелирует одно из главных преимуществ модульного тестирования — быструю обратную связь.

• Если вы захотите убедиться в том, что ваш код справляется с различными крайними случаями, связанными с поведением этой внешней зависимости (например, как ваш код реагирует на перенаправление?), придется делать это без контроля над ней.

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

Например, класс Handlers не должен знать все подробности о том, как вызывать веб-сервис. Эту логику можно вынести в отдельный класс WebService:

class WebService

  def initialize(url)

    @uri = URI(url)

  end

  def proxy

    response = Net::HTTP.get_response(@uri)

     [response.code.to_i, response['Content-Type'], response.body]

  end

end

Этот класс принимает на вход URL-адрес и предоставляет метод proxy для про­ксирования с этого адреса HTTP-ответа типа GET. Мы можем сделать так, чтобы класс Handlers принимал в качестве ввода экземпляр WebService и использовал его в своем методе web_service:

class Handlers

  def initialize(web_service)

    @web_service = web_service

  end

  def handle(path)

    case path

    when "/"

      [200, 'text/plain', 'Hello, World']

    when "/api"

      [201, 'application/json', '{"foo":"bar"}']

    when "/web-service"

      # Новая точка входа, которая вызывает веб-сервис

      @web_service.proxy

    else

      [404, 'text/plain', 'Not Found']

    end

  end

end

Теперь в коде реализации можно внедрить реальный экземпляр WebService, который шлет HTTP-вызовы к example.org:

class WebServer < WEBrick::HTTPServlet::AbstractServlet