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