前一篇文章介紹了 3 種不同的測試方式,今天就從第一個單元測試開始介紹!
之前提過,可以使用任何程式語言的測試框架進行單元測試,今天的範例會使用 TypeScript 搭配 mocha 測試框架,並使用內建的 assert module 進行驗證。
本篇中使用的待側程式,一樣為之前實戰練習中建立的 Vpc Component Resource。
要使用 mocha
撰寫單元測試,就必須安裝 mocha
與它的型別定義檔 (使用 TypeScript 才需要安裝)。另外還需要額外安裝 ts-node
,才可以直接執行 TypeScript 的程式碼。
$ npm install --save-dev mocha @types/mocha ts-node
接著就可以新增一個測試檔案 ./tests/vpc.test.ts
開始撰寫測試了。
以下先提供一個使用 mocha 撰寫 Pulumi 單元測試的模板,接著再來解釋這在幹嘛。
import * as pulumi from '@pulumi/pulumi';
import 'mocha';
import * as assert from "assert";
pulumi.runtime.setMocks({
newResource: function (args: pulumi.runtime.MockResourceArgs): { id: string, state: any } {},
call: function (args: pulumi.runtime.MockCallArgs) {},
}, "project", "stacak", false);
describe('VCP', function () {
let vpc: typeof import('../vpc');
before(async function () {
vpc = await import('../vpc');
})
it('should create 1 public and 1 private subnet for 1 AZ', function (done) {});
it('should create nat gateway for all private subnets', function (done) {});
it('should create nat gateway for each AZ', function (done) {});
});
要隔離 Pulumi 做單元測試,目標是希望資源的建立過程都可以回傳一個假的值代替就好。主要是要知道是否有將對應的參數送給對應的資源建立。
因此就可以使用 Pulumi 提供的 mock 功能來回傳假的值。
Pulumi 設定 mock 的方式是透過 pulumi.runtime.setMocks()
函式設定。在此我們必須傳入兩種狀態下的 mock 要怎麼處理。也就是 newResource
與 call
對應的處理方式。
newResource
為 IaC 中建立了 CustomResource 時會被呼叫,自己撰寫的 ComponentResource 不會被 mock。call
為在 IaC 中呼叫了特定函式,例如呼叫aws.getAvailabilityZones
取得可用區列表時,會進入這個區塊。接著來撰寫 mock 的內容:
// 建立資源的 Mock
newResource: function (args: pulumi.runtime.MockResourceArgs) {
return {
id: args.name + "_id",
state: args.inputs,
};
},
// 呼叫函式取得狀態的 Mock
call: function (args: pulumi.runtime.MockCallArgs) {
if (args.token === "aws:index/getAvailabilityZones:getAvailabilityZones") {
return {
names: ["us-east-1a", "us-east-1b", "us-east-1c"],
ids: ["use1-az1", "use1-az2", "use1-az3"],
};
}
return args.inputs;
},
在建立資源的 mock 中,必須回傳一個物件包含 id 與 state。
_id
來代替。Note: 也可以透過判斷 args.type 得知建立的資源類型,並做出不同的因應
在函式呼叫的 mock 中,特別判斷 getAvailabilityZones 的呼叫,並回傳一個寫死的值,其他函式就回傳 Inputs 的資料。
接著可以看到在 before hook 中才使用 dynamic import 動態將待側模組引入。
describe('VCP', function () {
let vpc: typeof import('../vpc');
before(async function () {
vpc = await import('../vpc');
})
});
為什麼這裡要這樣做呢?
因為 Pulumi 設定 mock 的程式碼一定要在所有 Pulumi 程式碼之前執行,不然就會 mock 失敗。所以才會在開始測試時,在 setup 階段將待側程式引入。
接著就可以開始撰寫測試案例了,第一個測試案例想要測試,將 numOfAzs
設為 1 時,是否只會建立 1 個 Public Subnet 與 1 個 Private Subnet。
先來看一下測試案例的樣子:
it('should create 1 public and 1 private subnet for 1 AZ', function (done) {
// 建立 VPC,並設定使用 1 個 AZ
const myVpc = new vpc.Vpc('vpc', {
vpcCidrBlocks: "10.0.0.0/16",
numOfAzs: 1,
});
使用 pulumi.all 取得 Output 的內容
pulumi.all([
myVpc.vpc.cidrBlock,
myVpc.publicSubnetIds,
myVpc.privateSubnetIds,
myVpc.publicSubnets[0].subnet.cidrBlock,
myVpc.privateSubnets[0].subnet.cidrBlock,
]).apply(([
vpcCidrBlock,
publicSubnetIds,
privateSubnetId,
firstPublicSubnetCidrBlock,
firstPrivateSubnetCidrBlock]) => {
try {
// 驗證所有資料是否符合預期
assert.equal(vpcCidrBlock, "10.0.0.0/16");
assert.equal(publicSubnetIds.length, 1);
assert.equal(privateSubnetId.length, 1);
assert.equal(firstPublicSubnetCidrBlock, "10.0.0.0/20");
assert.equal(firstPrivateSubnetCidrBlock, '10.0.128.0/20');
done();
} catch (err) {
done(err);
}
});
});
這邊需要特別注意的就是驗證的地方了,因為使用了 Pulumi Output.apply
這個非同步方式,因此需要在測試案例中處理非同步測試。
這邊透過在測試函式中的 done 來通知 mocha 測試是否完成,如果測試失敗,也必須透過 done 回傳錯誤結果。
當然也可以透過將整個測試函式包裝成 Promise 的形式來完成非同步測試。
接下來的兩個測試,是測試 NAT Gateway 的建立行為是否符合預期。
也就是給 One
時,永遠只會建立一個 NAT Gateway。給 OnePerAz
時會在每個 AZ 建立一個 NAT Gateway。
這邊測試只建立 1 個 NAT Gateway 的測試案例中,就不使用 done 來通知 mocha 測試完成,而是改為回傳 Promise 的方式處理非同步的驗證。
it('should create nat gateway for all private subnets', function () {
return new Promise((resolve, reject) => {
const myVpc = new vpc.Vpc('vpc', {
vpcCidrBlocks: "10.0.0.0/16",
numOfAzs: 2,
numOfPrivateSubnet: 4,
natGateway: 'One',
});
pulumi.all([
myVpc.natGateways
]).apply(([natGateways]) => {
try {
assert.equal(natGateways.length, 1);
return resolve(null);
} catch (err) {
return reject(err);
}
});
});
});
it('should create nat gateway for each AZ', function (done) {
const myVpc = new vpc.Vpc('vpc', {
vpcCidrBlocks: "10.0.0.0/16",
numOfAzs: 2,
numOfPrivateSubnet: 4,
natGateway: 'OnePerAz',
});
pulumi.all([
myVpc.natGateways,
]).apply(([natGateways, natSubnetIds, publicSubnetIds]) => {
try {
assert.equal(natGateways.length, 2);
done();
} catch (err) {
done(err);
}
});
});
接著我們就可以執行測試了!
這邊執行 mocha 指令,並設定 ts-node/register,讓 mocha 可以執行 TypeScript 撰寫的測試案例
$ npx mocha -r ts-node/register ./tests/vpc_test.ts
VCP
✔ should create 1 public and 1 private subnet for 1 AZ (124ms)
✔ should create nat gateway for all private subnets (215ms)
✔ should create nat gateway for each AZ (270ms)
3 passing (757ms)
如果順利的話就可以看到所有案例都通過的結果了!