iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 28
2
Modern Web

使用 Modern Web 技術來打造 Chat App系列 第 28

Day 28:有效測試的方法論

熟悉測試的讀者可能知道,自動化測試可以大致分成幾個不同的層級:

  • 單元測試(Unit Test):只測試一個「單元」,通常是一個 Function 或一個 Class,其他參與到的部分應該盡量使用假的替代。
  • 整合測試(Integration Test):把幾個「單元」組裝起來,一起進行測試。
  • 端對端測試(End to End Test):模擬使用者的行為,從 UI 操作到從 UI 上得到結果。

傳統的測試概念上,測試的佈局要有如金字塔:

https://ithelp.ithome.com.tw/upload/images/20191013/20103630psiSHu6EE1.png

因為每往上一層,撰寫測試的成本都會更高,而單元測試可以在比較小的範疇上抓到 Bug。當然靜態檢查的成本更低,所以 Eslint 等等工具是一定要用的。

但這幾年,Web 界的測試大師 - Kent C. Dodds 提出了這個「盃」狀 的測試概念:

https://ithelp.ithome.com.tw/upload/images/20191013/20103630M6jfZTqX5s.jpg

個人經驗也很認同這個說法,依靠單元測試的比例過高,會讓自己對整體的運作狀況沒那麼有信心。另外,當想要重構的時候,很多「單元」都被改寫過了,測試當然也要改寫,這種太過於測試實作的細節的測試對於重構很不利。如果當你重構時,你的測試不用修改,而且能讓測試繼續通過,那是個好現象。當然演算法相關的東西還是很適合單元測試的,所以也要看你的軟體的本質來確定要寫怎樣的測試來確保它。

更好的是,「使用者怎麼去使用你的軟體,你就應該怎麼去測試」,並避免測試實作的細節。

在聊天機器人上,要做到自動化的「端對端測試」是很不容易的,你不太容易自動登入一個 LINE 的帳號去跟你的機器人聊天,也不太容易去真的對語音助理講話。而且各平台有太多不同的玩法,就算辦得到成本也是太高。

因此我們大部分應該專注在「整合測試」上面:

在 Bottender 上執行整合測試

這邊我們先以 LINE 以及最簡單的 App 作為範例:

// index.js
module.exports = async function App(context) {
  await context.sendText('Welcome to Bottender');
};

我們寫一個 index.test.js 來寫幾個測試,這個測試會使用 nock 這個套件來攔截送去 LINE 的 HTTP Request,並用 supuertest 這個套件來送出 HTTP Request 到我們的 Bot Server:

const crypto = require('crypto');

const nock = require('nock');
const request = require('supertest');
const { initializeServer } = require('bottender');

nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

let scope;

// Bottender 會去查 User Profile
beforeEach(() => {
  nock('https://api.line.me')
    .persist()
    .get('/v2/bot/profile/U00000000000000000000000000000001')
    .reply(200, {
      displayName: 'LINE taro',
      userId: 'U4af4980629...',
      pictureUrl: 'https://obs.line-apps.com/...',
      statusMessage: 'Hello, LINE!',
    });
});

afterEach(() => {
  afterEach(() => {
    nock.cleanAll();
  });
});

it('should reply "Welcome to Bottender"', async () => {
  let replyBody;

  scope = nock('https://api.line.me')
    .persist()
    .post('/v2/bot/message/reply')
    .reply(200, (_, requestBody) => {
      replyBody = requestBody;
      return {};
    });

  const server = initializeServer({
    config: {
      session: {
        driver: 'memory',
      },
      channels: {
        line: {
          sync: true,
        },
      },
    },
  });

  const body = {
    destination: 'Uaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
    events: [
      {
        replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA',
        type: 'message',
        timestamp: 1462629479859,
        source: {
          type: 'user',
          userId: 'U00000000000000000000000000000001',
        },
        message: {
          id: '325708',
          type: 'text',
          text: 'Hi', // 送出 Hi 這個文字
        },
      },
    ],
  };

  // 需要有 Signature 才能防偽造
  const signature = crypto
    .createHmac('sha256', process.env.LINE_CHANNEL_SECRET)
    .update(JSON.stringify(body), 'utf8')
    .digest()
    .toString('base64');

  const res = await request(server)
    .post('/webhooks/line')
    .set('X-Line-Signature', signature)
    .send(body);

  expect(res.status).toEqual(200);
  expect(replyBody).toEqual({
    replyToken: 'nHuyWiB7yP5Zw52FIkcQobQuGDXCTA',
    messages: [
      {
        type: 'text',
        text: 'Welcome to Bottender',
      },
    ],
  });
});

這個範例看起來相當的長,從假裝成 LINE 發「Hi」的訊息給 Server 到攔截送給 LINE 的 API 收到「Welcome to Bottender」為止,雖然長但這是目前最能穩定確認功能正常的方法。未來會持續看看怎麼樣在能穩定確保程式正確執行的情況,新增些簡潔的寫法出來。

Snapshot Testing

有些時候如果文案一改,測試就要跟著改也是略顯麻煩,所以可以用 Snapshot 來確保不會發生 Regression 就好。只要把最後那個 toEqual 改成 toMatchSnapshot 就好:

  const res = await request(server)
    .post('/webhooks/line')
    .set('X-Line-Signature', signature)
    .send(body);

  expect(res.status).toEqual(200);
  expect(replyBody).toMatchSnapshot();

https://ithelp.ithome.com.tw/upload/images/20191013/20103630vlSXYL1j6y.png

跑完第一次後,就會產生 __snapshots__/index.test.js.snap 這個檔案,然後裡面會有資料的快照:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should match snapshot 1`] = `
Object {
  "messages": Array [
    Object {
      "text": "Welcome to Bottender",
      "type": "text",
    },
  ],
  "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
}
`;

這後若是文案改了,可以得清楚的去看到 Diff,也可以透過 -u 參數去更新。

https://ithelp.ithome.com.tw/upload/images/20191013/201036305U8qj6uDVV.png

結語

在寫測試時只要去思考,這測試是否有讓開發者安心?上次遇到的 Bug 是不是再也不會發生?這些測試是不是每次重構都要重寫?大概心裡也有些答案,知道什麼時候寫測試、什麼時候不寫測試、該寫怎樣的測試就能變成一個好的工程師。


上一篇
Day 27:聊天機器人的錯誤處理
下一篇
Day 29:把機器人部署到「Heroku」
系列文
使用 Modern Web 技術來打造 Chat App30

尚未有邦友留言

立即登入留言