# 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)