継続は力なり

タイトル通り定期的な更新を心掛けるブログです。

AWS CDK のインフラストラクチャテストフレームワーク『Jest』での『Snapshot tests』実践

タダです.

来年の技術書典8に出ることが決まりました! テーマは AWS CDK の本です.AWS CDK の本を出すからこそむちゃくちゃ詳しくなれるようにより一層使い込みと勉強をしていきます.今回の記事では,テストフレームワークJest」の使い方を学びます.

AWS CDK におけるテストの現状

ドキュメントを見ると,テストでサポートされているのは TypeScript のみになります.「Jest」は JavaScript テストフレームワークAWS CDK の TypeScript のテストも可能です.

jestjs.io

テストの種類について

AWS CDK のテストには次の3種類の方法があります.今回は 「Snapshot tests」のやり方を学びます.「Snapshot tests」とは 作りたい CloudFormation テンプレート全体を準備して AWS CDK で作成したテンプレートが一致することを確認するためのテストです.AWS CDK の開発においては Integration test として利用されているそうです.

  • Snapshot tests(Golden master tests)
  • Fine-grained assertions tests
  • Validation tests

「Jest」の導入

それでは「Jest」の実践を公式ブログのチュートリアルに沿って使ってみます.公式ブログによると,AWS CDK の TypeScript の Construct ライブラリには @aws-cdk/assert というアサーションライブラリがあります.

aws.amazon.com

なお,環境は以下になります.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G2022
$ cdk --version
1.19.0 (build 5597bbe)

チュートリアル用プロジェクトの準備

チュートリアル用のプロジェクトを準備します.チュートリアルでは,SQS のデッドレターキューにアイテムがある場合にアラーム通知するよう CloudWatch を設定するケースのインフラとテストのコードを作ります.

$ cdk init --language typescript lib
Applying project template lib for typescript
Executing npm install...
npm WARN deprecated left-pad@1.3.0: use String.prototype.padStart()
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN jest-handson@0.1.0 No repository field.
npm WARN jest-handson@0.1.0 No license field.

# Welcome to your CDK TypeScript Construct Library project!

You should explore the contents of this project. It demonstrates a CDK Construct Library that includes a construct (`JestHandson`)
which contains an Amazon SQS queue that is subscribed to an Amazon SNS topic.

The construct defines an interface (`JestHandsonProps`) to configure the visibility timeout of the queue.

## Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `npm run test`    perform the jest unit tests
$ npm install @aws-cdk/aws-sqs @aws-cdk/aws-cloudwatch
npm WARN jest-handson@0.1.0 No repository field.
npm WARN jest-handson@0.1.0 No license field.

+ @aws-cdk/aws-sqs@1.19.0
+ @aws-cdk/aws-cloudwatch@1.19.0
updated 2 packages and audited 1755462 packages in 11.635s

14 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Snapshot tests の実践

インフラソースコードの準備

デッドレターキューを構成するソースコード

import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import sqs = require('@aws-cdk/aws-sqs');
import { Construct. Duration } from '@aws-cdk/core';

export class DeadLetterQueue extends sqs.Queue {
    public readonly messagesInQueueAlarm: cloudwatch.IAlarm;

    constructor(scope: Construct, id: string) {
        super(scope,id);

        // Add the alarm
        this.messagesInQueueAlarm = new cloudwatch.Alarm(this, 'Alarm', {
            alarmDescription: 'There are messages in the Dead Letter Queue',
            evaluationPeriods: 1,
            threshold: 1,
            metric: this.metricApproximateNumberOfMessagesVisible(),
        });
    }
}

テストコードの準備

Jest」と AWS CDK アサーションライブラリをインストールします.

$ npm install --save-dev jest @types/jest @aws-cdk/assert
npm WARN deprecated left-pad@1.3.0: use String.prototype.padStart()
npm WARN jest-handson@0.1.0 No repository field.
npm WARN jest-handson@0.1.0 No license field.

+ jest@24.9.0
+ @types/jest@24.0.24
+ @aws-cdk/assert@1.19.0
updated 3 packages and audited 1755462 packages in 35.82s

