AWS CDKのテストをやってみる

こんにちは。 エキサイト株式会社で内定者アルバイトをしている平石です。

今回は、AWS CDKで正しくリソースが定義されているかをテストするための方法をご紹介します。

前の記事

tech.excite.co.jp

2種類のテスト

AWS CDKは通常のプログラミング言語で記述できるということもあって、テストが書けるというのも魅力の一つなようです。

AWS CDKでは2種類のテストがサポートされています。
一つは、CDKで記述したコードから生成されたAWS CloudFormationテンプレートの特定の要素をテストするためのFine-grained assertionsと呼ばれるものです。

あるリソースが存在するか?いくつあるか?特定のプロパティの値が正しくセットされているか?といったテストができ、テスト駆動開発をする上で便利なものです。こちらの方がよく使われるようです。

もう一つは、生成されたAWS CloudFormationテンプレートが以前(直前の成功したテスト)に生成されたテンプレートと一致するかをテストするためのSnapshot testsと呼ばれるものです。

こちらは、リソースの追加や削除等の変更を伴うと失敗してしまうので、通常の開発時に行うことはほとんどありません。
では、このテストをいつ使用するかというと、コードをリファクタリングする時です。
すなわち、リファクタリング後のコードが生成するリソースは、リファクタリング前のコードが生成するリソースと全く同じであるかをチェックできます。

実際に簡単な例で試してみる

Fine-grained assertions

では、まずはFine-grained assertionsを簡単な例でやってみましょう。

前提となるコードは、以下のVPCとサブネットおよびEC2インスタンスを生成するコードです。

このコードの詳細については、以下の記事をご覧ください。

tech.excite.co.jp

package com.myorg;

import software.amazon.awscdk.CfnTag;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.services.ec2.CfnInternetGateway;
import software.amazon.awscdk.services.ec2.CfnVPCGatewayAttachment;
import software.amazon.awscdk.services.ec2.IMachineImage;
import software.amazon.awscdk.services.ec2.Instance;
import software.amazon.awscdk.services.ec2.InstanceClass;
import software.amazon.awscdk.services.ec2.InstanceSize;
import software.amazon.awscdk.services.ec2.InstanceType;
import software.amazon.awscdk.services.ec2.MachineImage;
import software.amazon.awscdk.services.ec2.Peer;
import software.amazon.awscdk.services.ec2.Port;
import software.amazon.awscdk.services.ec2.PrivateSubnet;
import software.amazon.awscdk.services.ec2.PublicSubnet;
import software.amazon.awscdk.services.ec2.SecurityGroup;
import software.amazon.awscdk.services.ec2.SubnetSelection;
import software.amazon.awscdk.services.ec2.Vpc;
import software.constructs.Construct;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;

import java.util.List;

