def do_GET(request, response)
web_service = WebService.new("http://www.example.org")
handlers = Handlers.new(web_service)
status_code, content_type, body = handlers.handle(request.path)
response.status = status_code
response['Content-Type'] = content_type
response.body = body
end
end
Вы можете создать в коде своего теста mock-объект класса WebService, который позволяет указать фиктивный ответ:
class MockWebService
def initialize(response)
@response = response
end
def proxy
@response
end
end
Теперь вы можете создать экземпляр этого класса MockWebService и внедрить его в класс Handlers в своих модульных тестах:
def test_unit_web_service
expected_status = 200
expected_content_type = 'text/html'
expected_body = 'mock example.org'
mock_response = [expected_status, expected_content_type, expected_body]
mock_web_service = MockWebService.new(mock_response)
handlers = Handlers.new(mock_web_service)
status_code, content_type, body = handlers.handle("/web-service")
assert_equal(expected_status, status_code)
assert_equal(expected_content_type, content_type)
assert_equal(expected_body, body)
end
Выполните тесты еще раз, чтобы убедиться в том, что они по-прежнему работают:
$ ruby web-server-test.rb
Loaded suite web-server-test
Started
...
Finished in 0.000645 seconds.
--------------------------------------------
4 tests, 12 assertions, 0 failures, 0 errors
100% passed
--------------------------------------------
Превосходно! Этот подход минимизирует внешние зависимости, позволяя писать быстрые и надежные тесты, а также проверять всевозможные крайние случаи. И, поскольку три тестовых случая, которые вы добавили ранее, по-прежнему успешно выполняются, вы можете быть уверены, что ваш рефакторинг ничего не сломал.
Теперь вернемся к Terraform и посмотрим, как внедрение зависимостей будет выглядеть для наших модулей. Начнем с hello-world-app. Сперва следует создать простой в развертывании пример в папке examples (если вы этого еще не сделали):
provider "aws" {
region = "us-east-2"
# Разрешаем любую версию провайдера AWS вида 2.x
version = "~> 2.0"
}
module "hello_world_app" {
source = "../../../modules/services/hello-world-app"
server_text = "Hello, World"
environment = "example"
db_remote_state_bucket = "(YOUR_BUCKET_NAME)"
db_remote_state_key = "examples/terraform.tfstate"
instance_type = "t2.micro"
min_size = 2
max_size = 2
enable_autoscaling = false
}
Проблема с зависимостями сразу бросается в глаза: hello-world-app предполагает, что вы уже развернули модуль mysql, и требует, чтобы вы предоставили информацию о бакете S3, в котором mysql хранит свое состояние. Для этого предусмотрены аргументы db_remote_state_bucket и db_remote_state_key. Наша цель — создать для hello-world-app модульный тест и по возможности минимизировать число внешних зависимостей, хотя свести его к нулю в Terraform не получится.
Один из первых шагов по минимизации зависимостей состоит в прояснении того, от чего зависит ваш модуль. Для этого все источники данных и ресурсы, представляющие внешние зависимости, можно вынести в отдельный файл dependencies.tf. Так будет выглядеть файл modules/services/hello-world-app/dependencies.tf:
data "terraform_remote_state" "db" {
backend = "s3"
config = {
bucket = var.db_remote_state_bucket
key = var.db_remote_state_key
region = "us-east-2"
}
}
data "aws_vpc" "default" {
default = true
}
data "aws_subnet_ids" "default" {
vpc_id = data.aws_vpc.default.id
}
Это помогает пользователям вашего кода понять с первого взгляда, на какие внешние компоненты он полагается. В случае с модулем hello-world-app можно сразу же увидеть, что он зависит от базы данных, VPC и подсетей. Но как внедрить зависимости снаружи модуля, чтобы их можно было заменить во время тестирования? Вы уже знаете ответ: входные переменные.
Для каждой из этих зависимостей в файле modules/services/hello-world-app/variables.tf нужно добавить входную переменную:
variable "vpc_id" {
description = "The ID of the VPC to deploy into"
type = string
default = null
}
variable "subnet_ids" {
description = "The IDs of the subnets to deploy into"
type = list(string)
default = null
}
variable "mysql_config" {
description = "The config for the MySQL DB"
type = object({
address = string
port = number
})
default = null
}
Теперь у нас есть по одной входной переменной для VPC ID, идентификаторов подсетей и конфигурации MySQL. У каждой есть поле default, поэтому все они опциональные: пользователь может указать для них собственные значения или оставить default. Значение по умолчанию, которое используется для этих переменных, вам еще не встречалось: null. Если бы вместо этого в качестве default было указано пустое значение (например, пустая строка для vpc_id или пустой список для subnet_ids), вы бы не смогли отличить его от такого же значения, сознательно установленного пользователем. В таких случаях может пригодиться значение null, так как оно сигнализирует о том, что переменная не задана и что пользователь полагается на поведение по умолчанию.
Обратите внимание: переменная mysql_config имеет конструктор типа object для создания вложенного типа с ключами address и port. Этот тип специально предназначен для того, чтобы соответствовать выходным типам модуля mysql:
output "address" {
value = aws_db_instance.example.address
description = "Connect to the database at this endpoint"
}
output "port" {
value = aws_db_instance.example.port
description = "The port the database is listening on"
}
Одно из преимуществ этого состоит в том, что по завершении рефакторинга у вас появится возможность совместного использования модулей hello-world-app и mysql следующим образом:
module "hello_world_app" {
source = "../../../modules/services/hello-world-app"
server_text = "Hello, World"
environment = "example"
# Все выходные переменные из модуля mysql передаются напрямую!
mysql_config = module.mysql
instance_type = "t2.micro"
min_size = 2
max_size = 2
enable_autoscaling = false
}
module "mysql" {
source = "../../../modules/data-stores/mysql"
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password
}
Поскольку типы mysql_config и выходных переменных модуля mysql совпадают, вы можете передавать их напрямую одной строчкой. И если типы когда-нибудь поменяются и перестанут совпадать, Terraform сразу же выдаст вам ошибку, чтобы вы знали, что их нужно обновить. Это пример не простой, а типобезопасной композиции функций.
Но, чтобы это заработало, нужно закончить рефакторинг кода. Поскольку конфигурацию MySQL можно передавать в качестве ввода, значит, переменные db_remote_state_bucket и db_remote_state_key должны теперь быть опциональными, поэтому присвойте им значение null по умолчанию:
variable "db_remote_state_bucket" {
description = "The name of the S3 bucket for the DB's Terraform state"
type = string
default = null
}
variable "db_remote_state_key" {
description = "The path in the S3 bucket for the DB's Terraform state"
type = string
default = null
}
Дальше используйте параметр count, чтобы опционально создать в файле modules/services/hello-world-app/dependencies.tf три источника данных в зависимости от того, установлено ли значение null соответствующей входной переменной:
data "terraform_remote_state" "db" {
count = var.mysql_config == null ? 1 : 0
backend = "s3"
config = {
bucket = var.db_remote_state_bucket
key = var.db_remote_state_key
region = "us-east-2"
}
}
data "aws_vpc" "default" {
count = var.vpc_id == null ? 1 : 0
default = true
}
data "aws_subnet_ids" "default" {
count = var.subnet_ids == null ? 1 : 0
vpc_id = data.aws_vpc.default.id
}
Теперь нужно обновить все ссылки на эти источники данных так, чтобы пользователь мог выбирать между источником и входной переменной. Оформим это в виде локальных значений:
locals {
mysql_config = (
var.mysql_config == null
? data.terraform_remote_state.db[0].outputs
: var.mysql_config
)
vpc_id = (
var.vpc_id == null
? data.aws_vpc.default[0].id
: var.vpc_id
)
subnet_ids = (
var.subnet_ids == null
? data.aws_subnet_ids.default[0].ids
: var.subnet_ids
)
}
Заметьте, что источники данных теперь являются массивами, так как они используют параметр count, поэтому каждый раз при ссылке на них нужно применять синтаксис поиска по массиву (например, [0]). Пройдитесь по коду и вместо ссылок на эти источники подставьте ссылки на соответствующие локальные значения. Сначала сделайте так, чтобы источник aws_subnet_ids применял local.vpc_id:
data "aws_subnet_ids" "default" {
count = var.subnet_ids == null ? 1 : 0
vpc_id = local.vpc_id
}
Затем присвойте local.subnet_ids параметрам subnet_ids в модулях asg и alb: