[譯] 學習如何構建自動化、跨瀏覽器的 JavaScript 單元測試
我們都知道在多個瀏覽器中測試我們的代碼是多么的重要。至少在我們發布第一個項目的時候,我認為我們在網絡開發社區做大部分工作還是相當不錯的。
我們做的不夠好的工作是測試代碼時每一次做出的改變。
我個人對此感到很慚愧。我已經把“學習如何構建自動化、跨瀏覽器的JavaScript的單元測試”列在我的年度to-do清單中,但我每一次坐下來真正想要做的時候,我又退卻了。雖然我肯定這一部分原因是因為我的懶惰,同時我認為這也是由于缺乏良好的可用信息在這個主題上。
有許多工具和框架(例如 Karma)宣稱“要使自動化的JavaScript測試變得簡單”,但以我的經驗看來這些工具引入的復雜性比他們擺脫的復雜性更多。在我的工作經驗中,如果你是一個專家這些工具“能工作”的很好,但對于一個初學者是很糟糕的。我想要真正了解的是這個流程是如何在引擎中工作的,以便在它出現問題的時候(總會出現問題的),我能解決它。
對我來說,充分了解這些是如何工作的最好方法就是嘗試從頭開始重新創建它。所以我決定去構建我自己的測試工具,然后把我的所學分享到社區中。
手工測試流程
在我解釋自動化過程之前,我認為最重要的是確保我們都在同一頁面上進行手工測試工作。
畢竟,自動化是關于使用機器來關閉負載的重復部分的現有工作流程。如果你在充分理解手工過程之前嘗試去開始自動化,它也不會像你理解了自動化過程一樣。
在手工過程中,你寫了一個你的測試文件,它可能看起來像是:
var assert = require('assert');
var SomeClass = require('../lib/some-class');
describe('SomeClass', function() {
describe('someMethod', function() {
it('accept thing A and transforms it into thing B',function() {
var sc = new SomeClass();
assert.equal(sc.someMethod('A'), 'B');
});
});
});
這個例子用了 Mocha 和Node.js 資源 模塊,但是重要的不是你是用的測試庫或者斷言庫,它可以使任意一個。
在Mocha中運行Node.js,在你終端通過命令行你就能運行這個測試:
mocha test/some-class-test.js
你需要一個帶有 <script> 標簽的HTML文件加載這段腳本,才能在瀏覽器運行這個測試,瀏覽器并不認識 require 聲明,你需要一個像是 browserify 或者 webpack 的模塊打包工具去解決這些依賴。
browserify test/*-test.js > test/index.js
像是browserify或是webpack的模塊打包工具的好處就是它能整合你的所有測試(也包括依賴)到一個單一的文件中,這樣就能很容易加載到你的測試頁面。
一個用Mocha寫的典型測試文件看起來像是這樣的:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Tests<title>
<link href="../node_modules/mocha/mocha.css" rel="stylesheet" />
<script src="../node_modules/mocha/mocha.js"></script>
</head>
<body>
<!-- A container element for the visual Mocha results -->
<div id="mocha"></div>
<!-- Mocha setup and initiation code -->
<script>
mocha.setup('bdd');
window.onload = function() {
mocha.run();
};
</script>
<!-- The script under test -->
<script src="index.js></script>
</body>
</html>
如果你不使用Node.js,那么你的起點看起來已經很像這個HTML文本了,唯一不同的是你的依賴可能需要列成一個個單獨的 <script> 標簽。
錯誤檢測
如果一個測試由于是斷言不正確,你的斷言庫任何時間都會拋出一個錯誤,這個時候你的測試框架就能發現這個錯誤。測試框架運行在每個測試的try/catch中來捕獲可能會拋出的錯誤,這些錯誤報告會顯示在你的頁面中或是在console中顯示這些log。
大多數的測試框架(像是Mocha)將會提供鉤子,這樣你就能在測試過程中讓頁面中的其他腳本訪問測試結果。這是一個自動化測試過程的一個關鍵特征,因為為了自動化工作,自動化腳本需要能夠提取測試腳本的結果。
手工方式的優點
在瀏覽器中手工運行測試的最大好處是,假如你的一個測試失敗了你能用瀏覽器的開發者工具去調試它。
像是這樣的一個簡單例子:
describe('SomeClass', () => {
describe('someMethod', () => {
it('accepts thing A and transforms it into thing B', () => {
const sc = new SomeClass();
debugger:
assert.equal(sc.someMethod('A'), 'B');
)};
)};
)};
現在當你重新打包并刷新瀏覽器打開開發者工具,你就可以通過你的代碼,很容易定位到問題的根源所在。
相比之下,大多數流行的自動化測試框架使這變得很困難!它們提供的方便之處是它們捆綁了你的單元測試并且為你創建一個宿主的HTML頁面。
在你的任何一個測試都不會失敗的時候,這是很好的方式。因為當它們這樣做時,就沒有辦法輕松地reproduce和本地調試。
自動化流程
手工流程有它的有點,同時也有一些缺點。打開幾個瀏覽器去運行測試,每次你想做出改動的時候都會變得繁瑣且容易出錯。更不用說,我們大部分人沒有安裝每一個瀏覽器的每一個版本到我們的本地開發機器上。
如果你在認真的測試你的代碼,并希望確保它的每一個變化都做適當的,那么你需要自動化這個流程。
無論你是多么的自覺,手動測試是太容易忘記或忽略,最終它不會充分利用你的時間。
但是自動化測試同樣也有它的不足。過于頻繁的自動測試工具引入了一個全新的問題。輕微不同的構建,測試就會變得不同,測試失敗的話面臨的將是痛苦的調試。
當我計劃如何構建我的自動化測試系統的時候,我不想再掉進這個陷阱和失去手工測試流程的便利性。所以我決定在開始之前做一個需求列表。
畢竟,一個自動化系統如果引入了新的令人頭疼的麻煩和復雜性,那它就不是一個成功的自動化系統。
需求
-
我需要能夠使用命令行運行測試
-
我需要能夠在本地調試失敗測試
-
我需要所有必需的依賴通過 npm 運行測試就能被安裝,所以任何人查看我的代碼就能很簡單的運行,通過
npm install && npm test
-
我需要運行在CI機器上的測試流程和運行在我的開發機器一樣簡單。這樣構建方式是相同的,并且無需檢查新的變化就能調試錯誤。
-
我需要所有的測試我(或者任意人)提交新的變化或者拉取請求都能在任何時間自動化運行。
有了這個粗略的列表之后,下一步就是深入到在主流的云測試如何自動化,跨瀏覽器測試的工作。
云測試如何工作
有很多云測試的供應商,每個供應商都有自己的長處和短處。我是一個開源作者,所以我只看那些提供開源項目的供應商,它們之中,只有 Sauce Labs 是唯一一個不需要我郵箱支持就能啟動一個新的開源賬戶。
更令我吃驚的是當我真正開始鉆研Sauce Labs關于JavaScript單元測試的文檔是有多么簡單。由于好多測試框架都有聲稱讓單元測試變得簡單,我認為這真的很難!
我前面強調了一點就是,我不想我的自動化流程和我的手工流程有什么根本上的不同。事實證明,Sauce Labs提供的自動化方法真的很像我的手工方法。
這里是所涉及的步驟:
-
你給Sauce Labs一個你測試頁面的URL以及你要運行的測試的瀏覽器/平臺列表。
-
Sauce Labs使用 selenium webdriver 去加載你給它的每一個瀏覽器和平臺的組合的測試頁面。
-
WebDriver檢查網頁是否測試失敗,并將結果存儲。
-
Sauce Labs將有用的結果給你。
這真的很簡單。
我錯誤地假設你不得不把你的JavaScript代碼給Sauce Labs,并且它將會運行在它的機器上,而不是它們只是去訪問你給它們的URL。這樣的話看起來就像手工流程了;唯一不同的是Sauce Labs去打開所有的瀏覽器并為你記錄下結果。
API方法
Sauce Labs有兩個運行單元測試的API方法:
StartJS Unit Tests方法在你指定的瀏覽器/平臺啟動一個測試頁面。
文檔給了一個使用 curl 的例子:
curl https://saucelabs.com/rest/v1/SAUCE_USERNAME/js-tests \
-X POST \
-u SAUCE_USERNAME:SAUCE_ACCESS_KEY \
-H 'Content-Type: application/json' \
--data '{"url": "https://example.com/tests.html", "framework": "mocha", "platforms": [["Windows 7", "firefox", "27"], ["Linux", "chrome", "latest"]]}'
因為這是JavaScript單元測試,我將給你一個使用node模塊 request 的例子,如果你正在用Node.js它可能更接近你最終要做的:
request({
url: `https://saucelabs.com/rest/v1/${username}/js-tests`,
method: 'POST',
auth: {
username: process.env.SAUCE_USERNAME,
password: process.env.SAUCE_ACCESS_KEY
},
json: true,
body: {
url: 'https://example.com/tests.html',
framework: 'mocha',
platforms: [
['Windows 7', 'firefox', '27'],
['Linux', 'chrome', 'latest']
]
}
}, (err, response) => {
if (err) {
console.error(err);
} else {
console.log(response.body);
}
});
你注意到body中的 framework: 'mocha' 。Sauce Labs供應商支持許多主流的JavaScript單元測試框架,包括 Mocha,Jasmine,Qunit和YUI。“支持”意味著Sauce Labs的webdriver客戶端知道去哪獲取測試結果。
如果你沒有使用上面提到的測試框架,你能可以通過設置 framework: 'custom' ,Sauce Labs將會代替找到的全局變量 window.global_test_results 。格式化的結果被列在文檔中的 自定義框架 一節中。
讓Mocha測試結果對于Sauce Labs的webdriver客戶端有用
盡管你在最初的請求中告訴Sauce Labs你在使用Mocha,你仍然需要去更新你的HTML頁面,去存儲Sauce Labs能訪問的全局變量的測試結果。
為你的HTML頁面增加Mocha支持:
<script>
mocha.setup('bdd');
window.onload = function() {
mocha.run();
};
</script>
做一些事情,像下面這樣:
<script>
mocha.setup('bdd');
window.onload = function() {
var runner = mocha.run();
var failedTests = [];
runner.on('end', function() {
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
};
</script>
在上面的代碼和默認的Mocha模板中唯一不同的是分配給測試結果的變量名,就像Sauce Labs期望的格式一樣叫做 window.mochaResults 。因為這個新的代碼不會影響正在瀏覽器中運行的手工測試,你不妨就開始使用它作為默認的Mocha模板。
再次強調一點,當Sauce Labs“運行”你的測試時,它并沒有做任何事,它只是單純的訪問一個頁面,等到發現一個 window.mochaResults 對象,然后記錄下這個結果。
確定你的測試通過還是失敗
StartJS Unit Tests 方法會告訴Sauce Labs去挨個在你指定的瀏覽器/平臺運行測試,但是它不會返回測試的結果。
它返回所有工作隊列中的ID,響應看起來像是這樣的:
{
"js tests": [
"9b6a2d7e6c8d4fd2afeeb0ff7e54e694",
"d38688ec7256497da6966f4523ddee76",
"14054e68ccd344c0bed77a798a9ce1e8",
"dbc54181f7d947458f52201ea5fcb901"
]
}
要確定你測試通過還是失敗,你要調用GetJS Unit Status方法,它接手一個工作隊列并且返回當前每個工作的工作狀態。
這個想法是你要定期調用這個方法,知道所有工作都完成。
request({
url: `https://saucelabs.com/rest/v1/${username}/js-tests/status`,
method: 'POST',
auth: {
username: process.env.SAUCE_USERNAME,
password: process.env.SAUCE_ACCESS_KEY
},
json: true,
body: jsTests, // The response.body from the first API call.
}, (err, response) => {
if (err) {
console.error(err);
} else {
console.log(response.body);
}
});
響應的結果看起來像是這樣:
{
"completed": false,
"js tests": [
{
"url": "https://saucelabs.com/jobs/75ac4cadb85e415fae957f7811d778b8",
"platform": [
"Windows 10",
"chrome", "latest"
],
"result": {
"passes": 29,
"tests": 30,
"end": {},
"suites": 7,
"reports": [],
"start": {},
"duration": 97,
"failures": 0,
"pending": 1
},
"id": "1f74a237d5ba4a47b5a42570ae1e7999",
"job_id": "75ac4cadb85e415fae957f7811d778b8"
},
// ... the rest of the jobs
]
}
一旦 response.body.complete 屬性值為 true ,就表示你的測試已經運行完成,然后你就可以通過檢查每個工作流程的通過還是失敗。
本地訪問測試
我已經解釋過Sauce Labs“運行”你的測試通過訪問一個URL。當然,這意味著這個URL必須是公開在網絡上可訪問的鏈接。
有一個問題就是如果你的測試服務啟動在 localhost 。
有很多解決這個問題的方案,包括 Sauce Connect (官方推薦的一種),這是一個由Sauce Labs創建的代理服務器,在Sauce Labs虛擬機和本地主機之間開啟一個安全連接。
Sauce Labs是處于安全性的考慮被設計的,并且使得外部無法獲得你的代碼。它的缺點就是十分復雜的設置與使用。
如果你的代碼涉及到安全性,它可能值得你去弄清楚Sauce Labs;如果不是的話,有許多相似的方案去更簡單的解決這個問題。
我選擇的方案是 ngrok
ngrok
ngrok是一個用于創建安全隧道連接工具。它給你一個公共的URL到web服務器運行在你的本地機器上,確切的是你需要運行測試在Sauce Labs上。
如果你在虛擬機上進行開發或手動測試,你可能已經聽說過ngrok,如果沒有,那你應該去查閱一下了,它是極其有用的工具。
在你的機器上安裝ngrok像是下載二進制文件,然后添加到你的路徑中一樣簡單;如果你將會在Node中使用ngrok,你也需要通過npm安裝它。
npm install ngrok
你可以用下面的代碼以編程方式從Node中開始ngrok進程:
const ngrok = require('ngrok');
ngrok.connect(port, (err, url) => {
if (err) {
console.error(err);
} else {
console.log(`Tests now accessible at: ${url}`);
}
});
只要你有一個公共的URL能訪問你的測試文件,用Sauce Labs跨瀏覽器測試你的本地代碼會變得十分容易。
整合碎片化
這篇文章包含了很多主題,給人的印象是自動化的,跨瀏覽器的JavaScript單元測試是復雜的。但情況并非如此。
我從我的角度來看這篇文章-當我試圖去解決這個問題。然后回顧我之前的經驗,真正復雜的是缺少解決整個流程如何工作的有效信息,和怎么樣把所有的整合到一起。
一旦你了解了所有的步驟,它很簡單。總結:
最初的手工流程
-
寫一個測試然后創建一個單一的HTML頁面去運行它。
-
在本地的一個或者兩個瀏覽器中運行這個測試,確保它能工作。
增加自動化流程
-
創建一個開源的Sauce Labs賬號,獲得一個用戶名和訪問權限。
-
更新你的測試頁面源碼,以便Sauce Labs能通過JavaScript全局變量讀取測試結果。
-
用ngrok給你的本地測試頁面創建一個安全隧道,這樣就能在互聯網公開的訪問了。
-
調用StartJS Unit Tests接口方法列出你想測試的瀏覽器/平臺。
-
定時調用GetJS Unit Test Status方法知道工作完成。
-
報告結果。
使測試變得更容易
我知道這篇文章開頭我談了很多關于你不需要一個框架來做自動化,跨瀏覽器的JavaScript單元測試,我現在仍然堅信這個。然而,盡管每一步都很簡單,你可能不想在每次都為項目編寫代碼。
我想給我的很多老項目增加自動化測試,所以對我來說打包這些邏輯到我的模塊中是很有意義的。
我推薦你嘗試實現一個你自己的框架,這樣你就可以完全理解它是如何工作的,但如果你沒有時間并且還想快速建立一個測試,我建議你使用我創建的庫 Easy Sauce 。
Easy Sauce
Easy Sauce 是一個Node包和一個命令行工具,現在我為我想做跨瀏覽器測試的每一個JavaScript項目都使用這個包。
easy-sauce 命令可以設置你的HTML測試文件的路徑(默認是 /test/ )、開啟本地服務的端口(默認是 1337 端口)和一系列的瀏覽器/平臺進行測試。 easy-sauce 將會在Sauce Lab’s selenium cloud運行你的測試,將日志打印在控制臺并通過合適的狀態碼告知你測試是否通過。
npm包使它變得更方便, easy-sauce 將會默認在 package.json 文件中查找配置選項,所以你不必分別的存儲它們。好處是用戶更加明確的知道你的包支持瀏覽器/平臺。
對于 easy sauce 完整的用法介紹,請查看Github 文檔 。
最后,我想強調的是我專門建立這個項目來解決我的需要。雖然我認為這個項目對于很多開發人員都十分有用,但我沒有計劃把它變成一個功能齊全的測試解決方案。
結語
在這篇文章的開始,我寫下了一系列的需求。在Easy Sauce的幫助下,我正努力的在任何項目中滿足這些需求。
如果你還沒有為你的項目做自動化、跨瀏覽器的JavaScript單元測試,我鼓勵你給Easy Sauce一個嘗試的機會。即使你不想用Easy Sauce,你至少應該了解你自己的需求或更好地了解現有的工具。
Happy testing!
如果你能看到這里,很感謝你的耐心閱讀。這是我翻譯的第一篇技術文檔,自身水平有限,所以翻譯總有不當與疏漏,如有發現還請您耐心評論指出。