W3Cschool
恭喜您成為首批注冊用戶
獲得88經驗值獎勵
自動化測試將被用于進一步的任務中,并且還將被廣泛應用在實際項目中。
當我們在寫一個函數(shù)時,我們通??梢韵胂蟪鏊鼞撟鍪裁矗耗男﹨?shù)會給出哪些結果。
在開發(fā)期間,我們可以通過運行程序來檢查它并將結果與預期進行比較。例如,我們可以在控制臺中這么做。
如果出了問題 —— 那么我們會修復代碼,然后再一次運行并檢查結果 —— 直到它工作為止。
但這樣的手動“重新運行”是不完美的。
當通過手動重新運行來測試代碼時,很容易漏掉一些東西。
例如,我們要創(chuàng)建一個函數(shù) f
。寫一些代碼,然后測試:f(1)
可以執(zhí)行,但是 f(2)
不執(zhí)行。我們修復了一下代碼,現(xiàn)在 f(2)
可以執(zhí)行了??雌饋硪呀浉愣??但是我們忘了重新測試 f(1)
。這樣有可能會導致出現(xiàn)錯誤。
這是非常典型的。當我們在開發(fā)一些東西時,我們會保留很多可能需要的用例。但是不要想著程序員在每一次代碼修改后都去檢查所有的案例。所以這就很容易造成修復了一個問題卻造成另一個問題的情況。
自動化測試意味著測試是獨立于代碼的。它們以各種方式運行我們的函數(shù),并將結果與預期結果進行比較。
我們來使用一種名為 行為驅動開發(fā) 或簡言為 BDD 的技術。
BDD 包含了三部分內容:測試、文檔和示例。
為了理解 BDD,我們將研究一個實際的開發(fā)案例。
我們想要創(chuàng)建一個函數(shù) pow(x, n)
來計算 x
的 n
次冪(n
為整數(shù))。我們假設 n≥0
。
這個任務只是一個例子:JavaScript 中有一個 **
運算符可以用于冪運算。但是在這里我們專注于可以應用于更復雜任務的開發(fā)流程上。
在創(chuàng)建函數(shù) pow
的代碼之前,我們可以想象函數(shù)應該做什么并且描述出來。
這樣的描述被稱作 規(guī)范(specification, spec),包含用例的描述以及針對它們的測試,如下所示:
describe("pow", function() {
it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
});
});
正如你所看到的,一個規(guī)范包含三個主要的模塊:
?describe("title", function() { ... })
?
表示我們正在描述的功能是什么。在我們的例子中我們正在描述函數(shù) pow
。用于組織“工人(workers)” —— it
代碼塊。
?it("use case description", function() { ... })
?
it
里面的描述部分,我們以一種 易于理解 的方式描述特定的用例,第二個參數(shù)是用于對其進行測試的函數(shù)。
?assert.equal(value1, value2)
?
it
塊中的代碼,如果實現(xiàn)是正確的,它應該在執(zhí)行的時候不產生任何錯誤。
assert.*
函數(shù)用于檢查 pow
函數(shù)是否按照預期工作。在這里我們使用了其中之一 —— assert.equal
,它會對參數(shù)進行比較,如果它們不相等則會拋出一個錯誤。這里它檢查了 pow(2, 3)
的值是否等于 8
。還有其他類型的比較和檢查,我們將在后面介紹到。
規(guī)范可以被執(zhí)行,它將運行在 it
塊中指定的測試。我們稍后會看到。
開發(fā)流程通??雌饋硐襁@樣:
如此來看,開發(fā)就是不斷地 迭代。我們寫規(guī)范,實現(xiàn)它,確保測試通過,然后寫更多的測試,確保它們工作等等。最后,我們有了一個能工作的實現(xiàn)和針對它的測試。
讓我們在我們的開發(fā)案例中看看這個開發(fā)流程吧。
在我們的案例中,第一步已經完成了:我們有一個針對 pow
的初始規(guī)范。因此讓我們來實現(xiàn)它吧。但在此之前,讓我們用一些 JavaScript 庫來運行測試,就是看看測試是通過了還是失敗了。
在本教程中,我們將使用以下 JavaScript 庫進行測試:
describe
?和 ?it
?,以及用于運行測試的主函數(shù)。assert.equal
?。這些庫都既適用于瀏覽器端,也適用于服務器端。這里我們將使用瀏覽器端的變體。
包含這些框架和 ?pow
?規(guī)范的完整的 HTML 頁面:
<!DOCTYPE html>
<html>
<head>
<!-- add mocha css, to show results -->
<link rel="stylesheet" rel="external nofollow" target="_blank" >
<!-- add mocha framework code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js" rel="external nofollow" ></script>
<script>
mocha.setup('bdd'); // minimal setup
</script>
<!-- add chai -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js" rel="external nofollow" ></script>
<script>
// chai has a lot of stuff, let's make assert global
let assert = chai.assert;
</script>
</head>
<body>
<script>
function pow(x, n) {
/* function code is to be written, empty now */
}
</script>
<!-- the script with tests (describe, it...) -->
<script src="test.js"></script>
<!-- the element with id="mocha" will contain test results -->
<div id="mocha"></div>
<!-- run tests! -->
<script>
mocha.run();
</script>
</body>
</html>
該頁面可分為五個部分:
<head>
? —— 添加用于測試的第三方庫和樣式文件。<script>
? 包含測試函數(shù),在我們的例子中 —— 和 ?pow
?相關的代碼。test.js
? 的腳本,它包含上面 ?describe("pow", ...)
? 的那些代碼。<div id="mocha">
? 將被 Mocha 用來輸出結果。mocha.run()
? 命令來開始測試。結果:
到目前為止,測試失敗了,出現(xiàn)了一個錯誤。這是合乎邏輯的:我們的 pow
是一個空函數(shù),因此 pow(2,3)
返回了 undefined
而不是 8
。
未來,我們會注意到有更高級的測試工具,像是 karma 或其他的,使自動運行許多不同的測試變得更容易。
為了可以通過測試,讓我們寫一個 pow
的簡單實現(xiàn):
function pow() {
return 8; // :) 我們作弊啦!
}
哇哦,現(xiàn)在它可以工作了。
我們所做的這些絕對是作弊。函數(shù)是不起作用的:嘗試計算 pow(3,4)
的話就會得到一個不正確的結果,但是測試卻通過了。
……但是這種情況卻是在實際中相當?shù)湫屠?。測試通過了,但是函數(shù)卻是錯誤的。我們的規(guī)范是不完善的。我們需要給它添加更多的測試用例。
這里我們又添加了一個測試來檢查 pow(3, 4) = 81
。
我們可以選擇兩種方式中的任意一種來組織測試代碼:
it
中再添加一個 assert
:describe("pow", function() {
it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
assert.equal(pow(3, 4), 81);
});
});
describe("pow", function() {
it("2 raised to power 3 is 8", function() {
assert.equal(pow(2, 3), 8);
});
it("3 raised to power 4 is 81", function() {
assert.equal(pow(3, 4), 81);
});
});
主要的區(qū)別是,當 assert
觸發(fā)一個錯誤時,it
代碼塊會立即終止。因此,在第一種方式中,如果第一個 assert
失敗了,我們將永遠不會看到第二個 assert
的結果。
保持測試之間獨立,有助于我們獲知代碼中正在發(fā)生什么,因此第二種方式更好一點。
除此之外,還有一個規(guī)范值得遵循。
一個測試檢查一個東西。
如果我們在看測試代碼的時候,發(fā)現(xiàn)在其中有兩個相互獨立的檢查 —— 最好將它拆分成兩個更簡單的檢查。
因此讓我們繼續(xù)使用第二種方式。
結果:
正如我們可以想到的,第二條測試失敗了。當然啦,我們的函數(shù)總會返回 8
,而 assert
期望的是 81
。
讓我們寫一些更加實際的代碼來通過測試吧:
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
為了確保函數(shù)可以很好地工作,我們來使用更多值測試它吧。除了手動地編寫 it
代碼塊,我們可以使用 for
循環(huán)來生成它們:
describe("pow", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
結果:
我們繼續(xù)添加更多的測試。但在此之前,我們需要注意到輔助函數(shù) makeTest
和 for
應該被組合到一起。我們在其他測試中不需要 makeTest
,只有在 for
循環(huán)中需要它:它們共同的任務就是檢查 pow
是如何自乘至給定的冪次方。
使用嵌套的 describe
來進行分組:
describe("pow", function() {
describe("raises x to power 3", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
// ……可以在這里寫更多的測試代碼,describe 和 it 都可以添加在這。
});
嵌套的 describe
定義了一個新的 “subgroup” 測試。在輸出中我們可以看到帶有標題的縮進:
將來,我們可以在頂級域中使用 it
和 describe
的輔助函數(shù)添加更多的 it
和 describe
,它們不會看到 makeTest
。
?
before/after
? 和 ?beforeEach/afterEach
?我們可以設置
before/after
函數(shù)來在運行測試之前/之后執(zhí)行。也可以使用beforeEach/afterEach
函數(shù)來設置在執(zhí)行 每一個it
之前/之后執(zhí)行。
例如:
describe("test", function() { before(() => alert("Testing started – before all tests")); after(() => alert("Testing finished – after all tests")); beforeEach(() => alert("Before a test – enter a test")); afterEach(() => alert("After a test – exit a test")); it('test 1', () => alert(1)); it('test 2', () => alert(2)); });
運行順序將為:
Testing started – before all tests (before) Before a test – enter a test (beforeEach) 1 After a test – exit a test (afterEach) Before a test – enter a test (beforeEach) 2 After a test – exit a test (afterEach) Testing finished – after all tests (after)
通常,
beforeEach/afterEach
和before/after
被用于執(zhí)行初始化,清零計數(shù)器或做一些介于每個測試(或測試組)之間的事情。
pow
的基礎功能已經完成了。第一次迭代開發(fā)完成啦。當我們慶祝和喝完香檳之后,讓我們繼續(xù)改進它吧。
正如前面所說,函數(shù) pow(x, n)
適用于正整數(shù) n
。
JavaScript 函數(shù)通常會返回 NaN
以表示一個數(shù)學錯誤。接下來我們對無效的 n
值執(zhí)行相同的操作。
讓我們首先將這個行為加到規(guī)范中(!):
describe("pow", function() {
// ...
it("for negative n the result is NaN", function() {
assert.isNaN(pow(2, -1));
});
it("for non-integer n the result is NaN", function() {
assert.isNaN(pow(2, 1.5));
});
});
新測試的結果:
新加的測試失敗了,因為我們的實現(xiàn)方式是不支持它們的。這就是 BDD 的做法:我們首先寫一些暫時無法通過的測試,然后去實現(xiàn)它們。
Other assertions
請注意斷言語句 assert.isNaN
:它用來檢查 NaN
。
在 Chai 中也有其他的斷言,例如:
assert.equal(value1, value2)
? —— 檢查相等 ?value1 == value2
?。assert.strictEqual(value1, value2)
? —— 檢查嚴格相等 ?value1 === value2
?。assert.notEqual
?,?assert.notStrictEqual
? —— 執(zhí)行和上面相反的檢查。assert.isTrue(value)
? —— 檢查 ?value === true
?。assert.isFalse(value)
? —— 檢查 ?value === false
?。因此我們應該給 pow
再加幾行:
function pow(x, n) {
if (n < 0) return NaN;
if (Math.round(n) != n) return NaN;
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
現(xiàn)在它可以工作了,所有的測試也都通過了:
在 BDD 中,規(guī)范先行,實現(xiàn)在后。最后我們同時擁有了規(guī)范和代碼。
規(guī)范有三種使用方式:
describe
?和 ?it
?的標題告訴我們函數(shù)做了什么。有了規(guī)范,我們可以安全地改進、修改甚至重寫函數(shù),并確保它仍然正確地工作。
這在一個函數(shù)會被用在多個地方的大型項目中尤其重要。當我們改變這樣一個函數(shù)時,沒有辦法手動檢查每個使用它們的地方是否仍舊正確。
如果沒有測試,一般有兩個辦法:
自動化測試則有助于避免這樣的問題!
如果這個項目被測試代碼覆蓋了,就不會出現(xiàn)這種問題。在任何修改之后,我們都可以運行測試,并在幾秒鐘內看到大量的檢查。
另外,一個經過良好測試的代碼通常都有更好的架構。
當然,這是因為覆蓋了自動化測試的代碼更容易修改和改進。但還有另一個原因。
要編寫測試,代碼的組織方式應確保每個函數(shù)都有一個清晰描述的任務、定義良好的輸入和輸出。這意味著從一開始就有一個好的架構。
在實際開發(fā)中有時候可能并不容易,有時很難在寫實際代碼之前編寫規(guī)范,因為還不清楚它應該如何表現(xiàn)。但一般來說,編寫測試使得開發(fā)更快更穩(wěn)定。
在本教程的后面部分,你將遇到許多包含了測試的任務。所以你會看到更多的實際例子。
編寫測試需要良好的 JavaScript 知識。但我們剛剛開始學習它。因此,為了解決所有問題,到目前為止,你不需要編寫測試,但是你應該已經能夠閱讀測試了,即使它們比本章中的內容稍微復雜一些。
重要程度: 5
下面這個 pow
的測試代碼有什么錯誤?
it("Raises x to the power n", function() {
let x = 5;
let result = x;
assert.equal(pow(x, 1), result);
result *= x;
assert.equal(pow(x, 2), result);
result *= x;
assert.equal(pow(x, 3), result);
});
附:從語法上來說這些測試代碼是正確且通過的。
這些測試代碼展示了開發(fā)人員在編寫測試代碼時遇到的一些疑惑。
我們這里實際上有三條測試,但是用了一個函數(shù)來放置 3 個斷言語句。
有時用這種方式編寫會更容易,但是如果發(fā)生錯誤,那么到底什么出錯了就很不明顯。
如果錯誤發(fā)生在一個復雜的執(zhí)行流的中間,那么我們就必須找出那個點的數(shù)據(jù)。我們必須 調試測試。
將測試分成多個具有明確輸入和輸出的 it
代碼塊會更好。
像是這樣:
describe("Raises x to power n", function() {
it("5 in the power of 1 equals 5", function() {
assert.equal(pow(5, 1), 5);
});
it("5 in the power of 2 equals 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5 in the power of 3 equals 125", function() {
assert.equal(pow(5, 3), 125);
});
});
我們使用 describe
和一組 it
代碼塊替換掉了單個的 it
?,F(xiàn)在,如果某個測試失敗了,我們可以清楚地看到數(shù)據(jù)是什么。
此外,我們可以通過編寫 it.only
而不是 it
來隔離單個測試,并以獨立模式運行它:
describe("Raises x to power n", function() {
it("5 in the power of 1 equals 5", function() {
assert.equal(pow(5, 1), 5);
});
// Mocha 將只運行這個代碼塊
it.only("5 in the power of 2 equals 25", function() {
assert.equal(pow(5, 2), 25);
});
it("5 in the power of 3 equals 125", function() {
assert.equal(pow(5, 3), 125);
});
});
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: