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 в действии — обнаружение проблем до того, как они попадут в продакшен.


Связанные статьи:

Внешние ресурсы: