# CDK - Testing Constructs 1 :::info 日期 : 2025/04/26 ::: 以下以 [Testing Constructs](https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US/2000-typescript-workshop/600-advanced-topics/650-construct-testing) 作為講解 步驟與完整程式碼 請參考上述網站 --- #### 觀念 * **`npm run test`** `npm run test` : 叫 npm 去執行 package.json 裡面設定好的 `"test"` 指令,package.json 裡面是 `"test": "jest"`,所以實際上是執行 `jest` `jest` 會去 1. 找所有 `*.test.ts` 或 `*.spec.ts` 的檔案 2. compile 並轉譯 `.ts` (Jest 本身只懂 JavaScript) 3. 執行檔案裡的 `test()` 函式(一個測試案例) ```typescript test("測試名稱", () => { // 測試邏輯 expect(結果).toBe(預期值); }); ``` 4. 在 console 中印出成功跟失敗 * **測試原則** * 只測"資源是否被正確建立" * 檢查資源屬性設定(Type,Properties...) * 分成 WHEN (建立 construct) / THEN (檢查產生出來的 Template) * 每個 `test()` 裡面自己建立 `new Stack()`,確保測試獨立 --- #### aws-cdk-lib/assertions `aws-cdk-lib/assertions` 是在 CDK 中專門寫測試的工具 以下為這行程式碼把 stack 轉成一個在程式內可以操作的 "CloudFormation 資源清單物件" template = cdk deploy 後的 CloudFormation 模板內容 ```typescript const template = Template.fromStack(stack); ``` 這個 lib 裡面有很多 API `hasResource`,`hasResourceProperties`,`resourceCountIs`,`findResources` ... 以下為測試 CloudFormation 某模板裡,ACM 憑證的 ShouldNotExist 這個屬性不存在 ( `Match.absent()` ) ```typescript template.hasResourceProperties('AWS::CertificateManager::Certificate', { DomainName: 'test.example.com', ShouldNotExist: Match.absent(), }) ``` --- #### 測試 DynamoDB Table 以下是幫 HitCounter construct 建立單元測試,來確認它有成功創建 DynamoDB Table 以下實例不用 `const ConstructName = new Construct`,因為 1. `new Construct(stack, "id", props)`,這個 construct 就會掛到這個 stack 的建構樹上 2. test 的目的是驗證,不需操作 `Construct` 本身內容 ```typescript // hitcounter.test.ts import { Stack } from "aws-cdk-lib"; import { Template, Capture } from "aws-cdk-lib/assertions"; import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; import { HitCounter } from "../lib/hitcounter"; // 回到上層資料夾 再進入lib中的hitcounter test("DynamoDB Table Created", () => { // Jest 測試框架語法 const stack = new Stack(); // 建立一個空的 Stack,只是為了測試用 // WHEN: 加入一個 HitCounter 到這個 stack new HitCounter(stack, "MyTestConstruct", { downstream: new Function(stack, "TestFunction", { runtime: Runtime.NODEJS_22_X, handler: "hello.handler", code: Code.fromAsset("lambda"), }), }); // THEN: 檢查 stack 產生出來的模板裡面,有 1 個 DynamoDB Table const template = Template.fromStack(stack); // 把 stack 轉成一個在程式可用的 CloudFormation 資源清單物件 template.resourceCountIs("AWS::DynamoDB::Table", 1); }); ``` 結果 ```bash > cdk-workshop@0.1.0 test > jest PASS test/hitcounter.test.ts (70.84 s) ✓ DynamoDB Table Created (8786 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 74.148 s Ran all test suites. ``` --- #### 測試 Lambda 測試 HitCounter construct 建出來的 Lambda,有沒有正確設定環境變數。 但麻煩的是環境變數是 CDK 建構時動態生成的,所以需要建立一個 Capture 物件,先跑一次 `npm run test`,故意失敗一次,然後拿錯誤訊息裡的真實值,填回測試 ```typescript // hitcounter.test.ts ... // Lambda test("Lambda Has Environment Variables", () => { const stack = new Stack(); // 建立一個空 Stack // WHEN: 在這個 Stack 中放進 HitCounters new HitCounter(stack, "MyTestConstruct", { downstream: new Function(stack, "TestFunction", { runtime: Runtime.NODEJS_22_X, handler: "hello.handler", code: Code.fromAsset("lambda"), }), }); // THEN: 檢查 Lambda function 是否有設置正確的環境變數 const template = Template.fromStack(stack); // 把 Stack 合成成可以檢查的模板 const envCapture = new Capture(); // 建立一個 Capture 物件,專門用來「抓取」Environment 設定 template.hasResourceProperties("AWS::Lambda::Function", { Environment: envCapture, // 在 Lambda resource 中,抓出 Environment 區塊 }); // 比對 Environment 區塊的內容 expect(envCapture.asObject()).toEqual({ Variables: { DOWNSTREAM_FUNCTION_NAME: { Ref: "TestFunctionXXXXX", // (測試會告訴你真實的 Ref 名稱) }, HITS_TABLE_NAME: { Ref: "MyTestConstructHitsXXXXX", // (測試會告訴你真實的 Ref 名稱) }, }, }); }); ``` 先跑一次 `npm run test`,從失敗訊息中,拿到真實的 CDK 產生值 ```bash Object { "Variables": Object { "DOWNSTREAM_FUNCTION_NAME": Object { - "Ref": "TestFunctionXXXXX", + "Ref": "TestFunction22AD90FC", }, "HITS_TABLE_NAME": Object { - "Ref": "MyTestConstructHitsXXXXX", + "Ref": "MyTestConstructHits24A357F0", }, }, ``` 把這些真實值填回測試程式碼,再跑一次 `npm run test` --- #### TDD 可以用 TDD (Test Driven Development) 的流程來開發 CDK Construct 1. 先寫一個關於新功能的測試案例(一定 fail 因為這時候還沒寫新功能) 2. 回去改主程式碼,讓這個測試通過 這樣保證新功能正確性,同時確保原功能沒壞掉 以下目標為幫 DynamoDB Table 加上加密功能 ```typescript ... // DynamoDB Table 加密 test("DynamoDB Table Created With Encryption", () => { const stack = new Stack(); // WHEN: 在這個 stack 中新增一個 HitCounter construct new HitCounter(stack, "MyTestConstruct", { downstream: new Function(stack, "TestFunction", { runtime: Runtime.NODEJS_22_X, handler: "hello.handler", code: Code.fromAsset("lambda"), }), }); // THEN: 將 Stack 合成成可以檢查的 CloudFormation Template const template = Template.fromStack(stack); // 驗證:在這個模板中,有一個 DynamoDB Table,並且它啟用了加密 (SSE) template.hasResourceProperties("AWS::DynamoDB::Table", { SSESpecification: { // 檢查加密設定區塊 SSEEnabled: true, // 必須啟用加密 }, }); }); ``` 測試失敗後,回去 `lib/hitcounter.ts` 改程式碼 ```typescript ... // 建立一個 DynamoDB 表格,用來儲存「每個路徑的點擊次數」 const table = new Table(this, "Hits", { partitionKey: { name: "path", type: AttributeType.STRING }, // key 是 path 是 str encryption: TableEncryption.AWS_MANAGED, }); ... ``` 測試成功 --- [Testing Constructs](https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/en-US/2000-typescript-workshop/600-advanced-topics/650-construct-testing)