Mocha
(發音"摩卡")誕生於2011年,是現在最流行的JavaScript測試框架之一,在瀏覽器和Node環境都可以使用。
所謂"測試框架",就是執行測試的工具。透過它,可以為JavaScript應用新增測試,從而保證程式碼的質量。
本文全面介紹如何使用Mocha
,讓你輕鬆上手。如果你以前對測試一無所知,本文也可以當作JavaScript單元測試入門。值得說明的是,除了Mocha以外,類似的測試框架還有Jasmine
、Karma
、Tape
等,也很值得學習。
一、安裝
我為本文寫了一個示例庫Mocha-demos
,請先安裝這個庫。
$ git clone https://github.com/ruanyf/mocha-demos.git
如果你的電腦沒裝Git,可以直接下載zip壓縮包,進行解壓。
然後,進入mocha-demos
目錄,安裝依賴(你的電腦必須有Node)。
$ cd mocha-demos $ npm install
上面程式碼會在目錄內部安裝Mocha
,為了操作的方便,請在全面環境也安裝一下Mocha
。
$ npm install --global mocha
二、測試指令碼的寫法
Mocha
的作用是執行測試指令碼,首先必須學會寫測試指令碼。所謂"測試指令碼",就是用來測試原始碼的指令碼。
下面是一個加法模組add.js
的程式碼。
// add.js function add(x, y) { return x + y; } module.exports = add;
要測試這個加法模組是否正確,就要寫測試指令碼。
通常,測試指令碼與所要測試的原始碼指令碼同名,但是字尾名為.test.js
(表示測試)或者.spec.js
(表示規格)。比如,add.js
的測試指令碼名字就是add.test.js
。
// add.test.js var add = require('./add.js'); var expect = require('chai').expect; describe('加法函式的測試', function() { it('1 加 1 應該等於 2', function() { expect(add(1, 1)).to.be.equal(2); }); });
上面這段程式碼,就是測試指令碼,它可以獨立執行。測試指令碼里面應該包括一個或多個describe
塊,每個describe
塊應該包括一個或多個it
塊。
describe
塊稱為"測試套件"(test suite),表示一組相關的測試。它是一個函式,第一個引數是測試套件的名稱("加法函式的測試"),第二個引數是一個實際執行的函式。
it
塊稱為"測試用例"(test case),表示一個單獨的測試,是測試的最小單位。它也是一個函式,第一個引數是測試用例的名稱("1 加 1 應該等於 2"),第二個引數是一個實際執行的函式。
三、斷言庫的用法
上面的測試指令碼里面,有一句斷言。
expect(add(1, 1)).to.be.equal(2);
所謂"斷言",就是判斷原始碼的實際執行結果與預期結果是否一致,如果不一致就丟擲一個錯誤。上面這句斷言的意思是,呼叫add(1, 1)
,結果應該等於2。
所有的測試用例(it塊)都應該含有一句或多句的斷言。它是編寫測試用例的關鍵。斷言功能由斷言庫來實現,Mocha本身不帶斷言庫,所以必須先引入斷言庫。
var expect = require('chai').expect;
斷言庫有很多種,Mocha並不限制使用哪一種。上面程式碼引入的斷言庫是chai
,並且指定使用它的expect
斷言風格。
expect
斷言的優點是很接近自然語言,下面是一些例子。
// 相等或不相等 expect(4 + 5).to.be.equal(9); expect(4 + 5).to.be.not.equal(10); expect(foo).to.be.deep.equal({ bar: 'baz' }); // 布林值為true expect('everthing').to.be.ok; expect(false).to.not.be.ok; // typeof expect('test').to.be.a('string'); expect({ foo: 'bar' }).to.be.an('object'); expect(foo).to.be.an.instanceof(Foo); // include expect([1,2,3]).to.include(2); expect('foobar').to.contain('foo'); expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo'); // empty expect([]).to.be.empty; expect('').to.be.empty; expect({}).to.be.empty; // match expect('foobar').to.match(/^foo/);
基本上,expect
斷言的寫法都是一樣的。頭部是expect
方法,尾部是斷言方法,比如equal
、a
/an
、ok
、match
等。兩者之間使用to
或to.be
連線。
如果expect
斷言不成立,就會丟擲一個錯誤。事實上,只要不丟擲錯誤,測試用例就算透過。
it('1 加 1 應該等於 2', function() {});
上面的這個測試用例,內部沒有任何程式碼,由於沒有丟擲了錯誤,所以還是會透過。
四、Mocha的基本用法
有了測試指令碼以後,就可以用Mocha執行它。請進入demo01
子目錄,執行下面的命令。
$ mocha add.test.js 加法函式的測試 ✓ 1 加 1 應該等於 2 1 passing (8ms)
上面的執行結果表示,測試指令碼透過了測試,一共只有1個測試用例,耗時是8毫秒。
mocha
命令後面緊跟測試指令碼的路徑和檔名,可以指定多個測試指令碼。
$ mocha file1 file2 file3
Mocha預設執行test
子目錄裡面的測試指令碼。所以,一般都會把測試指令碼放在test
目錄裡面,然後執行mocha
就不需要引數了。請進入demo02
子目錄,執行下面的命令。
$ mocha 加法函式的測試 ✓ 1 加 1 應該等於 2 ✓ 任何數加0應該等於自身 2 passing (9ms)
這時可以看到,test
子目錄裡面的測試指令碼執行了。但是,你開啟test
子目錄,會發現下面還有一個test/dir
子目錄,裡面還有一個測試指令碼multiply.test.js
,並沒有得到執行。原來,Mocha預設只執行test
子目錄下面第一層的測試用例,不會執行更下層的用例。
為了改變這種行為,就必須加上--recursive
引數,這時test
子目錄下面所有的測試用例----不管在哪一層----都會執行。
$ mocha --recursive 加法函式的測試 ✓ 1 加 1 應該等於 2 ✓ 任何數加0應該等於自身 乘法函式的測試 ✓ 1 乘 1 應該等於 1 3 passing (9ms)
五、萬用字元
命令列指定測試指令碼時,可以使用萬用字元,同時指定多個檔案。
$ mocha spec/{my,awesome}.js $ mocha test/unit/*.js
上面的第一行命令,指定執行spec
目錄下面的my.js
和awesome.js
。第二行命令,指定執行test/unit
目錄下面的所有js檔案。
除了使用Shell萬用字元,還可以使用Node萬用字元。
$ mocha 'test/**/*[email protected](js|jsx)'
上面程式碼指定執行test
目錄下面任何子目錄中、檔案字尾名為js
或jsx
的測試指令碼。注意,Node的萬用字元要放在單引號之中,否則星號(*
)會先被Shell解釋。
上面這行Node萬用字元,如果改用Shell萬用字元,要寫成下面這樣。
$ mocha test/{,**/}*.{js,jsx}
六、命令列引數
除了前面介紹的--recursive
,Mocha還可以加上其他命令列引數。請在demo02
子目錄裡面,執行下面的命令,檢視效果。
6.1 --help, -h
--help
或-h
引數,用來檢視Mocha的所有命令列引數。
$ mocha --help
6.2 --reporter, -R
--reporter
引數用來指定測試報告的格式,預設是spec
格式。
$ mocha # 等同於 $ mocha --reporter spec
除了spec
格式,官方網站還提供了其他許多報告格式。
$ mocha --reporter tap 1..2 ok 1 加法函式的測試 1 加 1 應該等於 2 ok 2 加法函式的測試 任何數加0應該等於自身 # tests 2 # pass 2 # fail 0
上面是tap
格式報告的顯示結果。
--reporters
引數可以顯示所有內建的報告格式。
$ mocha --reporters
使用mochawesome
模組,可以生成漂亮的HTML格式的報告。
$ npm install --save-dev mochawesome $ ../node_modules/.bin/mocha --reporter mochawesome
上面程式碼中,mocha
命令使用了專案內安裝的版本,而不是全域性安裝的版本,因為mochawesome
模組是安裝在專案內的。
然後,測試結果報告就在mochaawesome-reports
子目錄生成。
6.3 --growl, -G
開啟--growl
引數,就會將測試結果在桌面顯示。
$ mocha --growl
6.4 --watch,-w
--watch
引數用來監視指定的測試指令碼。只要測試指令碼有變化,就會自動執行Mocha。
$ mocha --watch
上面命令執行以後,並不會退出。你可以另外開啟一個終端視窗,修改test
目錄下面的測試指令碼add.test.js
,比如刪除一個測試用例,一旦儲存,Mocha就會再次自動執行。
6.5 --bail, -b
--bail
引數指定只要有一個測試用例沒有透過,就停止執行後面的測試用例。這對持續整合很有用。
$ mocha --bail
6.6 --grep, -g
--grep
引數用於搜尋測試用例的名稱(即it
塊的第一個引數),然後只執行匹配的測試用例。
$ mocha --grep "1 加 1"
上面程式碼只測試名稱中包含"1 加 1"的測試用例。
6.7 --invert, -i
--invert
參數列示只執行不符合條件的測試指令碼,必須與--grep
引數配合使用。
$ mocha --grep "1 加 1" --invert
七,配置檔案mocha.opts
Mocha允許在test
目錄下面,放置配置檔案mocha.opts
,把命令列引數寫在裡面。請先進入demo03
目錄,執行下面的命令。
$ mocha --recursive --reporter tap --growl
上面這個命令有三個引數--recursive
、--reporter tap
、--growl
。
然後,把這三個引數寫入test
目錄下的mocha.opts
檔案。
--reporter tap --recursive --growl
然後,執行mocha
就能取得與第一行命令一樣的效果。
$ mocha
如果測試用例不是存放在test子目錄,可以在mocha.opts
寫入以下內容。
server-tests --recursive
上面程式碼指定執行server-tests
目錄及其子目錄之中的測試指令碼。
八、ES6測試
如果測試指令碼是用ES6寫的,那麼執行測試之前,需要先用Babel轉碼。進入demo04
目錄,開啟test/add.test.js
檔案,可以看到這個測試用例是用ES6寫的。
import add from '../src/add.js'; import chai from 'chai'; let expect = chai.expect; describe('加法函式的測試', function() { it('1 加 1 應該等於 2', function() { expect(add(1, 1)).to.be.equal(2); }); });
ES6轉碼,需要安裝Babel。
$ npm install babel-core babel-preset-es2015 --save-dev
然後,在專案目錄下面,新建一個.babelrc
配置檔案。
{ "presets": [ "es2015" ] }
最後,使用--compilers
引數指定測試指令碼的轉碼器。
$ ../node_modules/mocha/bin/mocha --compilers js:babel-core/register
上面程式碼中,--compilers
引數後面緊跟一個用冒號分隔的字串,冒號左邊是檔案的字尾名,右邊是用來處理這一類檔案的模組名。上面程式碼表示,執行測試之前,先用babel-core/register
模組,處理一下.js
檔案。由於這裡的轉碼器安裝在專案內,所以要使用專案內安裝的Mocha;如果轉碼器安裝在全域性,就可以使用全域性的Mocha。
下面是另外一個例子,使用Mocha測試CoffeeScript指令碼。測試之前,先將.coffee
檔案轉成.js
檔案。
$ mocha --compilers coffee:coffee-script/register
注意,Babel預設不會對Iterator、Generator、Promise、Map、Set等全域性物件,以及一些全域性物件的方法(比如Object.assign
)轉碼。如果你想要對這些物件轉碼,就要安裝babel-polyfill
。
$ npm install babel-polyfill --save
然後,在你的指令碼頭部加上一行。
import 'babel-polyfill'
九、非同步測試
Mocha預設每個測試用例最多執行2000毫秒,如果到時沒有得到結果,就報錯。對於涉及非同步操作的測試用例,這個時間往往是不夠的,需要用-t
或--timeout
引數指定超時門檻。
進入demo05
子目錄,開啟測試指令碼timeout.test.js
。
it('測試應該5000毫秒後結束', function(done) { var x = true; var f = function() { x = false; expect(x).to.be.not.ok; done(); // 通知Mocha測試結束 }; setTimeout(f, 4000); });
上面的測試用例,需要4000毫秒之後,才有執行結果。所以,需要用-t
或--timeout
引數,改變預設的超時設定。
$ mocha -t 5000 timeout.test.js
上面命令將測試的超時時限指定為5000毫秒。
另外,上面的測試用例裡面,有一個done
函式。it
塊執行的時候,傳入一個done
引數,當測試結束的時候,必須顯式呼叫這個函式,告訴Mocha測試結束了。否則,Mocha就無法知道,測試是否結束,會一直等到超時報錯。你可以把這行刪除試試看。
Mocha預設會高亮顯示超過75毫秒的測試用例,可以用-s
或--slow
調整這個引數。
$ mocha -t 5000 -s 1000 timeout.test.js
上面命令指定高亮顯示耗時超過1000毫秒的測試用例。
下面是另外一個非同步測試的例子async.test.js
。
it('非同步請求應該返回一個物件', function(done){ request .get('https://api.github.com') .end(function(err, res){ expect(res).to.be.an('object'); done(); }); });
執行下面命令,可以看到這個測試會透過。
$ mocha -t 10000 async.test.js
另外,Mocha內建對Promise的支援,允許直接返回Promise,等到它的狀態改變,再執行斷言,而不用顯式呼叫done
方法。請看promise.test.js
。
it('非同步請求應該返回一個物件', function() { return fetch('https://api.github.com') .then(function(res) { return res.json(); }).then(function(json) { expect(json).to.be.an('object'); }); });
十、測試用例的鉤子
Mocha在describe
塊之中,提供測試用例的四個鉤子:before()
、after()
、beforeEach()
和afterEach()
。它們會在指定時間執行。
describe('hooks', function() { before(function() { // 在本區塊的所有測試用例之前執行 }); after(function() { // 在本區塊的所有測試用例之後執行 }); beforeEach(function() { // 在本區塊的每個測試用例之前執行 }); afterEach(function() { // 在本區塊的每個測試用例之後執行 }); // test cases });
進入demo06
子目錄,可以看到下面兩個例子。首先是beforeEach
的例子beforeEach.test.js
。
// beforeEach.test.js describe('beforeEach示例', function() { var foo = false; beforeEach(function() { foo = true; }); it('修改全域性變數應該成功', function() { expect(foo).to.be.equal(true); }); });
上面程式碼中,beforeEach
會在it
之前執行,所以會修改全域性變數。
另一個例子beforeEach-async.test.js
則是演示,如何在beforeEach
之中使用非同步操作。
// beforeEach-async.test.js describe('非同步 beforeEach 示例', function() { var foo = false; beforeEach(function(done) { setTimeout(function() { foo = true; done(); }, 50); }); it('全域性變數非同步修改應該成功', function() { expect(foo).to.be.equal(true); }); });
十一、測試用例管理
大型專案有很多測試用例。有時,我們希望只執行其中的幾個,這時可以用only
方法。describe
塊和it
塊都允許呼叫only
方法,表示只執行某個測試套件或測試用例。
進入demo07
子目錄,測試指令碼test/add.test.js
就使用了only
。
it.only('1 加 1 應該等於 2', function() { expect(add(1, 1)).to.be.equal(2); }); it('任何數加0應該等於自身', function() { expect(add(1, 0)).to.be.equal(1); });
上面程式碼中,只有帶有only
方法的測試用例會執行。
$ mocha test/add.test.js 加法函式的測試 ✓ 1 加 1 應該等於 2 1 passing (10ms)
此外,還有skip
方法,表示跳過指定的測試套件或測試用例。
it.skip('任何數加0應該等於自身', function() { expect(add(1, 0)).to.be.equal(1); });
上面程式碼的這個測試用例不會執行。
十二、瀏覽器測試
除了在命令列執行,Mocha還可以在瀏覽器執行。
首先,使用mocha init
命令在指定目錄生成初始化檔案。
$ mocha init demo08
執行上面命令,就會在demo08
目錄下生成index.html
檔案,以及配套的指令碼和樣式表。
<!DOCTYPE html> <html> <body> <h1>Unit.js tests in the browser with Mocha</h1> <div id="mocha"></div> <script src="mocha.js"></script> <script> mocha.setup('bdd'); </script> <script src="tests.js"></script> <script> mocha.run(); </script> </body> </html>
然後,新建一個原始碼檔案add.js
。
// add.js function add(x, y) { return x + y; }
然後,把這個檔案,以及斷言庫chai.js
,加入index.html
。
<script> mocha.setup('bdd'); </script> <script src="add.js"></script> <script src="http://chaijs.com/chai.js"></script> <script src="tests.js"></script> <script> mocha.run(); </script>
最後,在tests.js
裡面寫入測試指令碼。
var expect = chai.expect; describe('加法函式的測試', function() { it('1 加 1 應該等於 2', function() { expect(add(1, 1)).to.be.equal(2); }); it('任何數加0等於自身', function() { expect(add(1, 0)).to.be.equal(1); expect(add(0, 0)).to.be.equal(0); }); });
現在,在瀏覽器裡面開啟index.html
,就可以看到測試指令碼的執行結果。
十三、生成規格檔案
Mocha支援從測試用例生成規格檔案。
進入demo09
子目錄,執行下面的命令。
$ mocha --recursive -R markdown > spec.md
上面命令根據test
目錄的所有測試指令碼,生成一個規格檔案spec.md
。-R markdown
引數指定規格報告是markdown格式。
如果想生成HTML格式的報告spec.html
,使用下面的命令。
$ mocha --recursive -R doc > spec.html
(完)