自動化測試將在進一步的任務中使用,並且在實際專案中也得到了廣泛的應用。
當我們編寫一個函數時,我們通常可以想像它應該做什麼:哪些參數給出哪些結果。
在開發過程中,我們可以透過運行函數並將結果與預期結果進行比較來檢查函數。例如,我們可以在控制台中執行此操作。
如果出現問題,我們會修復程式碼,再次運行,檢查結果,依此類推,直到它正常工作。
但這種手動「重新運行」並不完美。
透過手動重新運行來測試程式碼時,很容易錯過一些東西。
例如,我們正在建立一個函數f
。寫了一些程式碼,測試: f(1)
有效,但f(2)
不起作用。我們修復了程式碼,現在f(2)
可以工作了。看起來完整嗎?但我們忘記重新測試f(1)
。這可能會導致錯誤。
這非常典型。當我們開發某些東西時,我們會牢記許多可能的用例。但很難指望程式設計師在每次更改後手動檢查所有這些內容。因此,修復一件事並破壞另一件事變得很容易。
自動化測試意味著除了程式碼之外還可以單獨編寫測試。他們以各種方式運行我們的函數,並將結果與預期進行比較。
讓我們從一種名為行為驅動開發(Behavior Driven Development)或簡稱 BDD 的技術開始。
BDD 是三合一:測試、文件和範例。
為了理解 BDD,我們將研究一個實際的開發案例。
假設我們想要建立一個函數pow(x, n)
來將x
提升到整數n
次方。我們假設n≥0
。
這個任務只是一個範例:JavaScript 中的**
運算子可以做到這一點,但在這裡我們專注於也可以應用於更複雜任務的開發流程。
在創建pow
的程式碼之前,我們可以想像該函數應該做什麼並描述它。
這種描述稱為規範,或簡稱為規範,包含用例的描述以及用例的測試,如下所示:
描述(“戰俘”,函數(){ it("求 n 次方", function() { 斷言.equal(pow(2, 3), 8); }); });
規範有三個主要構建塊,您可以在上面看到:
describe("title", function() { ... })
我們正在描述什麼功能?在我們的例子中,我們描述了函數pow
。用於將「工人」分組it
塊。
it("use case description", 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> <頭> <!-- 新增 mocha css,以顯示結果 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"> <!-- 新增mocha框架代碼--> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <腳本> 摩卡.setup('bdd'); // 最小設定 </腳本> <!-- 新增柴 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <腳本> // chai 有很多東西,讓我們把assert設為全域 讓斷言 = chai.assert; </腳本> </頭> <正文> <腳本> 函數 pow(x, n) { /* 待寫入功能程式碼,現在為空 */ } </腳本> <!-- 帶測試的腳本(描述,它...)--> <腳本src =“test.js”></腳本> <!-- id="mocha" 的元素將包含測試結果 --> <div id="摩卡"></div> <!-- 運行測試! --> <腳本> 摩卡.run(); </腳本> </正文> </html>
該頁面可分為五個部分:
<head>
– 新增第三方函式庫和樣式以進行測試。
<script>
帶有要測試的函數,在我們的例子中 - 帶有pow
的程式碼。
測試 - 在我們的例子中,外部腳本test.js
具有上面的describe("pow", ...)
。
Mocha 將使用 HTML 元素<div id="mocha">
來輸出結果。
測試由指令mocha.run()
啟動。
結果:
截至目前,測試失敗,出現錯誤。這是合乎邏輯的:我們在pow
中有一個空函數程式碼,因此pow(2,3)
回傳undefined
而不是8
。
對於未來,我們要注意的是,會有更多進階測試運行程序,例如 karma 等,可以輕鬆自動執行許多不同的測試。
讓我們做一個簡單的pow
實現,以便測試通過:
函數 pow(x, n) { 返回8; // :) 我們作弊! }
哇,現在可以了!
我們所做的事情絕對是作弊。函數不起作用:嘗試計算pow(3,4)
將給出不正確的結果,但測試通過。
……但這種情況很典型,它在實踐中發生過。測試通過,但功能運行錯誤。我們的規格不完善。我們需要為其添加更多用例。
讓我們再增加一個測試來檢查pow(3, 4) = 81
。
我們可以選擇以下兩種方式之一來組織測試:
第一個變體 - 在同一個it
中加入一個assert
:
描述(“戰俘”,函數(){ it("求 n 次方", function() { 斷言.equal(pow(2, 3), 8); 斷言.equal(pow(3, 4), 81); }); });
第二個 – 進行兩次測試:
描述(“戰俘”,函數(){ it("2 的 3 次方為 8", function() { 斷言.equal(pow(2, 3), 8); }); it("3 的 4 次方是 81", function() { 斷言.equal(pow(3, 4), 81); }); });
主要區別在於,當assert
觸發錯誤時, it
塊立即終止。因此,在第一個變體中,如果第一個assert
失敗,那麼我們將永遠不會看到第二個assert
的結果。
單獨進行測試對於獲取有關正在發生的情況的更多資訊很有用,因此第二種變體更好。
除此之外,還有一條值得遵守的規則。
一項測試檢查一件事。
如果我們查看測試並看到其中有兩個獨立的檢查,那麼最好將其分成兩個更簡單的檢查。
那麼讓我們繼續第二個變體。
結果:
正如我們所預料的,第二次測試失敗了。當然,我們的函數總是返回8
,而assert
期望返回81
。
讓我們寫一些更真實的東西來讓測試通過:
函數 pow(x, n) { 讓結果= 1; for (設 i = 0; i < n; i++) { 結果*=x; } 返回結果; }
為了確保函數運作良好,讓我們測試它以獲得更多值。我們可以在for
中生成它們,而不是手動編寫it
:
描述(“戰俘”,函數(){ 函數 makeTest(x) { 讓預期 = x * x * x; it(`${x} 的 3 次方是 ${expected}`, function() { 斷言.equal(pow(x, 3), 預期); }); } for (設 x = 1; x <= 5; x++) { 進行測試(x); } });
結果:
我們將添加更多測試。但在此之前,我們要注意輔助函數makeTest
和for
應該要組合在一起。在其他測試中我們不需要makeTest
,只有在for
中才需要它:它們的共同任務是檢查pow
如何提升到給定的冪。
分組是透過嵌套的describe
完成的:
描述(“戰俘”,函數(){ 描述(“x 的 3 次方”, function() { 函數 makeTest(x) { 讓預期 = x * x * x; it(`${x} 的 3 次方是 ${expected}`, function() { 斷言.equal(pow(x, 3), 預期); }); } for (設 x = 1; x <= 5; x++) { 進行測試(x); } }); // ...更多測試要遵循這裡,既描述又可以添加 });
嵌套describe
定義了一個新的測試“子組”。在輸出中我們可以看到標題縮排:
將來我們可以添加更多it
並使用自己的輔助函數在頂層describe
,他們將看不到makeTest
。
before/after
和beforeEach/afterEach
我們可以設定在執行測試之前/之後執行的before/after
函數,以及在每個it
之前/之後執行的beforeEach/afterEach
函數。
例如:
描述(“測試”,函數(){ before(() =>alert("測試開始 – 在所有測試之前")); after(() =>alert("測試完成-所有測試之後")); beforeEach(() =>alert("測試之前 – 輸入測試")); afterEach(() =>alert("測試後 – 退出測試")); it('測試 1', () => 警報(1)); it('測試 2', () => 警報(2)); });
運行順序將是:
測試開始 – 在所有測試之前(之前) 測試之前 – 輸入測試 (beforeEach) 1 測試後 – 退出測試 (afterEach) 測試之前 – 輸入測試 (beforeEach) 2 測試後 – 退出測試 (afterEach) 測試完成-所有測試之後(之後)
在沙箱中開啟範例。
通常, beforeEach/afterEach
和before/after
用於執行初始化、將計數器清零或在測試(或測試組)之間執行其他操作。
pow
的基本功能已經完成。開發的第一次迭代已經完成。當我們慶祝完畢並喝完香檳後,讓我們繼續改進它。
如所說,函數pow(x, n)
旨在處理正整數值n
。
為了指示數學錯誤,JavaScript 函數通常會傳回NaN
。讓我們對n
的無效值執行相同的操作。
讓我們先將行為加入規範(!):
描述(“戰俘”,函數(){ // ... it("對於負數 n,結果為 NaN", function() { 斷言.isNaN(pow(2, -1)); }); it("對於非整數 n,結果為 NaN", function() { 斷言.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
中添加幾行:
函數 pow(x, n) { 如果 (n < 0) 返回 NaN; if (Math.round(n) != n) 返回 NaN; 讓結果= 1; for (設 i = 0; i < n; i++) { 結果*=x; } 返回結果; }
現在它可以工作了,所有測試都通過了:
在沙箱中打開完整的最終範例。
在 BDD 中,首先是規範,然後是實作。最後我們得到了規範和程式碼。
該規範可以透過三種方式使用:
作為測試——它們保證程式碼正常工作。
作為文檔- describe
的標題, it
告訴了該函數的作用。
作為範例——測試實際上是展示如何使用函數的工作範例。
有了規範,我們可以安全地改進、更改,甚至從頭開始重寫函數,並確保它仍然正常運作。
當一個函數在很多地方使用時,這一點在大型專案中尤其重要。當我們更改這樣的功能時,沒有辦法手動檢查使用它的每個地方是否仍然正常運作。
如果沒有測試,人們有兩種方法:
無論如何都要執行更改。然後我們的用戶會遇到錯誤,因為我們可能無法手動檢查某些內容。
或者,如果對錯誤的懲罰很嚴厲,因為沒有測試,人們就會害怕修改這些功能,然後程式碼就會過時,沒有人願意參與其中。不利於發展。
自動測試有助於避免這些問題!
如果項目經過測試,就不會有這樣的問題。進行任何更改後,我們可以運行測試並在幾秒鐘內看到進行的大量檢查。
此外,經過良好測試的程式碼具有更好的架構。
當然,這是因為自動測試的程式碼更容易修改和改進。但還有另一個原因。
要編寫測試,程式碼的組織方式應使每個函數都有明確描述的任務、明確定義的輸入和輸出。這意味著從一開始就有一個好的架構。
在現實生活中,有時並不那麼容易。有時,在實際程式碼之前編寫規範是很困難的,因為尚不清楚它應該如何表現。但總的來說,編寫測試可以讓開發更快、更穩定。
在本教程的後面部分,您將遇到許多具有內建測試的任務。所以你會看到更多實際的例子。
編寫測試需要良好的 JavaScript 知識。但我們才剛開始學習它。因此,為了解決所有問題,到目前為止,您不需要編寫測試,但您應該已經能夠閱讀它們,即使它們比本章中的稍微複雜一點。
重要性:5
下面的pow
測試有什麼問題嗎?
it("x 的 n 次方", function() { 設 x = 5; 讓結果=x; 斷言.equal(pow(x, 1), 結果); 結果*=x; 斷言.equal(pow(x, 2), 結果); 結果*=x; 斷言.equal(pow(x, 3), 結果); });
PS 從文法上來說,測驗是正確的並且通過了。
該測試展示了開發人員在編寫測試時遇到的誘惑之一。
我們這裡實際上有 3 個測試,但佈局為具有 3 個斷言的單一函數。
有時這樣寫會比較容易,但如果發生錯誤,就不太明顯出了問題。
如果在複雜的執行流程中間發生錯誤,那麼我們就必須找出此時的資料。我們實際上必須調試測試。
最好將測試分成多個具有清晰書面輸入和輸出的it
。
像這樣:
描述(“x 的 n 次方”,function(){ it("5 的 1 次方等於 5", function() { 斷言.equal(pow(5, 1), 5); }); it("5 的 2 次方等於 25", function() { 斷言.equal(pow(5, 2), 25); }); it("5 的 3 次方等於 125", function() { 斷言.equal(pow(5, 3), 125); }); });
我們用describe
和一組it
塊取代了單一it
。現在,如果出現問題,我們將清楚地看到數據是什麼。
我們還可以隔離單一測試並透過編寫it.only
而不是it
來以獨立模式運行它:
描述(“x 的 n 次方”,function(){ it("5 的 1 次方等於 5", function() { 斷言.equal(pow(5, 1), 5); }); // Mocha 將只運行這個區塊 it.only("5的2次方等於25", function() { 斷言.equal(pow(5, 2), 25); }); it("5 的 3 次方等於 125", function() { 斷言.equal(pow(5, 3), 125); }); });