Karma
是Google
開源的一個基於Node.js
的 JavaScript
測試執行過程管理工具(Test Runner
)。該工具可用於測試全部主流Web
瀏覽器,也可集成到 CI
(Continuous integration
)工具,也可和其餘代碼編輯器一塊兒使用。javascript
咱們測試用的無界面瀏覽器phantomjs
。測試框架使用mocha
和chai
。css
如下是咱們項目中使用的主要配置信息:html
/** * 測試啓動的瀏覽器 * 可用的瀏覽器:https://npmjs.org/browse/keyword/karma-launcher */ browsers: ['PhantomJS'], /** * 測試框架 * 可用的框架:https://npmjs.org/browse/keyword/karma-adapter */ frameworks: ['mocha', 'chai'], /** * 須要加載到瀏覽器的文件列表 */ files: [ '../../src/dcv/plugins/jquery/jquery-1.8.1.min.js', '../../src/dcv/plugins/common/mock.min.js', '../../src/dcv/plugins/common/bluebird.min.js', '../../src/dcv/javascripts/uinv.js', '../../src/dcv/javascripts/uinv_util.js', '../../src/dcv/javascripts/browser/uinv_browser.js', 'specs/validators.js' ], /** * 排除的文件列表 */ exclude: [ ], /** * 在瀏覽器使用以前處理匹配的文件 * 可用的預處理: https://npmjs.org/browse/keyword/karma-preprocessor */ preprocessors: { //報告覆蓋 "../../src/dcv/javascripts/**/*.js": ["coverage"] }, /** * 使用測試結果報告者 * 可能的值: "dots", "progress" * 可用的報告者:https://npmjs.org/browse/keyword/karma-reporter */ reporters: ['spec', 'coverage'], /** * 使用reporters爲"coverage"時報告輸出的類型和那目錄 */ coverageReporter: { type: 'html', dir: 'coverage/' }, /** * 服務端口號 */ port: 9876, /** * 啓用或禁用輸出報告或者日誌中的顏色 */ colors: true, /** * 日誌等級 * 可能的值: * config.LOG_DISABLE //不輸出信息 * config.LOG_ERROR //只輸出錯誤信息 * config.LOG_WARN //只輸出警告信息 * config.LOG_INFO //輸出所有信息 * config.LOG_DEBUG //輸出調試信息 */ logLevel: config.LOG_INFO, /** * 啓用或禁用自動檢測文件變化進行測試 */ autoWatch: true, /** * 開啓或禁用持續集成模式 * 設置爲true, Karma將打開瀏覽器,執行測試並最後退出 */ // singleRun: true, /** * 併發級別(啓動的瀏覽器數) */ concurrency: Infinity
在package.json
中配置以下:前端
"scripts": { "unit": "./node_modules/.bin/karma start test/unit/karma.conf.js --single-run" }
--single-run
意思是單次執行測試,此處會覆蓋上面的singleRun
配置項。最終會在test/unit/coverage
目錄下生成測試覆蓋率的html格式報告。java
mocha
是JavaScript
的一種單元測試框架,既能夠在瀏覽器環境下運行,也能夠在Node.js
環境下運行。node
使用mocha
,咱們就只須要專一於編寫單元測試自己,而後,讓mocha
去自動運行全部的測試,並給出測試結果。jquery
mocha
的特色主要有:git
JavaScript
函數,又能夠測試異步代碼,由於異步是JavaScript
的特性之一;before
、after
、beforeEach
和afterEach
來編寫初始化代碼。describe
表示測試套件,是一序列相關程序的測試;it
表示單元測試(unit test
),也就是測試的最小單位。例:es6
describe("樣例", function () { it("deep用法", function () { expect({a: 1}).to.deep.equal({a: 1}); expect({a: 1}).to.not.equal({a: 1}); expect([{a: 1}]).to.deep.include({a: 1}); // expect([{a: 1}]).to.not.include({a: 1}); expect([{a: 1}]).to.be.include({a: 1}); }); });
mocha
一共四個生命鉤子github
before()
:在該區塊的全部測試用例以前執行
after()
:在該區塊的全部測試用例以後執行
beforeEach()
:在每一個單元測試前執行
afterEach()
:在每一個單元測試後執行
利用describe.skip
能夠跳過測試,而不用註釋大塊代碼;異步只須要在函數中增長done
回調。例:
describe.skip('異步 beforeEach 示例', function () { var foo = false; beforeEach(function (done) { setTimeout(function () { foo = true; done(); }, 50); }); it('全局變量異步修改應該成功', function () { expect(foo).to.be.equal(true); }); it('read book async', function (done) { book.read((err, result) => { expect(err).equal(null); expect(result).to.be.a('string'); done(); }) }); });
chai
是斷言庫,能夠理解爲比較函數,也就是斷言函數是否和預期一致,若是一致則表示測試經過,若是不一致表示測試失敗。
自己mocha
是不包含斷言庫的,因此必須引入第三方斷言庫,目前比較受歡迎的斷言庫有 should.js
、expect.js
、chai
,具體的語法規則須要你們去查閱相關文檔。
由於chai
既包含should
、expect
和assert
三種風格,可擴展性比較強。本質是同樣的,按我的習慣選擇。詳見api
下面簡單的介紹一下這是那種風格
should
例:
let num = 4+5 num.should.equal(9); num.should.not.equal(10); //boolean 'ok'.should.to.be.ok; false.should.to.not.be.ok; //type 'test'.should.to.be.a('string'); ({ foo: 'bar' }).should.to.be.an('object');
expect
例:
// equal or no equal let num = 4+5 expect(num).equal(9); expect(num).not.equal(10); //boolean expect('ok').to.be.ok; expect(false).to.not.be.ok; //type expect('test').to.be.a('string'); expect({ foo: 'bar' }).to.be.an('object');
assert
例:
// equal or no equal let num = 4+5 assert.equal(num,9); //type assert.typeOf('test', 'string', 'test is a string');
e2e
(end to end
)測試是指端到端測試,又叫功能測試,站在用戶視角,使用各類功能、各類交互,是用戶的真實使用場景的仿真。
在產品高速迭代的如今,有個自動化測試,是重構、迭代的重要保障。對web
前端來講,主要的測試就是,表單、動畫、頁面跳轉、dom
渲染、Ajax
等是否按照指望。
e2e
測試正是保證功能的最高層測試,不關注代碼實現細節,專一於代碼可否實現對應的功能。對咱們開發人員而言,測試的主要關注點是映射到頁面的邏輯(通常是存儲的變量)是否正確。
咱們使用nigthwatch
來作e2e
測試
nightwatch
是一個使用selenium
或者webdriver
或者phantomjs
的nodejs
編寫的e2e
自動測試框架,能夠很方便的寫出測試用例來模仿用戶的操做來自動驗證功能的實現。
nightwatch
的使用很簡單,一個nightwatch.json
或者nightwatch.config.js
(後者優先級高)配置文件,使用runner
會自動找同級的這兩個文件來獲取配置信息。也能夠手動使用--config
來制定配置文件的相對路徑。
selenium
是一個強大瀏覽器測試平臺,支持firefox
、chrome
、edge
等瀏覽器的模擬測試,其原理是打開瀏覽器時,把本身的JavaScript
文件嵌入網頁中。而後selenium
的網頁經過frame
嵌入目標網頁。這樣,就可使用selenium
的JavaScript
對象來控制目標網頁。
項目中nightwatch.config.js
的主要配置以下:
{ "src_folders": ["test/e2e/specs"],//測試代碼所在文件夾 "output_folder": "test/e2e/reports",//測試報告所在文件夾 "globals_path": "test/e2e/global.js",//全局變量所在文件夾,能夠經過browser.globals.XX來獲取 "custom_commands_path": ["node_modules/nightwatch-helpers/commands"],//自定義擴展命令 "custom_assertions_path": ["node_modules/nightwatch-helpers/assertions"],//自定義擴展斷言 "selenium": { "start_process": true, "server_path": seleniumServer.path,//selenium的服務所在地址,通常是個jar包 "host": "127.0.0.1", "port": 4444, "cli_args": { "webdriver.chrome.driver": chromedriver.path,//谷歌瀏覽器的drvier地址,在windows下是個exe文件 "webdriver.firefox.profile": "", "webdriver.ie.driver": "", "webdriver.phantomjs.driver": phantomjsDriver.path } }, "test_settings": { "phantomjs": { "desiredCapabilities": { "browserName": "phantomjs", "marionette": true, "acceptSslCerts": true, "phantomjs.binary.path": phantomjsDriver.path, "phantomjs.cli.args": ["--ignore-ssl-errors=false"] } }, "chrome": { "desiredCapabilities": { "browserName": "chrome", "javascriptEnabled": true, "acceptSslCerts": true, 'chromeOptions': { 'args': [ // "start-fullscreen" // '--headless', //開啓無界面 // '--disable-gpu' ] } } }, "firefox": { "desiredCapabilities": { "browserName": "firefox", "javascriptEnabled": true, "acceptSslCerts": true } }, "ie": { "desiredCapabilities": { "browserName": "internet explorer", "javascriptEnabled": true, "acceptSslCerts": true } } } }
在package.json
中配置以下:
"scripts": { "e2e_ci": "node test/e2e/runner.js --env phantomjs", "e2e_parallel": "node test/e2e/runner.js --env phantomjs,chrome" }
以上2個命令都是執行runner.js
文件,前者配置了個環境變量phantomjs
,這樣就會在上面查找test_settings
中的phantomjs
;後者併發執行,同時用phantomjs
和chrome
瀏覽器進行測試。
凡是在上述src_folders
文件夾下的js文件,都會被認爲是測試代碼,會執行測試。要跳過測試,有幾種方式:
@disabled
,這樣整個文件會跳過測試@tags
標籤,多個文件能夠標記同樣的標籤。能夠命令行中添加--tag manager
,這樣,只會測試標籤爲manager
的js
文件,其它都會略過function
轉換爲字符串,好比module.exports = { 'step1': function (browser) { }, 'step2': "" + function (browser) { } }
如下是項目中一個樣例,幾乎涵蓋了各類操做。具體可參看http://nightwatchjs.org/api
var path = require("path"); module.exports = { //'@disabled': true, //不執行這個測試模塊 '@tags': ["manager"],//標籤 'test manager': function (browser) { const batchFile = browser.globals.batchFile; const url = browser.globals.managerURL; browser .url(url) .getCookie("token", function (result) { if (result) { // browser.deleteCookie("token"); } else { this .waitForElementVisible('#loginCode', 50) .setValue('#loginCode', browser.globals.userName) .setValue("#loginPwd", browser.globals.password) .element("css selector", "#mntCode", function (res) { //判斷是否有多租戶 if (res.status != -1) { browser .click("#mntCode", function () { browser .assert.cssProperty("#mntList", "display", "block") //展現多租戶列表 .assert.elementPresent("#mntList li[value=uinnova]"); }) .pause(500) .moveToElement("#mntList li[value=uinnova]", 0, 0, function () { //將鼠標光標移動到優鍩 browser.click("#mntList li[value=uinnova]", function () { browser.assert.containsText("#mntCode", "優鍩科技"); }); }); } }) .click("#fm-login-submit") .pause(50) .url(function (res) { if (res.value !== url) { //這個命令能夠用來截圖 browser.saveScreenshot(browser.globals.imagePath + "login.png"); } }) .assert.urlContains(url, "判斷有沒有跳轉成功,不然便是登錄失敗"); .execute(function (param) { //此處能夠執行頁面中的代碼,且獲得後面傳遞的參數 try { return uinv.data3("token"); } catch (e) { } }, ["param1"], function (res) { //此處能夠獲得上面方法返回值 }); } }) .maximizeWindow() //窗口最大化 .waitForElementVisible("#app", 1000) .pause(1000) .elements("css selector", ".data .clear li", function (res) { var nums = res.value.length - 1; //獲取到manage.html頁面中場景的個數 browser.expect.element('.data_num').text.to.equal('(' + nums + ')'); // 用來統計場景個數的sapn標籤中的值是否等於實際的場景個數 browser.pause(500); }) .click(".clear .last .add_data") .waitForElementPresent("#dcControlFrame") .frame("dcControlFrame", function () { //定位到頁面中的iframe,須要填寫iframe的id(不須要加#) browser .waitForElementPresent("#dataCenterId") .saveScreenshot(browser.globals.imagePath + "dcControlFrame.png") .setValue("#dataCenterId", browser.globals.sceneId) .setValue("#dataCenterName", browser.globals.sceneName) .setValue("#dataCenterText", "歡迎光臨") .setValue("#up_picture[type='file']", path.resolve(batchFile + '/color.png')) //上傳圖片 .click(".group-btn .save", function () { browser .pause(1000) .click(".layui-layer-btn0"); }) .waitForElementVisible("#dataCenterMenu3", 1000) .pause(1500) //上傳場景 .click("#dataCenterMenu3", function () { browser .setValue("#img-3d-max-model input[type='file']", path.resolve(batchFile + '/20121115uinnovaDEMO.zip')) //上傳場景文件 .waitForElementVisible(".layui-layer-btn0", 20000, function () { browser .click(".layui-layer-btn0"); }) .setValue("#img-3d-max-layout input[type='file']", path.resolve(batchFile + '/DEMO20140424-2016-01-14-17-48-17.js')) //上傳佈局文件 .waitForElementVisible(".layui-layer-btn0", 5000, function () { browser .click(".layui-layer-btn0"); }); }) .pause(500) .saveScreenshot(browser.globals.imagePath + "frameParentBefore.png"); }) // .frameParent() //回到iframe的父級頁面;//TODO 無界面下,frame退出有問題,因此暫時改用refresh從新刷新頁面 .refresh() .end(); } };
如下是XX同窗的使用總結
pause
)是必須的,好比在表單操做中須要上傳圖片,須要等文件上傳成功後再點擊保存按鈕pause
就必須傳入一個固定時毫秒值,數值太大浪費時間,數值過小可能未執行完畢,須要反覆測試。若是能夠的話,可使用 waitForElementVisible
類的方法,時間設置的長些也無妨。command
方法的回調函數中的返回值會是一個對象,先把這個對象打印出來看一下格式,再使用這個對象assert
和command
最後都有一個可選參數,自定義測試經過時命令行提示信息PhantomJS
是一個基於webkit
的JavaScript API
。它使用QtWebKit
做爲它核心瀏覽器的功能,使用webkit
來編譯解釋執行JavaScript
代碼。任何你能夠在基於webkit
瀏覽器作的事情,它都能作到。它不只是個隱形的瀏覽器,提供了諸如CSS
選擇器、支持Web
標準、DOM
操做、JSON
、HTML5
、Canvas
、SVG
等,同時也提供了處理文件I/O
的操做,從而使你能夠向操做系統讀寫文件等。PhantomJS
的用處可謂很是普遍,諸如網絡監測、網頁截屏、無需瀏覽器的 Web
測試、頁面訪問自動化等。
由於phantomjs
自己並非一個nodejs
庫,因此咱們使用的實際上是phantomjs-prebuilt
這個包,它會根據當前操做系統判斷從phantomjs
官網下載驅動包。
遺憾的是,PhantomJS
的核心開發者之一 Vitaly Slobodin
近日宣佈,已辭任 maintainer
,再也不維護項目。
Vitaly
發文表示,Chrome 59
將支持 headless
模式,用戶最終會轉向去使用它。Chrome
比PhantomJS
更快,更穩定,也不會像 PhantomJS
這樣瘋狂吃內存:
「我看不到 PhantomJS
的將來,做爲一個單獨的開發者去開發 PhantomJS 2
和 2.5
,簡直就像是一個血腥的地獄。即使是最近發佈的 2.5 Beta
版本擁有全新、亮眼的 QtWebKit
,但我依然沒法作到真正的支持 3 個平臺。咱們沒有獲得其餘力量的支持!」
隨着 Vitaly
的退出,項目僅剩下兩位核心開發者進行維護。
上面也有說到,項目並未獲得資源支持,如此大型的項目,就算兩人正職維護,也很艱難。
Phantom.js
是fully functional headless browser
,可是它和真正的瀏覽器仍是有很大的差異,並不能徹底模擬真實的用戶操做。不少時候,咱們在Phantom.js
發現一些問題,可是調試了半天發現是Phantom.js
本身的問題。2k
的issue
,仍然須要人去修復。Javascript
天生單線程的弱點,須要用異步方式來模擬多線程,隨之而來的callback
地獄,對於新手而言很是痛苦,不過隨着es6
的普遍應用,咱們能夠用promise
來解決多重嵌套回調函數的問題。webdriver
支持htmlunit
與phantomjs
,但因爲沒有任何界面,當咱們須要進行調試或復現問題時,就很是麻煩。Puppeteer
是谷歌官方出品的一個經過DevTools
協議控制headless Chrome
的Node
庫。能夠經過Puppeteer
的提供的api
直接控制Chrome
模擬大部分用戶操做來進行UI Test
或者做爲爬蟲訪問頁面來收集數據。相似於webdriver
的高級別的api
,去幫助咱們經過DevTools
協議控制無界面Chrome
。
在puppteteer
以前,咱們要控制chrome headless
須要使用chrome-remote-interface
來實現,可是它比 Puppeteer API
更接近低層次實現,不管是閱讀仍是編寫都要比puppteteer
更復雜。也沒有具體的dom
操做,尤爲是咱們要模擬一下click
事件,input
事件等,就顯得力不從心了。
咱們用一樣2段代碼來對比一下2個庫的區別。
首先來看看 chrome-remote-interface
const chromeLauncher = require('chrome-launcher'); const CDP = require('chrome-remote-interface'); const fs = require('fs'); function launchChrome(headless=true) { return chromeLauncher.launch({ // port: 9222, // Uncomment to force a specific port of your choice. chromeFlags: [ '--window-size=412,732', '--disable-gpu', headless ? '--headless' : '' ] }); } (async function() { const chrome = await launchChrome(); const protocol = await CDP({port: chrome.port}); const {Page, Runtime} = protocol; await Promise.all([Page.enable(), Runtime.enable()]); Page.navigate({url: 'https://www.github.com/'}); await Page.loadEventFired( console.log("start") ); const {data} = await Page.captureScreenshot(); fs.writeFileSync('example.png', Buffer.from(data, 'base64')); // Wait for window.onload before doing stuff. protocol.close(); chrome.kill(); // Kill Chrome.
再來看看 puppeteer
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://www.github.com'); await page.screenshot({path: 'example.png'}); await browser.close(); })();
就是這麼簡短明瞭,更接近天然語言。沒有callback
,幾行代碼就能搞定咱們所需的一切。
再來段打印阮一峯大神的《ECMAScript 6 入門》
的pdf
文檔的例子:
const puppeteer = require('puppeteer'); const getRootDir = require('root-directory'); (async () => { const rootDir = await getRootDir(); let pdfDir = rootDir + "/public/pdf/es6-pdf/"; const browser = await puppeteer.launch({ headless: false, devtools: true //開發,在headless爲true時頗有用 }); let page = await browser.newPage(); await page.goto('http://es6.ruanyifeng.com/#README'); await page.waitFor(2000); const aTags = await page.evaluate(() => { let as = [...document.querySelectorAll('ol li a')]; return as.map((a) => { return { href: a.href.trim(), name: a.text }; }); }); if (!aTags) { browser.close(); return; } await page.pdf({path: pdfDir + `${aTags[0].name}.pdf`}); page.close(); // 這裏也可使用promise all,但cpu可能吃緊,謹慎操做 for (var i = 1; i < aTags.length; i++) { page = await browser.newPage(); var a = aTags[i]; await page.goto(a.href); await page.waitFor(2000); await page.pdf({path: pdfDir + `${a.name}.pdf`}); console.log(a.name); page.close(); } browser.close(); })();