在瀏覽器中測試 Vue 元件
原文由 Julia Evans 于 發布,訂閱此部落格
哈囉!我在這個部落格的其中一個長期計畫是研究如何在不使用 Node 或任何其他伺服器端 JS 執行環境的情況下撰寫前端 JavaScript。
我在前端 JS 專案中常遇到的一個問題是,我不知道該怎麼幫它們寫測試。以前我試過用 Playwright,但感覺每次都要啟動新的瀏覽器程序,又慢又笨重,而且還需要一些 Node 程式碼來協調整個測試流程。
結果就是,我乾脆都不測試前端程式碼,感覺不太好。通常我也不太常更新這些專案,所以這個問題不常出現,但如果能更有信心地做修改就太好了!所以,找到一種我喜歡的前端測試方式,已經在我的願望清單上很久了。
點子:直接在瀏覽器分頁裡跑測試
前陣子 Alex Chan 寫了一篇很棒的文章,叫做Testing JavaScript without a (third-party) framework,是回應我在這個系列中之前的一篇文章,裡面說明了如何寫一個能在瀏覽器頁面中執行的小型單元測試框架。
當時我很喜歡那篇文章,但它只談了單元測試,而我想為我的 Vue 元件寫端對端整合測試,卻不知道該怎麼做。
所以前幾天在跟Marco 聊天的時候,他說了類似「你知道嗎,你可以直接在瀏覽器裡跑 Vue 元件的測試」這樣的話,我就想:「嘿,我應該再來試試看!!!」
這些事都是我昨天才剛做完的,當然還有很多可以改進的地方,但在忘記之前,我想先把過程中注意到的幾件事記錄下來。
這對我來說有點棘手,因為 Vue 官網通常會假設你在建置流程中多少會用到 Node(很多教學都是「步驟一:npm install THING」),而我不想用 Node/Deno 等等。不過最後發現其實沒那麼複雜。
我接下來要談的測試對象,是我在 2023 年寫的這個 zine 回饋網站。
測試框架:QUnit
我用的是QUnit。它運作得很好,但關於它的運作原理我沒什麼特別好說的,就先這樣吧。我覺得 Alex 那種「自己寫一個測試框架」的做法應該也行得通。我是照著這份說明做的。
我很喜歡 QUnit 有一個「重新執行測試」的按鈕,可以只重跑單一測試。因為我的測試裡有很多網路請求,有辦法只跑一個測試,在除錯時就清楚多了。
步驟一:為測試設定好元件
我需要做的第一件事,是在測試環境中把我的 Vue 元件設定好。
我把主應用程式改成把所有元件都放到 window._components 裡,大概像這樣:
const components = {
'Feedback': FeedbackComponent,
...
}
window._components = components;接著我就能寫一個 mountComponent 函式,它做的事情基本上跟我平常的主應用程式一模一樣(渲染一個包含我想用元件的小模板)。唯一的差別在於:
- 我可以選擇性地傳入一些額外的資料來當作它的 props。
- 它會把元件掛載到一個暫時的、看不見的 div 上,測試結束後這個 div 就會從 DOM 中移除。這個 div 被定位在頁面之外(
position: absolute; top: -10000, ...),所以你看不到它。
使用 mountComponent 函式的樣子像這樣:
const {div} = mountComponent(
'<Page :feedbacks="feedbacks" id=2 />',
{feedbacks: [testFeedback]},
);而它的程式碼如下:
function mountComponent(template, data) {
const app = Vue.createApp({
template: template,
data: () => data,
})
for (const [c, v] of Object.entries(window._components)) {
app.component(c, v);
}
const div = document.getElementById('qunit-fixture')
.appendChild(document.createElement('div'));
return div;
}結果會得到一個 div,我可以在上面用程式去點擊、填寫表單資料、檢查是否出現正確的內容等等。
步驟二:加入一些測試用的固定資料
因為我要寫端對端整合測試來確保我的客戶端 JS 能跟伺服器正常搭配,我需要在資料庫裡準備一些測試資料。所以我寫了大約 25 行 SQL 來在資料庫中建立測試資料,並在開發伺服器上加了一個端點來執行這些 SQL,把測試資料重設到已知的狀態。
async function reset() {
return fetch('/api/reset_test_data', {method: "POST"})
}然後只要在任何需要測試資料的測試開頭執行 await reset() 就好了。
我的 reset() 函式其實沒有每次都完全重設所有東西,這有點不好,但一開始這樣還算堪用,之後隨時可以再改進。
步驟三:一個基本的測試
這就是一個基本測試的樣子!基本上就是渲染出那個 div,並確保裡面包含大致正確的資料。
QUnit.test('renders feedback content', async function (assert) {
const {div} = mountComponent(
'<Page :feedbacks="feedbacks" id=2 image=2 page_hash=2 />',
{feedbacks: [testFeedback]},
);
assert.ok(div.textContent.includes('loved this section'));
})以上就是所有基本的部分!接下來分享一下我在過程中遇到的幾個問題
等待頁面的一部分渲染完成
我的測試中有很多網路請求,需要一些時間才能完成,並讓 Vue 程式碼處理結果、更新 DOM。
我想我們很久以前就學到,在測試中隨便加上 sleep() 然後祈禱時間剛好,是又慢又不穩定、而且非常令人沮喪的做法,所以我需要另一種方法。
就我所知,常見的做法是想辦法從 DOM 來判斷是否可以繼續下一步。例如「如果這個按鈕可見,我們就可以」。
所以我寫了一個小小的 waitFor() 函式,每 20 毫秒輪詢一次,看看條件是否已經達成。它會在 2 秒後逾時。
使用起來的樣子像這樣:
QUnit.test("click item", async function (assert) {
const {div} = mountComponent(
'<Feedback zine_id="test123" image_width="800px" />',
{});
const item = await waitFor(() => div.querySelector('.feedback-item'));
item.click();
// rest of test goes here...
})看起來外面已經有很多這個概念的實作,而且都比我的考慮得更周全。(隨手 Google 一下:qunit-wait-for、playwright expect.poll)
要判斷該等待什麼並不簡單
有些情況下,我以為已經找對了該在 DOM 中等待的東西(「只要等這個 textarea 出現就好!」),但結果因為程式內部的一些細節,實際上我需要等待的是更後面才出現、很難精準抓住的東西。
最後我把其中一個元件改成在完成某個重要動作時,在 DOM 上加上一個隨意的值(像是 data-this-thing-is-ready=true),感覺不是很好。
我猜,解決這類測試問題的正確方法,是做一次重構,同時也讓應用程式對使用者來說更可靠:如果 DOM 中有個元素其實還沒準備好讓使用者互動,或許我根本就不該先把它顯示出來!
加上一些 CSS 類別來辨識元素(但這樣做對嗎?)
最後我在一些 HTML 元素上加了幾個類別,好讓測試能找到它們,不管是因為需要點擊,還是需要等待它們出現在 DOM 中。
之後我可能會想改掉這個做法——前端測試框架似乎都建議避免使用 CSS 類別,而是改用像getByRole 這類方法,或是不得已時再用像data-testid 這樣的屬性。感覺應該有辦法讓應用程式同時變得更無障礙、也更容易測試。
填寫表單很麻煩
要填寫表單,光是設定 value 還不夠,我還需要派發一個事件來告訴 Vue 這個元素已經改變。舉例來說,checkbox 和 textarea 需要不同種類的事件。
textarea.value = 'banana banana banana';
textarea.dispatchEvent(new Event('input'));checkbox.checked = true;
checkbox.dispatchEvent(new Event('change'));這有點煩人,也讓我明白為什麼我可能會想用某種 UI 測試函式庫,例如:
測試涵蓋率
我想了解一下我的測試涵蓋率如何,結果發現 Chrome 其實內建了針對 JS 和 CSS 的程式碼涵蓋率功能!
我的 JS 是用 esbuild 打包成一個叫做 bundle.js 的檔案,所以我只要看 bundle.js 就能知道哪些行沒有被涵蓋到。
這個過程有點挑剔:我必須在 Chrome 開發者工具中關掉 sourcemap 才能讓它正常運作,而且要看到涵蓋率資料,還得照著一串不太直覺的特定步驟操作。
這真的太好玩了!
跟這些系列文章的慣例一樣,我其實從來沒有真正當過前端或後端工程師(除了幫自己做東西之外!),所以我覺得自己一直在學習怎麼完成一些超基本的任務。
做這件事真的讓我玩得很開心。我的前端專案因為都沒有測試,總是感覺很脆弱,或許有一天我也能擁有一套讓我有信心的測試套件!
還有一些我仍在思考的事情:
- 在寫這篇文章的時候,我發現了一個叫做Testing Library 的前端測試函式庫,它對於如何撰寫測試有很多指引,跟我最初的想法很不一樣。我試著把所有東西改成用 Testing Library 重寫,感覺還不錯,之後再看看發展如何。他們有提供一個不需要 Node 就能運作的
.umd.js檔案。 - 我還不太確定該怎麼看待完全沒有辦法在命令列上跑這些測試這件事。或許有個簡單的方法,可以讓我主要在瀏覽器中工作,但需要的時候也能在 CI 裡執行?
隨機一篇部落格
留言
登入後參與討論