後續任務中將會使用自動化測試,它在實際專案中也廣泛使用。
為什麼我們需要測試?
當我們撰寫函式時,通常可以想像它應該做什麼:哪些參數會產生哪些結果。
在開發過程中,我們可以透過執行函式並將結果與預期結果進行比較來檢查函式。例如,我們可以在主控台中執行此操作。
如果有些地方出錯,我們就修正程式碼,再次執行,檢查結果,依此類推,直到它正常運作為止。
但這種手動「重新執行」並不完美。
透過手動重新執行來測試程式碼時,很容易遺漏某些地方。
例如,我們正在建立函式 f
。撰寫了一些程式碼,進行測試:f(1)
正常運作,但 f(2)
無法運作。我們修正程式碼,現在 f(2)
正常運作了。看起來很完整?但我們忘記重新測試 f(1)
了。這可能會導致錯誤。
這是很典型的狀況。當我們開發某些東西時,我們會記住許多可能的用例。但很難期待程式設計師在每次變更後手動檢查所有用例。因此,很容易修正一件事,卻損壞另一件事。
自動化測試表示測試是另外編寫的,除了程式碼之外。它們以各種方式執行我們的函式,並將結果與預期結果進行比較。
行為驅動開發 (BDD)
讓我們從一種名為 行為驅動開發 的技術開始,簡稱 BDD。
BDD 三者合一:測試、文件和範例。
為了瞭解 BDD,我們將探討一個實際的開發案例。
開發「pow」:規格
假設我們要製作一個函式 pow(x, n)
,將 x
提升到整數次方 n
。我們假設 n≥0
。
該任務只是一個範例:JavaScript 中有 **
算子可以執行此操作,但我們在此專注於可應用於更複雜任務的開發流程。
在建立 pow
的程式碼之前,我們可以想像這個函式應該做什麼並加以描述。
這種描述稱為規格,簡稱 spec,其中包含用例的描述以及對它們的測試,如下所示
describe("pow", function() {
it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
});
});
規格有三個主要的建構區塊,您可以在上面看到
describe("標題", function() { ... })
-
我們正在描述什麼功能?在我們的案例中,我們正在描述函式
pow
。用於將「工作人員」分組,即it
區塊。 it("用例描述", function() { ... })
-
在
it
的標題中,我們以人類可讀的方式描述特定用例,而第二個引數是測試它的函式。 assert.equal(value1, value2)
-
如果實作正確,
it
區塊內的程式碼應執行而不會產生錯誤。函式
assert.*
用於檢查pow
是否按預期運作。我們在此使用其中一個函式assert.equal
,它會比較引數,如果它們不相等,就會產生錯誤。在此,它檢查pow(2, 3)
的結果是否等於8
。還有其他類型的比較和檢查,我們稍後會加入。
規格可以執行,它將執行 it
區塊中指定的測試。我們稍後會看到。
開發流程
開發流程通常如下所示
- 撰寫初始規格,其中包含最基本功能的測試。
- 建立初始實作。
- 為了檢查它是否運作,我們執行測試架構 Mocha(稍後會提供更多詳細資訊),它會執行規格。在功能尚未完成時,會顯示錯誤。我們會進行修正,直到一切正常運作。
- 現在我們有一個運作的初始實作和測試。
- 我們在規格中加入更多用例,實作可能還不支援這些用例。測試開始失敗。
- 前往第 3 步,更新實作直到測試沒有錯誤。
- 重複步驟 3-6 直到功能就緒。
因此,開發是反覆進行的。我們撰寫規格、實作它、確保測試通過,然後撰寫更多測試、確保它們運作等等。最後,我們同時擁有可運作的實作和對應的測試。
讓我們在實際案例中看看這個開發流程。
第一步已經完成:我們有一個 pow
的初始規格。現在,在進行實作之前,讓我們使用幾個 JavaScript 函式庫來執行測試,只為了確認它們運作正常(它們都會失敗)。
規格在動作中
在這個教學課程中,我們將使用以下 JavaScript 函式庫進行測試
- Mocha – 核心架構:它提供常見的測試函式,包括
describe
和it
,以及執行測試的主要函式。 - Chai – 具有許多斷言的函式庫。它允許使用許多不同的斷言,目前我們只需要
assert.equal
。 - Sinon – 一個用於監視函式、模擬內建函式等的函式庫,我們稍後會需要它。
這些函式庫適用於瀏覽器內和伺服器端的測試。這裡我們將考慮瀏覽器變體。
包含這些架構和 pow
規格的完整 HTML 頁面
<!DOCTYPE html>
<html>
<head>
<!-- add mocha css, to show results -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
<!-- add mocha framework code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
<script>
mocha.setup('bdd'); // minimal setup
</script>
<!-- add chai -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></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>
,在我們的案例中 – 包含pow
的程式碼。 - 測試 – 在我們的案例中,外部指令碼
test.js
具有上述的describe("pow", ...)
。 - HTML 元素
<div id="mocha">
將由 Mocha 用於輸出結果。 - 測試由指令碼
mocha.run()
啟動。
結果
截至目前,測試失敗,出現錯誤。這是合乎邏輯的:我們在 pow
中有一個空的函式程式碼,因此 pow(2,3)
會傳回 undefined
,而不是 8
。
對於未來,讓我們注意,還有更高級的測試執行器,例如 karma 等,它們可以輕鬆自動執行許多不同的測試。
初始實作
讓我們建立一個簡單的 pow
實作,以通過測試
function pow(x, n) {
return 8; // :) we cheat!
}
哇,現在它運作了!
改善規格
我們所做的絕對是一種作弊。此函式無法運作:嘗試計算 pow(3,4)
會產生不正確的結果,但測試通過。
…但這種情況很典型,在實務中會發生。測試通過,但函式運作錯誤。我們的規格不完美。我們需要為其新增更多使用案例。
讓我們新增一個測試,以檢查 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); }); });
主要差別在於當 assert
觸發錯誤時,it
區塊會立即終止。因此,在第一種變體中,如果第一個 assert
失敗,我們將永遠看不到第二個 assert
的結果。
將測試分開很有用,可以獲得有關正在發生的事情的更多資訊,因此第二種變體較佳。
除此之外,還有一個值得遵循的規則。
一個測試檢查一件事。
如果我們檢視測試並在其中看到兩個獨立的檢查,最好將其拆分為兩個更簡單的檢查。
因此,讓我們繼續使用第二種變體。
結果
正如我們所預期的,第二個測試失敗了。當然,我們的函式總是傳回 8
,而 assert
預期 81
。
改善實作
讓我們寫一些更真實的東西以通過測試
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
為了確保函式運作良好,讓我們針對更多值進行測試。我們可以在 for
中產生 it
區塊,而不是手動撰寫。
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);
}
});
結果
巢狀描述
我們將新增更多測試。但在那之前,讓我們注意輔助函式 makeTest
和 for
應該分組在一起。我們在其他測試中不需要 makeTest
,它只在 for
中需要:它們的共同任務是檢查 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);
}
});
// ... more tests to follow here, both describe and it can be added
});
巢狀 describe
定義一個新的測試「子群組」。在輸出中,我們可以看到標題縮排
未來,我們可以使用輔助函數在頂層新增更多 it
和 describe
,它們不會看到 makeTest
。
before/after
和 beforeEach/afterEach
我們可以設定 before/after
函數,在執行測試之前/之後執行,以及 beforeEach/afterEach
函數,在每個 it
之前/之後執行。
例如
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
用於執行初始化、將計數器歸零,或在測試(或測試群組)之間執行其他操作。
擴充規格
pow
的基本功能已完成。開發的第一個迭代已完成。當我們慶祝並喝香檳時 - 讓我們繼續改進它。
正如所說,函數 pow(x, n)
旨在與正整數值 n
一起使用。
為了指示數學錯誤,JavaScript 函數通常會傳回 NaN
。讓我們對無效的 n
值執行相同的操作。
讓我們先將行為新增到規格中(!)
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));
});
});
新增測試的結果
新增加的測試會失敗,因為我們的實作不支援它們。這就是 BDD 的執行方式:我們先撰寫失敗的測試,然後為它們建立實作。
請注意斷言 assert.isNaN
:它會檢查 NaN
。
在 Chai 中還有其他斷言,例如
assert.equal(value1, value2)
– 檢查相等性value1 == value2
。assert.strictEqual(value1, value2)
– 檢查嚴格相等性value1 === value2
。assert.notEqual
、assert.notStrictEqual
– 與上述相反的檢查。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;
}
現在它可以運作了,所有測試都通過
摘要
在 BDD 中,規格優先,然後才是實作。最後,我們同時擁有規格和程式碼。
規格可以用三種方式使用
- 作為測試 – 它們保證程式碼正確運作。
- 作為文件 –
describe
和it
的標題說明函數的功能。 - 作為範例 – 測試實際上是展示如何使用函數的實際範例。
有了規格,我們可以安全地改進、變更,甚至從頭重寫函數,並確保它仍然正常運作。
這在大型專案中特別重要,因為一個函式會在許多地方使用。當我們變更此類函式時,根本不可能手動檢查使用它的每個地方是否仍能正常運作。
沒有測試,人們有兩種方式
- 無論如何都要執行變更。然後,我們的使用者會遇到錯誤,因為我們可能無法手動檢查某些內容。
- 或者,如果錯誤的懲罰很嚴厲,由於沒有測試,人們會害怕修改此類函式,然後程式碼就會過時,沒人想深入了解它。這對開發沒有好處。
自動測試有助於避免這些問題!
如果專案有測試涵蓋,就不會有這樣的問題。在任何變更之後,我們都可以執行測試,並在幾秒鐘內看到許多檢查結果。
此外,經過良好測試的程式碼具有更好的架構。
當然,這是因為自動測試的程式碼更容易修改和改進。但還有另一個原因。
要撰寫測試,應以這種方式組織程式碼,使每個函式都有明確描述的任務、明確定義的輸入和輸出。這意味著從一開始就有良好的架構。
在現實生活中,這有時並不容易。有時在實際程式碼之前很難撰寫規格,因為它應該如何運作還不清楚。但一般來說,撰寫測試會讓開發更快速、更穩定。
在教學課程的後續部分,您會遇到許多內建測試的任務。因此,您將看到更多實際範例。
撰寫測試需要良好的 JavaScript 知識。但我們才剛開始學習它。因此,為了解決所有問題,目前您不需要撰寫測試,但即使它們比本章中所述的複雜一點,您也應該已經能夠閱讀它們。
留言
<code>
標籤,若要插入多行程式碼,請將它們包覆在<pre>
標籤中,若要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)