https://hackernoon.com/testing-node-js-in-2018-10a04dd77391
作者 | Nick Parsons
譯者 | 周家未 (BriFuture) ???共計翻譯:24 篇 貢獻時間:785 天
超過 3 億使用者正在使用 Stream[1]。這些使用者全都依賴我們的框架,而我們十分擅長測試要放到生產環境中的任何東西。我們大部分的程式碼庫是用 Go 語言編寫的,剩下的部分則是用 Python 編寫。
我們最新的展示應用,Winds 2.0[2],是用 Node.js 構建的,很快我們就瞭解到測試 Go 和 Python 的常規方法並不適合它。而且,創造一個好的測試套件需要用 Node.js 做很多額外的工作,因為我們正在使用的框架沒有提供任何內建的測試功能。
不論你用什麼語言,要構建完好的測試框架可能都非常複雜。本文我們會展示 Node.js 測試過程中的困難部分,以及我們在 Winds 2.0 中用到的各種工具,並且在你要編寫下一個測試集合時為你指明正確的方向。
為什麼測試如此重要
我們都向生產環境中推送過糟糕的提交,並且遭受了其後果。碰到這樣的情況不是好事。編寫一個穩固的測試套件不僅僅是一個明智的檢測,而且它還讓你能夠完全地重構程式碼,並自信重構之後的程式碼仍然可以正常執行。這在你剛剛開始編寫程式碼的時候尤為重要。
如果你是與團隊共事,達到測試改寫率極其重要。沒有它,團隊中的其他開發者幾乎不可能知道他們所做的工作是否導致重大變動(或破壞)。
編寫測試同時會促進你和你的隊友把程式碼分割成更小的片段。這讓別人去理解你的程式碼和修改 bug 變得容易多了。產品收益變得更大,因為你能更早的發現 bug。
最後,沒有測試,你的基本程式碼還不如一堆紙片。基本不能保證你的程式碼是穩定的。
困難的部分
在我看來,我們在 Winds 中遇到的大多數測試問題是 Node.js 中特有的。它的生態系統一直在變大。例如,如果你用的是 macOS,執行 brew upgrade
(安裝了 homebrew),你看到你一個新版本的 Node.js 的機率非常高。由於 Node.js 迭代頻繁,相應的庫也緊隨其後,想要與最新的庫保持同步非常困難。
以下是一些馬上映入腦海的痛點:
以上列出的情況不是理想的,而且這是 Node.js 社群應該儘管處理的事情。如果其他語言解決了這些問題,我認為也是作為廣泛使用的語言, Node.js 解決這些問題的時候。
編寫你自己的測試執行平臺
所以……你可能會好奇test runner測試執行平臺 是 什麼,說實話,它並不複雜。測試執行平臺是測試套件中最高層的容器。它允許你指定全域性配置和環境,還可以匯入配置。可能有人覺得做這個很簡單,對吧?別那麼快下結論。
我們所瞭解到的是,儘管現在就有足夠多的測試框架了,但沒有一個測試框架為 Node.js 提供了構建你的測試執行平臺的標準方式。不幸的是,這需要開發者來完成。這裡有個關於測試執行平臺的需求的簡單總結:
開發 Winds 的時候,我們選擇 Mocha 作為測試執行平臺。Mocha 提供了簡單並且可程式設計的方式,透過命令列工具(整合了 Babel)來執行 ES6 程式碼的測試。
為了進行測試,我們註冊了自己的 Babel 模組引導器。這為我們提供了更細的粒度,更強大的控制,在 Babel 改寫掉 Node.js 模組載入過程前,對匯入的模組進行控制,讓我們有機會在所有測試執行前對模組進行模擬。
此外,我們還使用了 Mocha 的測試執行平臺特性,預先把特定的請求賦給 HTTP 管理器。我們這麼做是因為常規的初始化程式碼在測試中不會執行(伺服器互動是用 Chai HTTP 外掛模擬的),還要做一些安全性檢查來確保我們不會連線到生產環境資料庫。
儘管這不是測試執行平臺的一部分,有一個配置載入器也是我們測試套件中的重要的一部分。我們試驗過已有的解決方案;然而,我們最終決定編寫自己的助手程式,這樣它就能貼合我們的需求。根據我們的解決方案,在生成或手動編寫配置時,透過遵循簡單專有的協議,我們就能載入資料依賴很複雜的配置。
Winds 中用到的工具
儘管過程很冗長,我們還是能夠合理使用框架和工具,使得針對後臺 API 進行的適當測試變成現實。這裡是我們選擇使用的工具:
Mocha
Mocha[3],被稱為 “執行在 Node.js 上的特性豐富的測試框架”,是我們用於該任務的首選工具。擁有超過 15K 的星標,很多支持者和貢獻者,我們知道對於這種任務,這是正確的框架。
Chai
然後是我們的斷言庫。我們選擇使用傳統方法,也就是最適合配合 Mocha 使用的 —— Chai[4]。Chai 是一個用於 Node.js,適合 BDD 和 TDD 樣式的斷言庫。擁有簡單的 API,Chai 很容易整合進我們的應用,讓我們能夠輕鬆地斷言出我們 期望 從 Winds API 中傳回的應該是什麼。最棒的地方在於,用 Chai 編寫測試讓人覺得很自然。這是一個簡短的例子:
describe('retrieve user', () => {
let user;
before(async () => {
await loadFixture('user');
user = await User.findOne({email: authUser.email});
expect(user).to.not.be.null;
});
after(async () => {
await User.remove().exec();
});
describe('valid request', () => {
it('should return 200 and the user resource, including the email field, when retrieving the authenticated user', async () => {
const response = await withLogin(request(api).get(`/users/${user._id}`), authUser);
expect(response).to.have.status(200);
expect(response.body._id).to.equal(user._id.toString());
});
it('should return 200 and the user resource, excluding the email field, when retrieving another user', async () => {
const anotherUser = await User.findOne({email: 'another_user@email.com'});
const response = await withLogin(request(api).get(`/users/${anotherUser.id}`), authUser);
expect(response).to.have.status(200);
expect(response.body._id).to.equal(anotherUser._id.toString());
expect(response.body).to.not.have.an('email');
});
});
describe('invalid requests', () => {
it('should return 404 if requested user does not exist', async () => {
const nonExistingId = '5b10e1c601e9b8702ccfb974';
expect(await User.findOne({_id: nonExistingId})).to.be.null;
const response = await withLogin(request(api).get(`/users/${nonExistingId}`), authUser);
expect(response).to.have.status(404);
});
});
});
Sinon
擁有與任何單元測試框架相適應的能力,Sinon[5] 是模擬庫的首選。而且,精簡安裝帶來的超級整潔的整合,讓 Sinon 把模擬請求變成了簡單而輕鬆的過程。它的網站有極其良好的使用者體驗,並且提供簡單的步驟,供你將 Sinon 整合進自己的測試框架中。
Nock
對於所有外部的 HTTP 請求,我們使用健壯的 HTTP 模擬庫 nock[6],在你要和第三方 API 互動時非常易用(比如說 Stream 的 REST API[7])。它做的事情非常酷炫,這就是我們喜歡它的原因,除此之外關於這個精妙的庫沒有什麼要多說的了。這是我們的速成示例,呼叫我們在 Stream 引擎中提供的 personalization[8]:
nock(config.stream.baseUrl)
.get(/winds_article_recommendations/)
.reply(200, { results: [{foreign_id:`article:${article.id}`}] });
Mock-require
mock-require[9] 庫允許依賴外部程式碼。用一行程式碼,你就可以替換一個模組,並且當程式碼嘗試匯入這個庫時,將會產生模擬請求。這是一個小巧但穩定的庫,我們是它的超級粉絲。
Istanbul
Istanbul[10] 是 JavaScript 程式碼改寫工具,在執行測試的時候,透過模組鉤子自動新增改寫率,可以計算陳述句,行數,函式和分支改寫率。儘管我們有相似功能的 CodeCov(見下一節),進行本地測試時,這仍然是一個很棒的工具。
最終結果 — 執行測試
有了這些庫,還有之前提過的測試執行平臺,現在讓我們看看什麼是完整的測試(你可以在 這裡[11] 看看我們完整的測試套件):
import nock from 'nock';
import { expect, request } from 'chai';
import api from '../../src/server';
import Article from '../../src/models/article';
import config from '../../src/config';
import { dropDBs, loadFixture, withLogin } from '../utils.js';
describe('Article controller', () => {
let article;
before(async () => {
await dropDBs();
await loadFixture('initial-data', 'articles');
article = await Article.findOne({});
expect(article).to.not.be.null;
expect(article.rss).to.not.be.null;
});
describe('get', () => {
it('should return the right article via /articles/:articleId', async () => {
let response = await withLogin(request(api).get(`/articles/${article.id}`));
expect(response).to.have.status(200);
});
});
describe('get parsed article', () => {
it('should return the parsed version of the article', async () => {
const response = await withLogin(
request(api).get(`/articles/${article.id}`).query({ type: 'parsed' })
);
expect(response).to.have.status(200);
});
});
describe('list', () => {
it('should return the list of articles', async () => {
let response = await withLogin(request(api).get('/articles'));
expect(response).to.have.status(200);
});
});
describe('list from personalization', () => {
after(function () {
nock.cleanAll();
});
it('should return the list of articles', async () => {
nock(config.stream.baseUrl)
.get(/winds_article_recommendations/)
.reply(200, { results: [{foreign_id:`article:${article.id}`}] });
const response = await withLogin(
request(api).get('/articles').query({
type: 'recommended',
})
);
expect(response).to.have.status(200);
expect(response.body.length).to.be.at.least(1);
expect(response.body[0].url).to.eq(article.url);
});
});
});
持續整合
有很多可用的持續整合服務,但我們鐘愛 Travis CI[12],因為他們和我們一樣喜愛開源環境。考慮到 Winds 是開源的,它再合適不過了。
我們的整合非常簡單 —— 我們用 [.travis.yml] 檔案設定環境,透過簡單的 npm[13] 命令進行測試。測試改寫率反饋給 GitHub,在 GitHub 上我們將清楚地看出我們最新的程式碼或者 PR 是不是透過了測試。GitHub 整合很棒,因為它可以自動查詢 Travis CI 獲取結果。以下是一個在 GitHub 上看到 (經過了測試的) PR 的簡單截圖:
除了 Travis CI,我們還用到了叫做 CodeCov[14] 的工具。CodeCov 和 [Istanbul] 很像,但它是個視覺化的工具,方便我們檢視程式碼改寫率、檔案變動、行數變化,還有其他各種小玩意兒。儘管不用 CodeCov 也可以視覺化資料,但把所有東西囊括在一個地方也很不錯。
我們學到了什麼
在開發我們的測試套件的整個過程中,我們學到了很多東西。開發時沒有所謂“正確”的方法,我們決定開始創造自己的測試流程,透過理清楚可用的庫,找到那些足夠有用的東西新增到我們的工具箱中。
最終我們學到的是,在 Node.js 中進行測試不是聽上去那麼簡單。還好,隨著 Node.js 持續完善,社群將會聚集力量,構建一個堅固穩健的庫,可以用“正確”的方式處理所有和測試相關的東西。
但在那時到來之前,我們還會接著用自己的測試套件,它開源在 Winds 的 GitHub 倉庫[11]。
侷限
建立配置沒有簡單的方法
有的框架和語言,就如 Python 中的 Django,有簡單的方式來建立配置。比如,你可以使用下麵這些 Django 命令,把資料匯出到檔案中來自動化配置的建立過程:
以下命令會把整個資料庫匯出到 db.json
檔案中:
./manage.py dumpdata > db.json
以下命令僅匯出 django 中 admin.logentry
表裡的內容:
./manage.py dumpdata admin.logentry > logentry.json
以下命令會匯出 auth.user
表中的內容:
./manage.py dumpdata auth.user > user.json
Node.js 裡面沒有建立配置的簡單方式。我們最後做的事情是用 MongoDB Compass 工具匯出資料到 JSON 中。這生成了不錯的配置,如下圖(但是,這是個乏味的過程,肯定會出錯):
使用 Babel,模擬模組和 Mocha 測試執行平臺時,模組載入不直觀
為了支援多種 node 版本,和獲取 JavaScript 標準的最新附件,我們使用 Babel 把 ES6 程式碼轉換成 ES5。Node.js 模組系統基於 CommonJS 標準,而 ES6 模組系統中有不同的語意。
Babel 在 Node.js 模組系統的頂層模擬 ES6 模組語意,但由於我們要使用 mock-require 來介入模組的載入,所以我們經歷了罕見的怪異的模組載入過程,這看上去很不直觀,而且能導致在整個程式碼中,匯入的、初始化的和使用的模組有不同的版本。這使測試時的模擬過程和全域性狀態管理複雜化了。
在使用 ES6 模組時宣告的函式,模組內部的函式,都無法模擬
當一個模組匯出多個函式,其中一個函式呼叫了其他的函式,就不可能模擬使用在模組內部的函式。原因在於當你取用一個 ES6 模組時,你得到的取用集合和模組內部的是不同的。任何重新系結取用,將其指向新值的嘗試都無法真正影響模組內部的函式,內部函式仍然使用的是原始的函式。
最後的思考
測試 Node.js 應用是複雜的過程,因為它的生態系統總在發展。掌握最新和最好的工具很重要,這樣你就不會掉隊了。
如今有很多方式獲取 JavaScript 相關的新聞,導致與時俱進很難。關註郵件新聞刊物如 JavaScript Weekly[15] 和 Node Weekly[16] 是良好的開始。還有,關註一些 reddit 子模組如 /r/node[17] 也不錯。如果你喜歡瞭解最新的趨勢,State of JS[18] 在測試領域幫助開發者視覺化趨勢方面就做的很好。
最後,這裡是一些我喜歡的部落格,我經常在這上面發文章:
覺得我遺漏了某些重要的東西?在評論區或者 Twitter @NickParsons[22] 讓我知道。
還有,如果你想要瞭解 Stream,我們的網站上有很棒的 5 分鐘教程。點 這裡[23] 進行檢視。
作者簡介:
Nick Parsons
Dreamer. Doer. Engineer. Developer Evangelist https://getstream.io.
via: https://hackernoon.com/testing-node-js-in-2018-10a04dd77391
作者:Nick Parsons[26] 譯者:BriFuture 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出