iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0

前一篇文章介紹了 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) {});

});

設定 mock

要隔離 Pulumi 做單元測試,目標是希望資源的建立過程都可以回傳一個假的值代替就好。主要是要知道是否有將對應的參數送給對應的資源建立。

因此就可以使用 Pulumi 提供的 mock 功能來回傳假的值。

Pulumi 設定 mock 的方式是透過 pulumi.runtime.setMocks() 函式設定。在此我們必須傳入兩種狀態下的 mock 要怎麼處理。也就是 newResourcecall 對應的處理方式。

  • 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 為該資源的在雲端上建立後的 id,這邊就直接使用資源的名字加上 _id 來代替。
  • state 為該雲端資源的所有狀態,可以直接把傳入的 inputs 當作狀態設定上去

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)


如果順利的話就可以看到所有案例都通過的結果了!


上一篇
[Day 24] 測試 Pulumi 的方法
下一篇
[Day 26] 撰寫 IaC 整合測試
系列文
30 天學習 Pulumi:用各種程式語言控制雲端資源30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言