熟悉測試的讀者可能知道,自動化測試可以大致分成幾個不同的層級:
傳統的測試概念上,測試的佈局要有如金字塔:
因為每往上一層,撰寫測試的成本都會更高,而單元測試可以在比較小的範疇上抓到 Bug。當然靜態檢查的成本更低,所以 Eslint 等等工具是一定要用的。
但這幾年,Web 界的測試大師 - Kent C. Dodds 提出了這個「盃」狀 的測試概念:
個人經驗也很認同這個說法,依靠單元測試的比例過高,會讓自己對整體的運作狀況沒那麼有信心。另外,當想要重構的時候,很多「單元」都被改寫過了,測試當然也要改寫,這種太過於測試實作的細節的測試對於重構很不利。如果當你重構時,你的測試不用修改,而且能讓測試繼續通過,那是個好現象。當然演算法相關的東西還是很適合單元測試的,所以也要看你的軟體的本質來確定要寫怎樣的測試來確保它。
更好的是,「使用者怎麼去使用你的軟體,你就應該怎麼去測試」,並避免測試實作的細節。
在聊天機器人上,要做到自動化的「端對端測試」是很不容易的,你不太容易自動登入一個 LINE 的帳號去跟你的機器人聊天,也不太容易去真的對語音助理講話。而且各平台有太多不同的玩法,就算辦得到成本也是太高。
因此我們大部分應該專注在「整合測試」上面:
這邊我們先以 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 來確保不會發生 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();
跑完第一次後,就會產生 __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
參數去更新。
在寫測試時只要去思考,這測試是否有讓開發者安心?上次遇到的 Bug 是不是再也不會發生?這些測試是不是每次重構都要重寫?大概心裡也有些答案,知道什麼時候寫測試、什麼時候不寫測試、該寫怎樣的測試就能變成一個好的工程師。