Linux中國

測試 Node.js,2018

超過 3 億用戶正在使用 Stream。這些用戶全都依賴我們的框架,而我們十分擅長測試要放到生產環境中的任何東西。我們大部分的代碼庫是用 Go 語言編寫的,剩下的部分則是用 Python 編寫。

我們最新的展示應用,Winds 2.0,是用 Node.js 構建的,很快我們就了解到測試 Go 和 Python 的常規方法並不適合它。而且,創造一個好的測試套件需要用 Node.js 做很多額外的工作,因為我們正在使用的框架沒有提供任何內建的測試功能。

不論你用什麼語言,要構建完好的測試框架可能都非常複雜。本文我們會展示 Node.js 測試過程中的困難部分,以及我們在 Winds 2.0 中用到的各種工具,並且在你要編寫下一個測試集合時為你指明正確的方向。

為什麼測試如此重要

我們都向生產環境中推送過糟糕的提交,並且遭受了其後果。碰到這樣的情況不是好事。編寫一個穩固的測試套件不僅僅是一個明智的檢測,而且它還讓你能夠完全地重構代碼,並自信重構之後的代碼仍然可以正常運行。這在你剛剛開始編寫代碼的時候尤為重要。

如果你是與團隊共事,達到測試覆蓋率極其重要。沒有它,團隊中的其他開發者幾乎不可能知道他們所做的工作是否導致重大變動(或破壞)。

編寫測試同時會促進你和你的隊友把代碼分割成更小的片段。這讓別人去理解你的代碼和修改 bug 變得容易多了。產品收益變得更大,因為你能更早的發現 bug。

最後,沒有測試,你的基本代碼還不如一堆紙片。基本不能保證你的代碼是穩定的。

困難的部分

在我看來,我們在 Winds 中遇到的大多數測試問題是 Node.js 中特有的。它的生態系統一直在變大。例如,如果你用的是 macOS,運行 brew upgrade(安裝了 homebrew),你看到你一個新版本的 Node.js 的概率非常高。由於 Node.js 迭代頻繁,相應的庫也緊隨其後,想要與最新的庫保持同步非常困難。

以下是一些馬上映入腦海的痛點:

  1. 在 Node.js 中進行測試是非常主觀而又不主觀的。人們對於如何構建一個測試架構以及如何檢驗成功有不同的看法。沮喪的是還沒有一個黃金準則規定你應該如何進行測試。
  2. 有一堆框架能夠用在你的應用里。但是它們一般都很精簡,沒有完好的配置或者啟動過程。這會導致非常常見的副作用,而且還很難檢測到;所以你最終會想要從零開始編寫自己的 測試執行平台 test runner 測試執行平台。
  3. 幾乎可以保證你 需要 編寫自己的測試執行平台(馬上就會講到這一節)。

以上列出的情況不是理想的,而且這是 Node.js 社區應該儘管處理的事情。如果其他語言解決了這些問題,我認為也是作為廣泛使用的語言, Node.js 解決這些問題的時候。

編寫你自己的測試執行平台

所以……你可能會好奇test runner測試執行平台 什麼,說實話,它並不複雜。測試執行平台是測試套件中最高層的容器。它允許你指定全局配置和環境,還可以導入配置。可能有人覺得做這個很簡單,對吧?別那麼快下結論。

我們所了解到的是,儘管現在就有足夠多的測試框架了,但沒有一個測試框架為 Node.js 提供了構建你的測試執行平台的標準方式。不幸的是,這需要開發者來完成。這裡有個關於測試執行平台的需求的簡單總結:

  • 能夠載入不同的配置(比如,本地的、測試的、開發的),並確保你 永遠不會 載入一個生產環境的配置 —— 你能想像出那樣會出什麼問題。
  • 播種資料庫——產生用於測試的數據。必須要支持多種資料庫,不論是 MySQL、PostgreSQL、MongoDB 或者其它任何一個資料庫。
  • 能夠載入配置(帶有用於開發環境測試的播種數據的文件)。

開發 Winds 的時候,我們選擇 Mocha 作為測試執行平台。Mocha 提供了簡單並且可編程的方式,通過命令行工具(整合了 Babel)來運行 ES6 代碼的測試。

為了進行測試,我們註冊了自己的 Babel 模塊引導器。這為我們提供了更細的粒度,更強大的控制,在 Babel 覆蓋掉 Node.js 模塊載入過程前,對導入的模塊進行控制,讓我們有機會在所有測試運行前對模塊進行模擬。

此外,我們還使用了 Mocha 的測試執行平台特性,預先把特定的請求賦給 HTTP 管理器。我們這麼做是因為常規的初始化代碼在測試中不會運行(伺服器交互是用 Chai HTTP 插件模擬的),還要做一些安全性檢查來確保我們不會連接到生產環境資料庫。

