LoginSignup
10
6

More than 1 year has passed since last update.

CDKv2で@aws-cdk/assertionsを試してみる

Last updated at Posted at 2021-12-02

これは CDK Advent Calendar 2021 の 2日目の記事です。

こんにちは!るね*(@lune_sta)です。
本日(2021年12月2日)、CDKv2がStableになりました!めでたいですね!!

Screen Shot 2021-12-02 at 20.07.33.png

v1からv2への変更点、移行方法については公式ドキュメントやAWS Blogの
AWS Cloud Development Kit v2 開発者プレビューのお知らせでも紹介していますので、合わせてご覧ください。

この記事のコードはGitHubにあります。CDKv2ベースです。

CDKのテストにまつわる話

CDKではアプリケーションのコードをテストするのと同じように、インフラのコードに対してテストを書けます。以前までは@aws-cdk/assertというモジュールを使ったテストが一般的でしたが、このモジュールはTypeScript/JavaScript専用なのがネックでした。
そのため、すべての言語に対応した @aws-cdk/assertions というモジュールが新しく開発され、こちらはCDKv1では2020年11月リリースの v1.132.0 からStableとして利用できるようになっています。もちろんCDKv2でも使用できます。
最近のバージョンではcdk initで作成されるテストの雛形も @aws-cdk/assert ではなく @aws-cdk/assertions に変わっており、今後はこちらが主流になっていくと思われます。

@aws-cdk/assertions の使い方については、AWS Blogのあらゆる言語でのCDKアプリケーションのテストで紹介していますが、この記事では実際にシンプルなコードに対してテストを書いてみて感触を確かめてみます。

テスト対象のプログラム

今回は、下記のようなシンプルなコードに対してテストする例を考えてみます。
Untitled Diagram.drawio.png

    const db = new dynamodb.Table(this, 'Table', {
      partitionKey: {
        name: 'itemId',
        type: dynamodb.AttributeType.STRING,
      },
      tableName: 'items',
      removalPolicy: RemovalPolicy.DESTROY,
    })

    const backend = new lambda.NodejsFunction(this, 'Function', {
      entry: './lambda/index.ts',
    })

    db.grantReadWriteData(backend)

    new apigateway.LambdaRestApi(this, 'Api', {
      handler: backend
    })

テストの始め方

@aws-cdk/assertions を使うには、まずテスト対象のStackのインスタンスを生成し、Template.fromStack() を使用してCloudformationテンプレートを生成する所から始まります。
Template.fromString()Template.fromJSON()なども用意されているので文字列やObjectからテンプレートを生成することもできます。

import { Template } from 'aws-cdk-lib/assertions'
import { App } from 'aws-cdk-lib'
import { CdkAssertionsSamplesStack } from '../lib/cdk-assertions-samples-stack'

test('Test name', () => {
    const app = new App();
    const stack = new CdkTestSamplesStack(app, 'CdkTestSamplesStack', {})
    const template = Template.fromStack(stack)

Full Template Match

一番シンプルなアサーションとして紹介されているのが、完成形のテンプレートと比較するFull Template Matchです。

    const expected = {
        'Resources': {
            'TableCD117FA1': {
                'Type': 'AWS::DynamoDB::Table',
                'Properties': {
                    'KeySchema': [
                        {
                            'AttributeName': 'itemId',
                            'KeyType': 'HASH'
                        }
                    ],
                    (中略)
    }

    template.templateMatches(expected)

templateMatches() に限らず、@aws-cdk/assertions のマッチ系のメソッドではデフォルトで完全一致ではなく、"オブジェクトライク"な比較が行われます。そのため実際のテンプレートは期待する値の上位集合(superset)であることが許容されます。イメージとしては下記のような感じです。完全一致を行いたい場合は後述するマッチャーを使います。

// テスト対象のテンプレート
// {
//   "Resources": {
//     "A": {
//       "Type": "Foo::Bar",
//       "Properties": {...}
//     },
//     "B": {
//       "Type": "BAZ::QUX",
//       "Properties": {...}
//     }
//   }
// }

// これはエラーにならない
const expected = {
   'Resources': {
     'A': {
       'Type': 'Foo::Bar',
       'Properties': {...}
     }
}
template.templateMatches(expected)

// これはエラーになる
const expected = {
   'Resources': {
     'C': {
       'Type': 'QUUX::CORGE',
       'Properties': {...}
     }
}
template.templateMatches(expected)

実際にFull Template Matchをする際は、手作業で期待するテンプレートを管理するよりスナップショットテストとして使うことが多いと思います。スナップショットテストは簡単で、TypeScriptでJestを使用する場合は下記のように書くだけでスナップショットの保存と比較が行われます。

    // スナップショットとの比較を行う
    expect(template.toJSON()).toMatchSnapshot()

テスト対象のテンプレートに意図して変更を加えた場合は、以下のコマンドでスナップショットを更新できます。詳しくはSnapshot Testingをご覧ください。

$ jest --updateSnapshot

CDKから生成されるCFnテンプレートは、CDKフレームワークとCDKアプリケーションの2つのコードの影響を受けます。スナップショットテストを使うことで、CDKのバージョンが変化してもデプロイされるリソースに影響が無いことを確認できます。また、CDKアプリケーションのリファクタをする際にも有用です。

Counting Resources

resourceCountIs()を使うと対象の種類のリソースの数を確認できます。

    // Functionが1つ作られていることを確認する
    template.resourceCountIs('AWS::Lambda::Function', 1)

Resource Matching & Retrieval

テンプレート全体ではなく、特定のリソースの Properties を確認したい場合は hasResourceProperties() を使います。
Properties だけでなくリソース全体(UpdateReplacePolicyDependsOnなど)を確認する場合は hasResource() を使います。

    // Propertiesが正しいことを確認する
    template.hasResourceProperties('AWS::Lambda::Function', {
        'Handler': 'index.handler'
    });

    // Properties以外を確認したい場合はhasResource()を使う
    template.hasResource('AWS::DynamoDB::Table', {
        'UpdateReplacePolicy': 'Delete'
    })

似たようなものに findResources() メソッドがあり、アサーションではなくマッチするリソースのセットを取得できます。

    // 生成されるIAM Roleをすべて表示する
    console.log(template.findResources('AWS::IAM::Role'))

Output and Mapping sections

OutputやMappingに対しても似たようなメソッドどして hasOutput()findOutput()hasMapping()findMappings() が利用できます。その場合、引数としてlogicalIdを指定しますが、*を指定して全てを対象にすることもできます。
(logicallIdはランダムに生成されるため、一部を * にして ApiEndpoint* のような書き方ができると便利そうですが、できませんでした。)

const expected = {
  Value: '...'
}

template.hasOutput('*', expected)

Special Matchers

hasXxx()findXxx()templateMatches() で使用する期待する値は、今までの例のようなリテラル値以外にもマッチャーを使用できます。

Object Matchers

Match.objectLike() を使うとテスト対象のオブジェクトがパターンの上位集合(superset)であることを確認できます。これは マッチャーを使わない場合と同じ挙動です。
Match.objectEquals() を使うと完全一致のアサートをすることができます。

    // Match.objectLike() は部分一致
    template.hasResourceProperties('AWS::DynamoDB::Table', {
        'ProvisionedThroughput': Match.objectLike({
            'ReadCapacityUnits': 5
        })
    })

    // Match.objectEquals() は完全一致
    template.hasResourceProperties('AWS::DynamoDB::Table', {
        'ProvisionedThroughput': Match.objectEquals({
            'ReadCapacityUnits': 5,
            'WriteCapacityUnits': 5
        })
    })

Presence and Absence

Match.absent() を使うと該当する値が存在しないことを確認できます。逆に値が存在することだけを確認したい場合は Match.anyValue() を使います。

    // 存在しないことを確認する
    template.hasResourceProperties('AWS::DynamoDB::Table', {
        'DummyKey': Match.absent()
    })

    // 存在することを確認する
    template.hasResourceProperties('AWS::DynamoDB::Table', {
        'TableName': Match.anyValue()
    })

Array Matchers

配列に対しては Match.arrayWith()Match.arrayEquals() が使用できます。
今回のテンプレートでは良い例が思いつきませんでしたが、公式ドキュメントには以下のような例が紹介されていました。

// Given a template -
// {
//   "Resources": {
//     "MyBar": {
//       "Type": "Foo::Bar",
//       "Properties": {
//         "Fred": ["Flob", "Cat"]
//       }
//     }
//   }
// }

// The following will NOT throw an assertion error
const expected = {
  Fred: Match.arrayWith(['Flob']),
};
template.hasResourceProperties('Foo::Bar', expected);

// The following will throw an assertion error
const unexpected = Match.objectLike({
  Fred: Match.arrayWith(['Wobble']),
});
template.hasResourceProperties('Foo::Bar', unexpected);

Not Matcher

Match.not()を使うとマッチ結果を反転させることができます。
以下も公式ドキュメントの例になります。

// Given a template -
// {
//   "Resources": {
//     "MyBar": {
//       "Type": "Foo::Bar",
//       "Properties": {
//         "Fred": ["Flob", "Cat"]
//       }
//     }
//   }
// }

// The following will NOT throw an assertion error
const expected = {
  Fred: Match.not(['Flob']),
};
template.hasResourceProperties('Foo::Bar', expected);

// The following will throw an assertion error
const unexpected = Match.objectLike({
  Fred: Match.not(['Flob', 'Cat']),
});
template.hasResourceProperties('Foo::Bar', unexpected);

Serialized JSON

公式ドキュメント

Capturing Values

Capture() を使用するとマッチした値をキャプチャーできます。

    // 値をキャプチャーするにはCapture()を使う
    const capture = new Capture()
    template.hasResourceProperties('AWS::Lambda::Function', {
        Runtime: capture
    })
    console.log(capture.asString())
    // nodejs14.x

テストを書いてみた感想

@aws-cdk/assertions を使ったテストは、生成されるCFnテンプレートへのアサーションになるため、CFnを意識しないとうまくテストが書けない所が好みが分かれそうです。たとえばAspectsを使うと通常のTypeScriptのコードでStackの内容を確認することもできるので、用途によって使い分けるのがよいと思いました。Aspectsの使い方はクラスメソッドさんの記事が分かりやすいです。

マッチャーはかゆい所に手が届かない部分もあって、たとえばhasResource()等のメソッドは単一のリソースに対してのみアサートするため、同じ種類のリソースが複数あった時
(すべてのS3のバージョニングがonになっているか?など)の簡単な書き方が分かりませんでした。@aws-cdk/assertions はできたばかりのモジュールのため今後の更新にも期待です。

スナップショットテストは手間がほぼかからず、CDKのバージョンアップやリファクタによる意図しないデグレを防ぐことができるため有用だと思います。まずはスナップショットテストから始めるのがよいかなと思いました。

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6