Написание тестов — необходимая неприятность. Я бы предпочел потратить свое время на написание функциональности, чем на написание кода для проверки того, что я написал, но ошибки случаются, и необходимо сохранить обратную совместимость. После того, как я закончил писать тесты, я всегда чувствую себя намного лучше в кодовой базе. По какой-то причине писать такой код просто не так приятно. Это одна из причин, почему я так долго тянул с написанием тестов для crow-api.

Еще одна важная причина, по которой я так долго писал тесты, заключается в том, что инфраструктура тестирования — это довольно новая вещь. Как мы можем протестировать конфигурацию виртуальной машины, созданную другой командой в собственном дата-центре? Эти сценарии также должны быть адаптированы и, вероятно, не стоят усилий. Скорее всего, будет проще написать E2E или интеграционные тесты после того, как код будет развернут на серверах. Я не ожидал найти много ресурсов в Интернете о тестировании конструкций и стеков CDK просто потому, что я полагал, что это достаточно ново.

Мое предположение об отсутствии документации и примеров было не так уж и далеко, но, как и во многих моих постах, я хотел бы внести свой вклад в то, чего мне не хватало.

Всякий раз, когда мы запускаем новое приложение CDK, инструмент автоматически создает папку test/, устанавливает jest и дает нам немного шаблонного кода. AWS явно хочет, чтобы мы писали тесты. Я обнаружил серьезную нехватку официальной документации по инструментам, которые CDK должен использовать для написания тестов. Модуль aws-cdk-lib.assertions — это все, что я смог найти (кто-нибудь, пожалуйста, укажите мне правильное направление, если я что-то упустил). Несмотря на то, что эта документация оказалась почти всем, что мне было нужно, все же было обескураживающе не найти многого другого.

Шаблонный код, предоставленный нам со свежим приложением CDK, выглядит следующим образом.

const app = new cdk.App();
  // WHEN
const stack = new ScheduleApi.ScheduleApiStack(app, 'MyTestStack');
  // THEN
const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::SQS::Queue', {
  VisibilityTimeout: 300
});

Первая строка должна выглядеть знакомо ( const app = new cdk.App(); ), потому что это то же самое, что инициализировать приложение всякий раз, когда мы хотим что-то развернуть. Создание стека такое же, const stack = new ScheduleApi.ScheduleApiStack(app, 'MyTestStack');. Как только мы добираемся до const template = Template.fromStack(stack);, все начинает расходиться. То, что я говорю с этого момента, основано на моих лучших знаниях на момент написания. Возможно, я не следую лучшим практикам, но я ничего не могу найти о лучших практиках.

Мне кажется, что лучший способ протестировать код CDK — это синтезировать код в стеки CloudFormation, а затем запускать утверждения для огромной строки, которая является шаблоном. Это то, что показывает шаблонный код, который генерирует CDK, а модуль aws-cdk-lib.assertions не показывает другого способа что-либо протестировать. Это означает, что props, присвоенное стекам, используемым в тестах, должно быть идентичным props, присвоенному стекам, развертываемым для корректной тестовой конфигурации.

Затем к Template, созданному в результате запуска Template.fromStack(), можно запросить ресурсы, сопоставления и выходные данные, используя методы класса Template. Методы, начинающиеся с has, будут выдавать ошибки, если соответствующий ресурс в шаблоне не будет найден, а методы, начинающиеся с find, будут возвращать сами ресурсы, а также их логические идентификаторы.

Я собираюсь показать несколько примеров из тестов, которые я написал для crow-api. (Эти тесты могут измениться, но точный файл фиксации, на который я ссылаюсь, находится здесь.)

Один из написанных мной первых и самых простых тестов выглядит следующим образом.

template.hasResourceProperties('AWS::ApiGateway::RestApi', {
  Name: 'testing-crow-api',
});

Этот вызов просто утверждает, что шаблон содержит ресурс RestApi со свойством Name, установленным на testing-crow-api. Обратите внимание, что свойство Name ссылается на имя из шаблона CloudFormation, а не на свойство из кода CDK ( restApiName).

Следующие тесты, которые я написал, стали усложняться. Я хотел начать тестирование того, что шлюзы API Resource указывают на правильных родителей. С CDK это просто, но для того, чтобы заставить CloudFormation работать, нужно сделать больше. Логический идентификатор ресурса указан в шаблоне CloudFormation, но с кодом CDK мы не взаимодействуем с логическими идентификаторами. Затем вопрос превращается в вопрос извлечения логического идентификатора из стека CDK или файла Template. Для этого первого примера мне удалось получить идентификатор из стека CDK.

function getLogicalId(stack: cdk.Stack, resource: cdk.IResource) {
  return stack.getLogicalId(resource.node.findChild('Resource') as cdk.CfnElement);
}

const restApiLogicalId = getLogicalId(stack, stack.api.gateway);

template.hasResourceProperties('AWS::ApiGateway::Resource', {
  ParentId: {
    'Fn::GetAtt': [
      restApiLogicalId,
      'RootResourceId',
    ],
  },
  PathPart: 'v1',
  RestApiId: {
    Ref: restApiLogicalId,
  },
});

Следующий пример становится немного сложнее. Сначала мне нужно было использовать свойства findResources Template, уникальные для конкретного ресурса, затем получить логический идентификатор из результата вызова findResources и, наконец, использовать логический идентификатор в вызове hasResourceProperties.

function logicalIdFromResource(resource: any) {
  try {
    const resKeys = Object.keys(resource);
    if (resKeys.length !== 1) {
      throw new Error('Resource is not unique.');
    }
    const [logicalId] = resKeys;
    return logicalId;
  } catch (err) {
    console.log(resource);
    throw err;
  }
}

const authorsPath = template.findResources('AWS::ApiGateway::Resource', {
  Properties: {
    PathPart: path,
  },
});
const v1AuthorsGetLambda = template.findResources('AWS::Lambda::Function', {
  Properties: {
    TracingConfig: {
      Mode: 'Active',
    },
  },
});

const authorsLogicalId = logicalIdFromResource(authorsPath);
const v1AuthorsGetLambdaLogicalId = logicalIdFromResource(v1AuthorsGetLambda);

template.hasResourceProperties('AWS::ApiGateway::Method', {
  HttpMethod: 'GET',
  ResourceId: {
    Ref: authorsLogicalId,
  },
  RestApiId: {
    Ref: restApiLogicalId,
  },
  Integration: {
    Uri: {
      'Fn::Join': [
        '',
        [
          'arn:',
          { Ref: 'AWS::Partition' },
          ':apigateway:',
          { Ref: 'AWS::Region' },
          ':lambda:path/2015-03-31/functions/',
          {
            'Fn::GetAtt': [
              v1AuthorsGetLambdaLogicalId,
              'Arn',
            ],
          },
          '/invocations',
        ],
      ],
    },
  },
});

В коде примера есть некоторые изменения по сравнению с постоянной ссылкой, но идея та же.

Хотя функции, доступные для использования, могут быть не самыми полными по сравнению с тем, что мы могли бы сделать, я, по крайней мере, смог найти способ протестировать то, что хотел. Я надеюсь, что мои мысли и примеры помогли кому-то на этом пути. Насколько мне известно, авторы CDK задумали эти примеры, но если я узнаю что-то другое позже, я либо обновлю этот пост, либо сделаю дополнительный пост. А пока удачного кодирования!

Первоначально опубликовано на https://thomasstep.com 22 января 2022 г.

Больше контента на plainenglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Получите эксклюзивный доступ к возможностям написания и советам в нашем сообществе Discord.