儘管這不是測試執行平台的一部分,有一個 配置 fixture 載入器也是我們測試套件中的重要的一部分。我們試驗過已有的解決方案;然而,我們最終決定編寫自己的助手程序,這樣它就能貼合我們的需求。根據我們的解決方案,在生成或手動編寫配置時,通過遵循簡單專有的協議,我們就能載入數據依賴很複雜的配置。

Winds 中用到的工具

儘管過程很冗長,我們還是能夠合理使用框架和工具,使得針對後台 API 進行的適當測試變成現實。這裡是我們選擇使用的工具:

Mocha

Mocha,被稱為 「運行在 Node.js 上的特性豐富的測試框架」,是我們用於該任務的首選工具。擁有超過 15K 的星標,很多支持者和貢獻者,我們知道對於這種任務,這是正確的框架。

Chai

然後是我們的斷言庫。我們選擇使用傳統方法,也就是最適合配合 Mocha 使用的 —— Chai。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 是模擬庫的首選。而且,精簡安裝帶來的超級整潔的整合,讓 Sinon 把模擬請求變成了簡單而輕鬆的過程。它的網站有極其良好的用戶體驗,並且提供簡單的步驟,供你將 Sinon 整合進自己的測試框架中。

Nock

對於所有外部的 HTTP 請求,我們使用健壯的 HTTP 模擬庫 nock,在你要和第三方 API 交互時非常易用(比如說 Stream 的 REST API)。它做的事情非常酷炫,這就是我們喜歡它的原因,除此之外關於這個精妙的庫沒有什麼要多說的了。這是我們的速成示例,調用我們在 Stream 引擎中提供的 personalization

nock(config.stream.baseUrl)
    .get(/winds_article_recommendations/)
    .reply(200, { results: [{foreign_id:`article:${article.id}`}] });

Mock-require

mock-require 庫允許依賴外部代碼。用一行代碼,你就可以替換一個模塊,並且當代碼嘗試導入這個庫時,將會產生模擬請求。這是一個小巧但穩定的庫,我們是它的超級粉絲。

Istanbul

Istanbul 是 JavaScript 代碼覆蓋工具,在運行測試的時候,通過模塊鉤子自動添加覆蓋率,可以計算語句,行數,函數和分支覆蓋率。儘管我們有相似功能的 CodeCov(見下一節),進行本地測試時,這仍然是一個很棒的工具。

最終結果 — 運行測試

有了這些庫,還有之前提過的測試執行平台,現在讓我們看看什麼是完整的測試(你可以在 [這裡](https://github.com/GetStream/Winds/tree/master/api/test) 看看我們完整的測試套件):

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,因為他們和我們一樣喜愛開源環境。考慮到 Winds 是開源的,它再合適不過了。

我們的集成非常簡單 —— 我們用 [.travis.yml] 文件設置環境,通過簡單的 npm 命令進行測試。測試覆蓋率反饋給 GitHub,在 GitHub 上我們將清楚地看出我們最新的代碼或者 PR 是不是通過了測試。GitHub 集成很棒,因為它可以自動查詢 Travis CI 獲取結果。以下是一個在 GitHub 上看到 (經過了測試的) PR 的簡單截圖:

除了 Travis CI,我們還用到了叫做 CodeCov 的工具。CodeCov 和 [Istanbul] 很像,但它是個可視化的工具,方便我們查看代碼覆蓋率、文件變動、行數變化,還有其他各種小玩意兒。儘管不用 CodeCov 也可以可視化數據,但把所有東西囊括在一個地方也很不錯。

我們學到了什麼

在開發我們的測試套件的整個過程中,我們學到了很多東西。開發時沒有所謂「正確」的方法,我們決定開始創造自己的測試流程,通過理清楚可用的庫,找到那些足夠有用的東西添加到我們的工具箱中。

最終我們學到的是,在 Node.js 中進行測試不是聽上去那麼簡單。還好,隨著 Node.js 持續完善,社區將會聚集力量,構建一個堅固穩健的庫,可以用「正確」的方式處理所有和測試相關的東西。

但在那時到來之前,我們還會接著用自己的測試套件,它開源在 Winds 的 GitHub 倉庫

局限

創建配置沒有簡單的方法

有的框架和語言,就如 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 WeeklyNode Weekly 是良好的開始。還有,關注一些 reddit 子模塊如 /r/node 也不錯。如果你喜歡了解最新的趨勢,State of JS 在測試領域幫助開發者可視化趨勢方面就做的很好。

最後,這裡是一些我喜歡的博客,我經常在這上面發文章:

覺得我遺漏了某些重要的東西?在評論區或者 Twitter @NickParsons 讓我知道。

還有,如果你想要了解 Stream,我們的網站上有很棒的 5 分鐘教程。點 這裡 進行查看。

作者簡介:

Nick Parsons

Dreamer. Doer. Engineer. Developer Evangelist https://getstream.io.

via: https://hackernoon.com/testing-node-js-in-2018-10a04dd77391

作者:Nick Parsons 譯者:BriFuture 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國