public class MyProjectStack extends Stack {
    public MyProjectStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public MyProjectStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);

        Vpc vpc = Vpc.Builder.create(this, "SampleVPC")
                .vpcName("sample-vpc")
                .cidr("cidr")
                .subnetConfiguration(List.of())
                .build();

        // Availability zone: ap-northeast-1a
        PrivateSubnet privateSubnetA = PrivateSubnet.Builder.create(this, "PrivateSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetA = PublicSubnet.Builder.create(this, "PublicSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // Availability zone: ap-northeast-1c
        PrivateSubnet privateSubnetB = PrivateSubnet.Builder.create(this, "PrivateSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        PublicSubnet publicSubnetB = PublicSubnet.Builder.create(this, "PublicSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidr")
                .build();

        // NAT gatewayの追加
        publicSubnetA.addNatGateway();
        publicSubnetB.addNatGateway();

        // Internet Gateway
        CfnInternetGateway internetGateway = CfnInternetGateway.Builder.create(this, "InternetGateway")
                .tags(List.of(CfnTag.builder()
                        .key("Name")
                        .value("sample-internet-gateway")
                        .build()))
                .build();

        // Internet gatewayをVPCにattachする
        CfnVPCGatewayAttachment.Builder.create(this, "VpcGateAwayAttachment")
                .vpcId(vpc.getVpcId())
                .internetGatewayId(internetGateway.getAttrInternetGatewayId())
                .build();

        SecurityGroup securityGroup = SecurityGroup.Builder.create(this, "SampleSecurityGroup")
                .securityGroupName("sample-security-group")
                .vpc(vpc)
                .build();

        IMachineImage machineImage = MachineImage.latestAmazonLinux();

        Instance instance01 = Instance.Builder.create(this, "ec2-instance-01")
                .instanceName("ec2-instance-01")
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetA)).build())
                .machineImage(machineImage)
                .vpc(vpc)
                .availabilityZone("ap-northeast-1a")
                .instanceType(InstanceType.of(InstanceClass.T2, InstanceSize.SMALL))
                .securityGroup(securityGroup)
                .build();

        Instance instance02 = Instance.Builder.create(this, "ec2-instance-02")
                .instanceName("ec2-instance-02")
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetB)).build())
                .machineImage(machineImage)
                .vpc(vpc)
                .availabilityZone("ap-northeast-1c")
                .instanceType(InstanceType.of(InstanceClass.T2, InstanceSize.SMALL))
                .securityGroup(securityGroup)
                .build();

        // Load Balancer
        // Load Balancer用のSecurity Group
        SecurityGroup securityGroupEC2LB = SecurityGroup.Builder.create(this, "SecurityGroupEC2LoadBalancer")
                .securityGroupName("ec2-lb-security-group")
                .vpc(vpc)
                .description("security-group-of-ec2-load-balancer")
                .allowAllOutbound(true)
                .build();

        securityGroupEC2LB.addIngressRule(Peer.ipv4("0.0.0.0/0"), Port.tcp(80));

        ApplicationLoadBalancer applicationLoadBalancer = ApplicationLoadBalancer.Builder.create(this, "SampleEC2ApplicationLoadBalancer")
                .loadBalancerName("sample-ec2-load-balancer")
                .deletionProtection(false)
                .vpc(vpc)
                .vpcSubnets(SubnetSelection.builder().subnets(List.of(privateSubnetA, privateSubnetB)).build())
                .securityGroup(securityGroupEC2LB)
                .ipAddressType(IpAddressType.IPV4)
                .idleTimeout(Duration.seconds(120))
                .http2Enabled(true)
                .build();

        ApplicationTargetGroup ec2TargetGroup = ApplicationTargetGroup.Builder.create(this, "EC2TargetGroup")
                .targetGroupName("ec2-target-group")
                .targetType(TargetType.INSTANCE)
                .port(80)
                .vpc(vpc)
                .targets(List.of(new InstanceTarget(instance01, 80), new InstanceTarget(instance02, 80)))
                .protocol(ApplicationProtocol.HTTP)
                .protocolVersion(ApplicationProtocolVersion.HTTP1)
                .healthCheck(HealthCheck.builder()
                        .enabled(true)
                        .path("/")
                        .healthyThresholdCount(3)
                        .unhealthyThresholdCount(2)
                        .interval(Duration.seconds(60))
                        .timeout(Duration.seconds(5))
                        .protocol(Protocol.HTTP)
                        .port("80")
                        .build())
                .build();

        ApplicationListener applicationListener = ApplicationListener.Builder.create(this, "ApplicationListener")
                .loadBalancer(applicationLoadBalancer)
                .protocol(ApplicationProtocol.HTTP)
                .port(80)
                .defaultAction(ListenerAction.forward(List.of(ec2TargetGroup)))
                .build();
    }
}

なお、コード中の"cidr"については、ご自身が使用されているものに変更してください。

では、このコードに対してEC2インスタンスが2個生成されることと、指定したプロパティを持つVPCが生成されることをテストしましょう。

自動生成されるテストのためのファイル<プロジェクト名>Test.javaを以下のように書き換えます。

package com.myorg;

 import org.junit.jupiter.api.Test;
 import software.amazon.awscdk.App;
 import software.amazon.awscdk.assertions.Template;

 import java.util.Collections;
 import java.util.Map;

 
 public class MyProjectTest {
     @Test
     public void testStack() {
         App app = new App();
         MyProjectStack stack = new MyProjectStack(app, "test");
         Template template = Template.fromStack(stack);

         template.resourceCountIs("AWS::EC2::Instance", 2);

         template.hasResourceProperties("AWS::EC2::VPC",
                 Map.of(
                         "CidrBlock", "cidr",
                         "Tags", Collections.singletonList(Map.of(
                                 "Key", "Name",
                                 "Value", "sample-vpc"))
                 ));
     }
 }

testStackメソッドの最初の3行で、テストのためのCloudFormationのテンプレートを準備しています。
おまじないのようなものだと思ってください...

次の行では、EC2インスタンスがちょうど2個作成されていることをテストしています。resourceCountIsは第一引数に対象となるリソースの種類を文字列で、第二引数にカウントを指定します。
第一引数で使用できる文字列はCloudFormationのテンプレートで使用されているものと同じです。

詳細はこちらを参照してください。

AWS リソースおよびプロパティタイプのリファレンス - AWS CloudFormation

さらに、次の行では指定したプロパティを持つリソースが生成されることをテストします。ここでは、hasResourcePropertiesを使用しています。
このメソッドは、第一引数に対象となるリソースの種類を文字列で、第二引数に実際のプロパティをMapで指定します。

ここまで記述できたら、実際にテストを実行してみましょう。

Javaでは以下のようなコマンドで実行できます。

mvn compile && mvn test

すると、以下のようにテストが成功します。

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.myorg.MyProjectTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.938 sec


Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.023 s
[INFO] Finished at: 2022-11-11T14:52:24+09:00
[INFO] ------------------------------------------------------------------------

では、ここで試しにテストコードのresourceCountIsのカウントを3に変更してみるとどうなるでしょうか?

         template.resourceCountIs("AWS::EC2::Instance", 3);

テストを実行してみます。

mvn compile && mvn test
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.myorg.MyProjectTest
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 3.066 sec <<< FAILURE!
com.myorg.MyProjectTest.testStack()  Time elapsed: 3.065 sec  <<< FAILURE!
java.lang.RuntimeException: Error: Expected 3 resources of type AWS::EC2::Instance but found 2
@jsii/kernel.RuntimeError: Error: Expected 3 resources of type AWS::EC2::Instance but found 2
    at Kernel._ensureSync (/private/var/folders/zq/xgjlrkpx6nd5lj0bq93n76v40000gr/T/jsii-java-runtime13468074528918510716/lib/program.js:8428:27)
    at Kernel.invoke (/private/var/folders/zq/xgjlrkpx6nd5lj0bq93n76v40000gr/T/jsii-java-runtime13468074528918510716/lib/program.js:7840:34)
    at KernelHost.processRequest (/private/var/folders/zq/xgjlrkpx6nd5lj0bq93n76v40000gr/T/jsii-java-runtime13468074528918510716/lib/program.js:11017:36)
    at KernelHost.run (/private/var/folders/zq/xgjlrkpx6nd5lj0bq93n76v40000gr/T/jsii-java-runtime13468074528918510716/lib/program.js:10977:22)
    at Immediate._onImmediate (/private/var/folders/zq/xgjlrkpx6nd5lj0bq93n76v40000gr/T/jsii-java-runtime13468074528918510716/lib/program.js:10978:46)
    at processImmediate (node:internal/timers:466:21)
    at software.amazon.jsii.JsiiRuntime.processErrorResponse(JsiiRuntime.java:127)
    at software.amazon.jsii.JsiiRuntime.requestResponse(JsiiRuntime.java:96)
    at software.amazon.jsii.JsiiClient.callMethod(JsiiClient.java:184)
    at software.amazon.jsii.Kernel.call(Kernel.java:52)
    at software.amazon.awscdk.assertions.Template.resourceCountIs(Template.java:259)
    at com.myorg.MyProjectTest.testStack(SampleDbTest.java:22)



Results :

Failed tests:   com.myorg.MyProjectTest.testStack(): Error: Expected 3 resources of type AWS::EC2::Instance but found 2(..)

Tests run: 1, Failures: 1, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.772 s
[INFO] Finished at: 2022-11-11T15:01:09+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project sample-db: There are test failures.
[ERROR]
[ERROR] Please refer to /path/my-project/target/surefire-reports for the individual test results.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

当然ですが、失敗しました。エラーメッセージを見てみるとjava.lang.RuntimeException: Error: Expected 3 resources of type AWS::EC2::Instance but found 2となっており、AWS::EC2::Instanceは3つやゆうて期待されとんのに、2つしか見つからんやんけ!と怒られています。
予想通りですね!

その他のテストのためのメソッドについては以下を参照してください。

class Template · AWS CDK

Snapshot tests

次に、Snapshot testsを簡単な例でやってみましょう。

ちなみに、このSnapshot tests、公式ドキュメントTesting constructs - AWS Cloud Development Kit (AWS CDK) v2通りにやるとうまくいかないので注意してください。

まずは、必要な依存関係を追加します。pom.xmlに以下のような内容を追加します。

        <dependency>
            <groupId>io.github.origin-energy</groupId>
            <artifactId>java-snapshot-testing-core</artifactId>
            <version>4.0.0</version>
        </dependency>

        <dependency>
            <groupId>io.github.origin-energy</groupId>
            <artifactId>java-snapshot-testing-junit5</artifactId>
            <version>4.0.0</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.3</version>
        </dependency>

次に、snapshotのためのプロパティファイルを作成します。test/resourcesディレクトリを作成し、その中にsnapshot.propertiesファイルを作成しましょう。