1 package is looking for funding
  run `npm fund` for details

found 0 vulnerabilities

package.json に「Jest」に関する定義を記載します.

{
  "name": "jest-handson",
  "version": "0.1.0",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest" <= 定義が必要
  },
  "devDependencies": {
    "@aws-cdk/assert": "^1.19.0",
    "@types/jest": "^24.0.24",<= 定義が必要
    "@types/node": "10.17.5",
    "jest": "^24.9.0",<= 定義が必要
    "ts-jest": "^24.1.0",
    "typescript": "~3.7.2"
  },
  "jest": {
    "moduleFileExtensions": ["js"]<= 定義が必要
  },
  "peerDependencies": {
    "@aws-cdk/core": "^1.19.0"
  },
  "dependencies": {
    "@aws-cdk/aws-cloudwatch": "^1.19.0",
    "@aws-cdk/aws-sns": "^1.19.0",
    "@aws-cdk/aws-sns-subscriptions": "^1.19.0",
    "@aws-cdk/aws-sqs": "^1.19.0",
    "@aws-cdk/core": "^1.19.0"
  }
}

テストコードのコンパイルと実行

ライブラリとpackage.json の定義が完了したら,テスト用のコードを書いていきます.キューの保持期間が2週間であることを確認するコードを書くのですが,今回は「Snapshot tests」を書きます.

import { SynthUtils } from '@aws-cdk/assert';
import { Stack } from '@aws-cdk/core';

import dlq = require('../lib/dead-letter-queue');

test('dlq creates an alarm', () => {
    const stack = new Stack();
    new dlq.DeadLetterQueue(stack, 'DLQ');
    expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot();
})

テストコードを書き終えたらテストコードをコンパイルして,ユニットテストを実行します.

$ npm run build                                          

> jest-handson@0.1.0 build /XXX/XXX/awscdk-handson/jest-handson
> tsc

lib/dead-letter-queue.ts:3:19 - error TS1005: ',' expected.

3 import { Construct. Duration } from '@aws-cdk/core';
                    ~


Found 1 error.

npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! jest-handson@0.1.0 build: `tsc`
npm ERR! Exit status 2
npm ERR! 
npm ERR! Failed at the jest-handson@0.1.0 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /XXX/XXX/.npm/_logs/2019-12-21T07_25_41_525Z-debug.log
$ npm run build

> jest-handson@0.1.0 build /XXX/XXX/awscdk-handson/jest-handson
> tsc
$ npm test

> jest-handson@0.1.0 test /Users/tada/awscdk-handson/jest-handson
> jest

 PASS  test/dead-letter-queue.test.ts
  ✓ dlq creates an alarm (132ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        5.396s, estimated 12s
Ran all test suites.

ユニットテストが終わると,test/__snapshots__というディレクトリができており, テストファイル名.test.ts.snap というCloudFormation テンプレートコピーが生成されます.

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`dlq creates an alarm 1`] = `
Object {
  "Resources": Object {
    "DLQ581697C4": Object {
      "Type": "AWS::SQS::Queue",
    },
    "DLQAlarm008FBE3A": Object {
      "Properties": Object {
        "AlarmDescription": "There are messages in the Dead Letter Queue",
        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
        "Dimensions": Array [
          Object {
            "Name": "QueueName",
            "Value": Object {
              "Fn::GetAtt": Array [
                "DLQ581697C4",
                "QueueName",
              ],
            },
          },
        ],
        "EvaluationPeriods": 1,
        "MetricName": "ApproximateNumberOfMessagesVisible",
        "Namespace": "AWS/SQS",
        "Period": 300,
        "Statistic": "Maximum",
        "Threshold": 1,
      },
      "Type": "AWS::CloudWatch::Alarm",
    },
  },
}
`;

インフラソースコードを変更した場合の「Snapshot tests」の実践

インフラのソースコードを変更した時はどのように再度テストを行うのでしょうか.その方法もみておきます.

CloudWatch Alarm の 間隔をデフォルト5分から1分に変更

