TL;DR
- Ключевое преимущество Pulumi: используйте нативные тестовые фреймворки вашего языка (Jest, pytest, Go testing) вместо изучения инструментов, специфичных для HCL
- Юнит-тесты с
pulumi.runtime.setMocks()работают в 60 раз быстрее интеграционных — одна команда сократила время выполнения набора тестов с 20 минут до 20 секунд- Property-тесты (CrossGuard) обнаруживают нарушения политик во время деплоя, а не после — shift-left для инфраструктуры
Подходит для: Команд, использующих TypeScript, Python или Go, которые хотят интегрировать тестирование инфраструктуры в CI/CD Пропустите если: У вас простая инфраструктура (<10 ресурсов) или вы предпочитаете экосистему Terraform Время чтения: 11 минут
Тестирование инфраструктурного кода в 2026 году — не опция, а необходимый минимум. Но вот что большинство руководств не скажут: подход к тестированию в Pulumi принципиально отличается от Terraform, и именно это различие заставляет компании переходить на него.
Пока команды Terraform обращаются к внешним инструментам вроде Terratest или полагаются на terraform validate, Pulumi позволяет писать тесты на том же языке, что и вашу инфраструктуру. Ваши знания pytest? Работают. Ваша настройка Jest? Работает. CI/CD pipeline вашей команды? Специальные плагины не нужны.
Пирамида Тестирования для Инфраструктуры
Прежде чем переходить к коду, поймите, где какой тип теста находится:
| Тип Теста | Скорость | Что Валидирует | Когда Выполняется |
|---|---|---|---|
| Юнит-тесты | ~20 секунд | Логика, конфигурация, свойства ресурсов | Каждый коммит |
| Property-тесты | Во время деплоя | Соответствие политикам, правила безопасности | pulumi up |
| Интеграционные тесты | 5-30 минут | Реальное поведение в облаке, end-to-end потоки | Pre-merge, ночные |
Большинство команд совершают ошибку: пропускают юнит-тесты (“инфраструктура — это другое”) и полагаются только на интеграционные тесты. Результат? Медленные циклы обратной связи и нестабильные CI pipelines.
Юнит-Тестирование с Моками
Система мокирования Pulumi перехватывает все вызовы к облачному провайдеру, позволяя тестировать конфигурацию ресурсов без реального provisioning.
TypeScript с Jest
import * as pulumi from "@pulumi/pulumi";
import "jest";
describe("S3 bucket configuration", () => {
let infra: typeof import("./index");
beforeAll(() => {
pulumi.runtime.setMocks({
newResource: (args: pulumi.runtime.MockResourceArgs) => ({
id: `${args.name}-id`,
state: {
...args.inputs,
arn: `arn:aws:s3:::${args.inputs.bucket || args.name}`,
},
}),
call: (args) => args.inputs,
});
});
beforeEach(async () => {
infra = await import("./index");
});
it("enables versioning on production buckets", (done) => {
infra.dataBucket.versioning.apply(versioning => {
expect(versioning?.enabled).toBe(true);
done();
});
});
it("blocks public access by default", (done) => {
infra.dataBucket.acl.apply(acl => {
expect(acl).toBe("private");
done();
});
});
});
Критический паттерн: Импортируйте модуль инфраструктуры после настройки моков. Паттерн beforeAll/beforeEach гарантирует, что моки активны до создания ресурсов Pulumi.
Python с pytest
import unittest
import pulumi
class InfraMocks(pulumi.runtime.Mocks):
def new_resource(self, args: pulumi.runtime.MockResourceArgs):
state = {
**args.inputs,
"arn": f"arn:aws:s3:::{args.inputs.get('bucket', args.name)}",
}
return [f"{args.name}_id", state]
def call(self, args: pulumi.runtime.MockCallArgs):
return {}
pulumi.runtime.set_mocks(InfraMocks(), preview=False)
# Импорт ПОСЛЕ настройки моков
from infra import data_bucket
class TestS3Bucket(unittest.TestCase):
@pulumi.runtime.test
def test_versioning_enabled(self):
def check_versioning(versioning):
self.assertTrue(versioning.get("enabled"))
return data_bucket.versioning.apply(check_versioning)
@pulumi.runtime.test
def test_encryption_configured(self):
def check_encryption(rules):
self.assertIsNotNone(rules)
self.assertGreater(len(rules), 0)
return data_bucket.server_side_encryption_configuration.apply(
lambda c: check_encryption(c.rules) if c else self.fail("No encryption")
)
Декоратор @pulumi.runtime.test обрабатывает асинхронные типы Output в Pulumi, делая ассерты чище.
Property-Тесты с CrossGuard
Property-тесты обеспечивают инварианты во время деплоя. В отличие от юнит-тестов, которые выполняются изолированно, property-тесты видят реальный граф ресурсов, который строит Pulumi.
import * as policy from "@pulumi/policy";
const securityPolicies = new policy.PolicyPack("security", {
policies: [
{
name: "s3-no-public-read",
description: "S3 buckets must not allow public read access",
enforcementLevel: "mandatory",
validateResource: policy.validateResourceOfType(
aws.s3.Bucket,
(bucket, args, reportViolation) => {
if (bucket.acl === "public-read" ||
bucket.acl === "public-read-write") {
reportViolation("S3 bucket has public read access");
}
}
),
},
{
name: "ec2-approved-instance-types",
description: "EC2 instances must use approved types",
enforcementLevel: "mandatory",
validateResource: policy.validateResourceOfType(
aws.ec2.Instance,
(instance, args, reportViolation) => {
const approved = ["t3.micro", "t3.small", "t3.medium"];
if (!approved.includes(instance.instanceType)) {
reportViolation(
`Instance type ${instance.instanceType} not approved. ` +
`Use: ${approved.join(", ")}`
);
}
}
),
},
],
});
Запуск политик: pulumi up --policy-pack ./policy
Когда политики сияют: Для обеспечения организационных стандартов во всех стеках. Один policy pack — все команды получают пользу.
Интеграционное Тестирование с Automation API
Когда нужно проверить реальное поведение в облаке — а не только конфигурацию — используйте интеграционные тесты с Automation API от Pulumi.
import { LocalWorkspace } from "@pulumi/pulumi/automation";
import * as aws from "@aws-sdk/client-s3";
describe("S3 infrastructure", () => {
let stack: any;
const stackName = `test-${Date.now()}`;
beforeAll(async () => {
stack = await LocalWorkspace.createOrSelectStack({
stackName,
projectName: "s3-test",
program: async () => {
const bucket = new aws.s3.Bucket("test-bucket", {
versioning: { enabled: true },
});
return { bucketName: bucket.id };
},
});
await stack.setConfig("aws:region", { value: "us-west-2" });
await stack.up({ onOutput: console.log });
}, 300000); // 5 минут таймаут
afterAll(async () => {
await stack.destroy({ onOutput: console.log });
await stack.workspace.removeStack(stackName);
});
it("creates a bucket with versioning", async () => {
const outputs = await stack.outputs();
const s3Client = new aws.S3Client({ region: "us-west-2" });
const versioning = await s3Client.send(
new aws.GetBucketVersioningCommand({
Bucket: outputs.bucketName.value,
})
);
expect(versioning.Status).toBe("Enabled");
});
});
Предупреждение: Интеграционные тесты создают реальные ресурсы. Всегда:
- Используйте уникальные имена стеков с временными метками
- Реализуйте правильную очистку в
afterAll - Устанавливайте подходящие таймауты (облачные операции медленные)
- Запускайте в изолированных AWS-аккаунтах или с алертами на бюджет
Подходы с Использованием ИИ
В 2026 году инструменты ИИ значительно ускоряют тестирование инфраструктуры. Вот где они преуспевают.
Что ИИ делает хорошо:
- Генерация конфигураций моков из схем ресурсов
- Написание правил валидации свойств на основе требований безопасности
- Создание вариаций тестовых данных для граничных случаев
- Предложение ассертов, которые вы могли упустить
Что всё ещё требует людей:
- Решение, какие ресурсы критичны для тестирования
- Понимание бизнес-контекста для правил валидации
- Ревью тестов, сгенерированных ИИ, чтобы избежать ложной уверенности
- Проектирование общей стратегии тестирования
Полезный промпт для генерации юнит-тестов:
Дано определение ресурса Pulumi:
const bucket = new aws.s3.Bucket("data-bucket", {
versioning: { enabled: true },
serverSideEncryptionConfiguration: {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: "aws:kms",
},
},
},
lifecycleRules: [{
enabled: true,
transitions: [{ days: 30, storageClass: "STANDARD_IA" }],
}],
});
Сгенерируй юнит-тесты на Jest, которые проверяют:
1. Версионирование включено
2. KMS-шифрование настроено
3. Lifecycle-переходы установлены корректно
Используй паттерн setMocks из Pulumi и правильно обрабатывай типы Output.
Фреймворк Принятия Решений
Когда Использовать Юнит-Тесты
Этот подход работает лучше всего когда:
- У вас сложная условная логика в создании ресурсов
- Несколько окружений используют общий инфраструктурный код с разными конфигами
- Ваша команда уже использует фреймворки тестирования TypeScript/Python
- Вам нужна быстрая обратная связь в CI (< 1 минуты)
Рассмотрите альтернативы когда:
- Инфраструктура чисто декларативная без логики
- Вы прототипируете и ожидаете частые изменения
- Команда новичок в Pulumi (начните с property-тестов)
Когда Использовать Property-Тесты
Этот подход работает лучше всего когда:
- Обеспечиваете безопасность/compliance во всех стеках
- У вас есть организационные стандарты для поддержки
- Команды работают независимо, но разделяют требования
- Вам нужны guardrails во время деплоя
Рассмотрите альтернативы когда:
- Политики специфичны для стека (используйте юнит-тесты)
- Нужно тестировать реальное поведение в облаке (используйте интеграционные)
- Ваши политики требуют внешних данных (сложно)
Когда Использовать Интеграционные Тесты
Этот подход работает лучше всего когда:
- Тестируете взаимодействия между облачными сервисами
- Валидируете, что IAM-права работают как ожидается
- Проверяете сетевое подключение и DNS-разрешение
- Smoke-тесты перед продакшеном
Рассмотрите альтернативы когда:
- Нужна быстрая обратная связь (используйте юнит-тесты)
- Тестируете конфигурацию, а не поведение (используйте юнит-тесты)
- Жёсткие ограничения по стоимости или времени
Измерение Успеха
| Метрика | Базовая Линия | Цель | Как Отслеживать |
|---|---|---|---|
| Покрытие юнит-тестами | 0% | 80%+ условной логики | Инструменты покрытия (nyc, coverage.py) |
| Время выполнения юнит-тестов | N/A | < 60 секунд | Метрики CI pipeline |
| Нарушения property-тестов | Неизвестно | 0 в продакшене | Отчёты policy pack |
| Нестабильность интеграционных тестов | Высокая | < 5% частота отказов | Анализ сбоев CI |
| Среднее время обнаружения проблем | Часы/дни | Минуты | Трекинг инцидентов |
Предупреждающие знаки, что не работает:
- Юнит-тесты проходят, но деплои падают — моки не соответствуют реальности
- Интеграционные тесты занимают > 30 минут — слишком медленно для значимой обратной связи
- Нарушения политик обнаруживаются в продакшене — политики недостаточно полные
- Высокая нагрузка на поддержку тестов — тесты слишком детализированы
Распространённые Ошибки
1. Тестирование Реализации, а не Поведения
// Плохо: Тестирование внутренней структуры
expect(bucket.tags).toEqual({ Name: "my-bucket", Env: "prod" });
// Хорошо: Тестирование значимого поведения
expect(bucket.versioning?.enabled).toBe(true);
2. Забыть про Unwrapping Output
Ресурсы Pulumi возвращают Output<T>, а не T. Всегда используйте .apply() или тестовый декоратор.
# Это молча провалится
def test_bad(self):
self.assertEqual(bucket.arn, "expected-arn") # Сравнивается объект Output!
# Это работает
@pulumi.runtime.test
def test_good(self):
return bucket.arn.apply(lambda arn: self.assertIn("s3", arn))
3. Неполные Моки
Если ваши тесты проходят, но деплои падают, ваши моки могут не соответствовать реальному поведению провайдера.
newResource: (args) => {
// Реалистично: включить вычисляемые поля
if (args.type === "aws:s3/bucket:Bucket") {
return {
id: args.name,
state: {
...args.inputs,
arn: `arn:aws:s3:::${args.inputs.bucket}`,
bucketDomainName: `${args.inputs.bucket}.s3.amazonaws.com`,
region: "us-west-2",
},
};
}
// Общий fallback
return { id: args.name, state: args.inputs };
}
Что Дальше
Начните с малого: выберите один критичный ресурс (вашу продакшен-базу данных, основной S3-бакет или конфигурацию VPC) и напишите для него три юнит-теста. Используйте паттерны Jest/pytest выше.
Когда освоитесь, добавьте policy pack с вашим главным требованием безопасности. Это shift-left в действии — обнаружение проблем до того, как они попадут в продакшен.
Связанные статьи:
- Стратегии Тестирования и Валидации Terraform
- Стратегия Пирамиды Автоматизации Тестов
- Тестирование Шаблонов CloudFormation
- Отчётность по Тестам в CI/CD
Внешние ресурсы: