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

    when "/api"

      response.status = 201

      response['Content-Type'] = 'application/json'

      response.body = '{"foo":"bar"}'

    else

      response.status = 404

      response['Content-Type'] = 'text/plain'

      response.body = 'Not Found'

    end

  end

end

Создание модульного теста для этого кода усложняется тем, что вам нужно выполнить несколько действий.

1. Создать экземпляр класса WebServer. Это сложнее, чем кажется, так как конструктору WebServer, который наследует AbstractServlet, необходимо передать целый класс HTTPServer из WEBrick. Вы могли бы создать его mock-объект, но это потребовало бы много усилий.

2. Создать объект request типа HTTPRequest. Получение экземпляра этого класса, равно как и создание его mock-объекта, требует довольно большого объема работы.

3. Создать объект response типа HTTPResponse. Опять же экземпляр этого класса нельзя получить легким путем, а для создания его mock-объекта нужно много усилий.

Если вам сложно писать модульные тесты, это часто говорит о плохом качестве кода и является поводом для рефакторинга. Чтобы облегчить модульное тестирование, обработчики (то есть код, который обрабатывает пути /, /api и «Страница не найдена») можно вынести в отдельный класс Handlers:

class Handlers

  def handle(path)

    case path

    when "/"

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

    when "/api"

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

    else

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

    end

  end

end

У этого нового класса есть два ключевых свойства.

• Простые значения в качестве ввода. Класс Handlers не зависит от HTTPServer, HTTPRequest или HTTPResponse. Весь его ввод состоит из простых параметров, таких как строка с URL-адресом.

• Простые значения в качестве вывода. Вместо того чтобы менять поля объекта HTTPResponse (создавая тем самым побочный эффект), методы класса Handlers возвращают HTTP-ответ в виде простого значения (массива с кодом состояния, типом содержимого и телом).

Код, который принимает на вход и возвращает простые значения, обычно легче понять, обновить и протестировать. Сначала изменим класс WebServer так, чтобы он отвечал на запросы с помощью Handlers.

class WebServer < WEBrick::HTTPServlet::AbstractServlet

  def do_GET(request, response)

    handlers = Handlers.new

    status_code, content_type, body = handlers.handle(request.path)

    response.status = status_code

    response['Content-Type'] = content_type

    response.body = body

  end

end

Этот код вызывает метод handle из класса Handlers и возвращает полученные из него код статуса, тип содержимого и тело в качестве HTTP-ответа. Как видите, код, использующий класс Handlers, выглядит аккуратным и простым. Кроме того, это облегчает тестирование. Вот как выглядит модульный тест для точки входа /:

class TestWebServer < Test::Unit::TestCase

  def initialize(test_method_name)

    super(test_method_name)

    @handlers = Handlers.new

  end

  def test_unit_hello

    status_code, content_type, body = @handlers.handle("/")

    assert_equal(200, status_code)

    assert_equal('text/plain', content_type)

    assert_equal('Hello, World', body)

  end

end

Код теста вызывает тот же метод handle из класса Handlers и использует несколько вызовов assert для проверки ответа, который возвращается из точки входа /. Вот как запустить этот тест:

$ ruby web-server-test.rb

Loaded suite web-server-test

Finished in 0.000287 seconds.

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

1 tests, 3 assertions, 0 failures, 0 errors

100% passed

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

Похоже, тест пройден. Теперь добавим модульные тесты для точек входа /api и 404:

def test_unit_api

  status_code, content_type, body = @handlers.handle("/api")

  assert_equal(201, status_code)

  assert_equal('application/json', content_type)

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

end

def test_unit_404

  status_code, content_type, body = @handlers.handle("/invalid-path")

  assert_equal(404, status_code)

  assert_equal('text/plain', content_type)

  assert_equal('Not Found', body)

end

Запустите тесты еще раз:

$ ruby web-server-test.rb

Loaded suite web-server-test

Finished in 0.000572 seconds.

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

3 tests, 9 assertions, 0 failures, 0 errors

100% passed

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

Всего за 0,000 527 2 секунды вы можете проверить, работает ли код вашего веб-сервера так, как вы того ожидали. В этом сила модульных тестов: быстрые результаты, которые помогают укрепить уверенность в своем коде. Если допущена какая-либо ошибка (например, случайно поменялся ответ точки входа /api), вы узнаете об этом почти мгновенно:

$ ruby web-server-test.rb

Loaded suite web-server-test

============================================================================

Failure: test_unit_api(TestWebServer)

web-server-test.rb:25:in `test_unit_api'

     22:     status_code, content_type, body = Handlers.new.handle("/api")

     23:     assert_equal(201, status_code)

     24:     assert_equal('application/json', content_type)

  => 25:     assert_equal('{"foo":"bar"}', body)

     26:   end

     27:

     28:   def test_unit_404

<"{\"foo\":\"bar\"}"> expected but was

<"{\"foo\":\"whoops\"}">

diff:

? {"foo":"bar   "}

?         whoops

============================================================================

Finished in 0.007904 seconds.

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

3 tests, 9 assertions, 1 failures, 0 errors

66.6667% passed

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


Основы модульных тестов

Какой эквивалент такого рода модульных тестов для кода Terraform? Для начала нужно определиться с тем, что в мире Terraform считается модулем. Ближайший аналог отдельной функции или класса — обобщенный модуль (определение термина дано в подразделе «Компонуемые модули» на с. 226), такой как alb из главы 6. Как бы вы его протестировали?

В Ruby для написания модульных тестов необходимо провести такой рефакторинг, чтобы код можно было выполнять без сложных зависимостей вроде HTTPServer, HTTPRequest или HTTPResponse. Если подумать о том, чем занимается код Terraform (обращение к API AWS для создания балансировщиков нагрузки, прослушивателей, целевых групп и т. д.), то в 99 % случаев он взаимодействует со сложными зависимостями! Не существует практичного способа свести число внешних зависимостей к нулю, но, даже если бы вы могли это сделать, у вас бы практически не осталось кода для тестирования56.

Это подводит нас к ключевому выводу о тестировании № 3: вы не можете проводить чистое тестирование кода Terraform.

Но не отчаивайтесь. Вы все еще можете укрепить свою уверенность в том, что ваш код Terraform ведет себя предсказуемо. Для этого автоматические тесты должны использовать ваш код для развертывания реальной инфраструктуры в реальном окружении (например, в настоящей учетной записи AWS). Иными словами, модульные тесты для Terraform на самом деле являются интеграционными. Но я все равно предпочитаю называть их модульными, чтобы подчеркнуть нашу цель: протестировать отдельный (обобщенный) модуль и как можно быстрее получить результат.

Это означает, что базовая стратегия написания модульных тестов для Terraform подразумевает следующее.

1. Создание обобщенного автономного модуля.

2. Создание простого в развертывании примера для этого модуля.

3. Выполнение terraformapply для развертывания примера в реальной среде.

4. Проверка того, что развернутый вами код работает так, как вы ожидали. Этот этап зависит от типа инфраструктуры, которую вы тестируете: например, чтобы проверить балансировщик ALB, ему нужно послать HTTP-запрос и убедиться в том, что он возвращает тот ответ, который вы ожидаете.

5. Выполнение terraformdestroy в конце для очистки ресурсов.

Иными словами, вы выполняете все те же шаги, что и при ручном тестировании, но оформляете их в виде кода. Такой образ мышления хорошо подходит для создания автоматических тестов для кода Terraform: спросите себя, как бы вы проверили данный модуль, чтобы убедиться в его работе, и затем запрограммируйте этот тест.

Для написания тестов подходит любой язык программирования. В этой книге все тесты написаны на языке Go. Это позволяет использовать открытую библиотеку тестирования Terratest (http://bit.ly/2Tbzvch), которая поддерживает широкий спектр инструментов IaC (скажем, Terraform, Packer, Docker, Helm) в разнообразных окружениях (таких как AWS, Google Cloud, Kubernetes). Библиотека Terratest напоминает швейцарский армейский нож: в ней сотни инструментов, которые существенно упрощают тестирование инфраструктурного кода, включая полноценную поддержку только что описанной стратегии, когда вы развертываете код с помощью terraformapply, убеждаетесь, что он работает, и затем выполняете в конце terraformdestroy, чтобы очистить ресурсы.

Чтобы использовать Terratest, вам нужно сделать следующее.

1. Установить Go: golang.org/doc/install.

2. Настроить переменную среды GOPATH: golang.org/doc/code.html#GOPATH.

3. Добавить $GOPATH/bin в переменную среды PATH.

4. Установить Dep, диспетчер зависимостей для Go: golang.github.io/dep/docs/installation.html57.

5. Создать внутри GOPATH папку для тестов. Учитывая, что переменная GOPATH по умолчанию равна $HOME/go, вы могли бы создать $HOME/go/src/terraform-up-and-running.

6. Выполнить команду depinit в только что созданной вами папке. В результате у вас должны появиться файлы Gopkg.toml и Gopkg.lock, а также пустая папка vendors.