インフラのソースコードの CloudWatch Alarm の 間隔をデフォルト5分から1分に変更して再度「Snapshot tests」を行います.

import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import sqs = require('@aws-cdk/aws-sqs');
import { Construct, Duration } from '@aws-cdk/core';

export class DeadLetterQueue extends sqs.Queue {
    public readonly messagesInQueueAlarm: cloudwatch.IAlarm;

    constructor(scope: Construct, id: string) {
        super(scope,id);

        // Add the alarm
        this.messagesInQueueAlarm = new cloudwatch.Alarm(this, 'Alarm', {
            alarmDescription: 'There are messages in the Dead Letter Queue',
            evaluationPeriods: 1,
            threshold: 1,
            metric: this.metricApproximateNumberOfMessagesVisible(),
            period: Duration.minutes(1), <= 追加
        });
    }
}

再度「Snapshot tests」の実践

テストを実行したところ Period 属性が 300 -> 60 になっていることを通知しています.

$ npm run build && npm test

> jest-handson@0.1.0 build /Users/tada/awscdk-handson/jest-handson
> tsc


> jest-handson@0.1.0 test /Users/tada/awscdk-handson/jest-handson
> jest

 FAIL  test/dead-letter-queue.test.ts
  ✕ dlq creates an alarm (69ms)

  ● dlq creates an alarm

    expect(received).toMatchSnapshot()

    Snapshot name: `dlq creates an alarm 1`

    - Snapshot
    + Received

    @@ -19,11 +19,11 @@
                },
              ],
              "EvaluationPeriods": 1,
              "MetricName": "ApproximateNumberOfMessagesVisible",
              "Namespace": "AWS/SQS",
    -         "Period": 300,
    +         "Period": 60,
              "Statistic": "Maximum",
              "Threshold": 1,
            },
            "Type": "AWS::CloudWatch::Alarm",
          },

       7 |     const stack = new Stack();
       8 |     new dlq.DeadLetterQueue(stack, 'DLQ');
    >  9 |     expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot();
         |                                                ^
      10 | });

      at Object.<anonymous> (test/dead-letter-queue.test.ts:9:48)1 snapshot failed.
Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or run `npm test -- -u` to update them.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        3.074s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

この変更が意図的であれば,npm test -- -uで「Snapshot tests」をコミットします.1 snapshot updated. とあるため,「Snapshot tests」が更新され,新しいアラームの期間がセットされます.

$ npm test -- -u           

> jest-handson@0.1.0 test /XXX/XXX/awscdk-handson/jest-handson
> jest "-u"

 PASS  test/dead-letter-queue.test.ts
  ✓ dlq creates an alarm (72ms)1 snapshot updated.
Snapshot Summary
 › 1 snapshot updated from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 updated, 1 total
Time:        3.059s
Ran all test suites.

テスト後の CloudFormation テンプレートコピーがどう変化しているかも確認します.こちらも Period 属性が 300 -> 60 に反映されているので意図した変更が反映されています.

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`dlq creates an alarm 1`] = `
Object {
  "Resources": Object {
    "DLQ581697C4": Object {
      "Type": "AWS::SQS::Queue",
    },
    "DLQAlarm008FBE3A": Object {
      "Properties": Object {
        "AlarmDescription": "There are messages in the Dead Letter Queue",
        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
        "Dimensions": Array [
          Object {
            "Name": "QueueName",
            "Value": Object {
              "Fn::GetAtt": Array [
                "DLQ581697C4",
                "QueueName",
              ],
            },
          },
        ],
        "EvaluationPeriods": 1,
        "MetricName": "ApproximateNumberOfMessagesVisible",
        "Namespace": "AWS/SQS",
        "Period": 60, <= 変更
        "Statistic": "Maximum",
        "Threshold": 1,
      },
      "Type": "AWS::CloudWatch::Alarm",
    },
  },
}
`;

まとめ

AWS CDK がサポートしている「Jest」の概要と「Snapshot tests」 の実践を行いました.他の2つのテスト手法も使い分けの説明ができるようにブログにしていきます.

関連記事

AWS CDK」の他の記事もぜひ!

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com