serializer=au.com.origin.snapshots.serializers.ToStringSnapshotSerializer
serializer.base64=au.com.origin.snapshots.serializers.Base64SnapshotSerializer
serializer.json=au.com.origin.snapshots.serializers.JacksonSnapshotSerializer
serializer.orderedJson=au.com.origin.snapshots.serializers.DeterministicJacksonSnapshotSerializer
comparator=au.com.origin.snapshots.comparators.PlainTextEqualsComparator
reporters=au.com.origin.snapshots.reporters.PlainTextSnapshotReporter
snapshot-dir=__snapshots__
output-dir=src/test/java
ci-env-var=CI

記述内容は基本的にこのままで問題ありません。

次に、スタックを定義します。

package com.myorg;

import software.amazon.awscdk.services.s3.Bucket;
import software.constructs.Construct;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;

public class SnapshotTestStack extends Stack {
    public SnapshotTestStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public SnapshotTestStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);

        Bucket bucket = Bucket.Builder.create(this, "SampleBucket")
                .bucketName("sample-bucket-desunoyo")
                .versioned(true)
                .build();
    }
}

S3バケットを一個生成するだけのスタックです。
ちなみに、S3バケットの名前はグローバルで一意でなければならないので、sample-bucket-desunoyoのままではデプロイは不可能ですのよ。
そのため、他と被らなそうな適当な名前に変更してください。

次に、テストコードを記述していきましょう。

自動生成されているコードを編集していきます。

package com.myorg;

import au.com.origin.snapshots.Expect;
import au.com.origin.snapshots.SnapshotVerifier;
import au.com.origin.snapshots.annotations.SnapshotName;
import au.com.origin.snapshots.config.PropertyResolvingSnapshotConfig;
import au.com.origin.snapshots.junit5.SnapshotExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import software.amazon.awscdk.App;
import software.amazon.awscdk.assertions.Template;


@ExtendWith({SnapshotExtension.class})
public class SnapshotTestTest {

    @SnapshotName("snapshot_test_shot")
    @Test
    public void testStack() throws NoSuchMethodException {
        App app = new App();
        SnapshotTestStack stack = new SnapshotTestStack(app, "test");

        Template template = Template.fromStack(stack);

        SnapshotVerifier snapshotVerifier = new SnapshotVerifier(new PropertyResolvingSnapshotConfig(), SnapshotTestTest.class);

        Expect expect = Expect.of(snapshotVerifier, SnapshotTestTest.class.getMethod("testStack"));

        expect.toMatchSnapshot(template.toJSON());

        snapshotVerifier.validateSnapshots();
    }
}

snapshot-testの公式のGitHubGitHub - origin-energy/java-snapshot-testing: Facebook style snapshot testing for JAVA Tests)には、

private Expect expect;

と書けば、expectに依存性注入されるというように書かれていましたが、どうもうまくいかないので、GitHub - origin-energy/java-snapshot-testing: Facebook style snapshot testing for JAVA Testsを参考にexpectを生成しています。

では、

mvn compile && mvn test

を実行してみましょう。
テスト部分のコンソールは以下のようになるはずです。

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.myorg.SnapshotTestTest
[main] INFO au.com.origin.snapshots.SnapshotFile - Snapshot File: src/test/java/com/myorg/__snapshots__/SnapshotTestTest.snap
[main] WARN au.com.origin.snapshots.SnapshotContext - We detected you are running on a developer machine - if this is incorrect please override the isCI() method in SnapshotConfig
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.785 sec


Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.914 s
[INFO] Finished at: 2022-11-11T16:30:25+09:00
[INFO] ------------------------------------------------------------------------

そして、__snapshots__ディレクトリが作成され、その中にSnapshotTestTest.snapファイルが作成されていると思います。
これがスナップショットです。

では、CDKのコードを変化させてテストの結果がどうなるかを見ていきましょう。

        Bucket bucket = Bucket.Builder.create(this, "SampleBucket")
                .bucketName("sample-bucket-desuno")
                .versioned(true)
                .build();

S3バケットの名前をsample-bucket-desunoyoからsample-bucket-desunoに変更しますの。

この状態で、

mvn compile && mvn test

を実行すると、

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.myorg.SnapshotTestTest
[main] INFO au.com.origin.snapshots.SnapshotFile - Snapshot File: src/test/java/com/myorg/__snapshots__/SnapshotTestTest.snap
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 2.707 sec <<< FAILURE!
com.myorg.SnapshotTestTest.testStack()  Time elapsed: 2.706 sec  <<< FAILURE!
au.com.origin.snapshots.exceptions.SnapshotMatchException: Error(s) matching snapshot(s) (1 failure)
    shadow.org.opentest4j.AssertionFailedError: Error on:
snapshot_test_shot=[
{Resources={SampleBucket7F6F8160={Type=AWS::S3::Bucket, Properties={BucketName=sample-bucket-desuno, VersioningConfiguration={Status=Enabled}}, UpdateReplacePolicy=Retain, DeletionPolicy=Retain}}, Parameters={BootstrapVersion={Type=AWS::SSM::Parameter::Value<String>, Default=/cdk-bootstrap/hnb659fds/version, Description=Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]}}, Rules={CheckBootstrapVersion={Assertions=[{Assert={Fn::Not=[{Fn::Contains=[[1, 2, 3, 4, 5], {Ref=BootstrapVersion}]}]}, AssertDescription=CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.}]}}}
]
Changed content at line 2:
expecting:
  ["{Resources={SampleBucket7F6F8160={Type=AWS::S3::Bucket, Properties={BucketName=sample-bucket-desunoyo, VersioningConfiguration={Status=Enabled}}, UpdateReplacePolicy=Retain, DeletionPolicy=Retain}}, Parameters={BootstrapVersion={Type=AWS::SSM::Parameter::Value<String>, Default=/cdk-bootstrap/hnb659fds/version, Description=Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]}}, Rules={CheckBootstrapVersion={Assertions=[{Assert={Fn::Not=[{Fn::Contains=[[1, 2, 3, 4, 5], {Ref=BootstrapVersion}]}]}, AssertDescription=CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.}]}}}"]
but was:
  ["{Resources={SampleBucket7F6F8160={Type=AWS::S3::Bucket, Properties={BucketName=sample-bucket-desuno, VersioningConfiguration={Status=Enabled}}, UpdateReplacePolicy=Retain, DeletionPolicy=Retain}}, Parameters={BootstrapVersion={Type=AWS::SSM::Parameter::Value<String>, Default=/cdk-bootstrap/hnb659fds/version, Description=Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]}}, Rules={CheckBootstrapVersion={Assertions=[{Assert={Fn::Not=[{Fn::Contains=[[1, 2, 3, 4, 5], {Ref=BootstrapVersion}]}]}, AssertDescription=CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.}]}}}"]
    at au.com.origin.snapshots.SnapshotContext.toMatchSnapshot(SnapshotContext.java:108)
    at au.com.origin.snapshots.Expect.toMatchSnapshot(Expect.java:56)
    at com.myorg.SnapshotTestTest.testStack(CdkEcsTest.java:29)



Results :

Failed tests:   com.myorg.SnapshotTestTest.testStack(): Error(s) matching snapshot(s) (1 failure)(..)

Tests run: 1, Failures: 1, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.841 s
[INFO] Finished at: 2022-11-11T17:01:23+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project cdk-ecs: There are test failures.
[ERROR]
[ERROR] Please refer to /path/snapshot-test/target/surefire-reports for the individual test results.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

上記のように表示されテストが失敗します(テンプレートが一致しないので当然ですが...)。

では、S3バケットの名前を元に戻し、以下のようにコードを変更するとどうなるでしょうか?

    Bucket bucket = new Bucket(this, "SampleBucket", BucketProps.builder()
                        .bucketName("sample-bucket-desunoyo")
                        .versioned(true)
                        .build());

CDKのコード自体は変更されていますが、生成されるCloudFormationのテンプレートは同じですので、

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.myorg.SnapshotTestTest
[main] INFO au.com.origin.snapshots.SnapshotFile - Snapshot File: src/test/java/com/myorg/__snapshots__/SnapshotTestTest.snap
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.702 sec


Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.743 s
[INFO] Finished at: 2022-11-11T17:07:58+09:00
[INFO] ------------------------------------------------------------------------

テストは成功します。

ちなみに、上の例からも分かるようにテストに失敗したものはスナップショットとして保存されないので安心してください。

補足

snapshot testが失敗した時、エラーメッセージのスナップショットが見づらいと感じたかもしれません。

その場合には以下のような依存関係を追加し、

        <dependency>
            <groupId>io.github.origin-energy</groupId>
            <artifactId>java-snapshot-testing-plugin-jackson</artifactId>
            <version>4.0.0</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.14.0</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.14.0</version>
            <scope>test</scope>
        </dependency>

テストコードを

       expect.serializer("json").toMatchSnapshot(template.toJSON());

のように変更すると、コンソールでの出力がjson形式になり多少見やすくなります。

終わりに

今回は、AWS CDKでのテストの書き方の基礎をご紹介しました。

では、また次回。

次の記事

tech.excite.co.jp

参考文献