摘要:持續集成單元測試是開源的一個基于的測試執行過程管理工具。表示測試套件,是一序列相關程序的測試表示單元測試,也就是測試的最小單位。
持續集成 單元測試(unit) karma
Karma 是Google開源的一個基于Node.js 的 JavaScript 測試執行過程管理工具(Test Runner)。該工具可用于測試所有主流Web瀏覽器,也可集成到 CI (Continuous integration)工具,也可和其他代碼編輯器一起使用。
我們測試用的無界面瀏覽器phantomjs。測試框架使用mocha和chai。
以下是我們項目中使用的主要配置信息:
/** * 測試啟動的瀏覽器 * 可用的瀏覽器: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格式報告。
mochamocha是JavaScript的一種單元測試框架,既可以在瀏覽器環境下運行,也可以在Node.js環境下運行。
使用mocha,我們就只需要專注于編寫單元測試本身,然后,讓mocha去自動運行所有的測試,并給出測試結果。
mocha的特點主要有:
既可以測試簡單的JavaScript函數,又可以測試異步代碼,因為異步是JavaScript的特性之一;
可以自動運行所有測試,也可以只運行特定的測試;
可以支持before、after、beforeEach和afterEach來編寫初始化代碼。
describe 表示測試套件,是一序列相關程序的測試;it表示單元測試(unit test),也就是測試的最小單位。例:
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一共四個生命鉤子
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
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)
e2e(end to end)測試是指端到端測試,又叫功能測試,站在用戶視角,使用各種功能、各種交互,是用戶的真實使用場景的仿真。
在產品高速迭代的現在,有個自動化測試,是重構、迭代的重要保障。對web前端來說,主要的測試就是,表單、動畫、頁面跳轉、dom渲染、Ajax等是否按照期望。
e2e測試正是保證功能的最高層測試,不關注代碼實現細節,專注于代碼能否實現對應的功能。對我們開發人員而言,測試的主要關注點是映射到頁面的邏輯(一般是存儲的變量)是否正確。
我們使用nigthwatch來做e2e測試
nightwatchnightwatch是一個使用selenium或者webdriver或者phantomjs的nodejs編寫的e2e自動測試框架,可以很方便的寫出測試用例來模仿用戶的操作來自動驗證功能的實現。
nightwatch的使用很簡單,一個nightwatch.json或者nightwatch.config.js(后者優先級高)配置文件,使用runner會自動找同級的這兩個文件來獲取配置信息。也可以手動使用--config來制定配置文件的相對路徑。
seleniumselenium是一個強大瀏覽器測試平臺,支持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最后都有一個可選參數,自定義測試通過時命令行提示信息
附錄 phantomjsPhantomJS是一個基于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,但由于沒有任何界面,當我們需要進行調試或復現問題時,就非常麻煩。
PuppeteerPuppeteer是谷歌官方出品的一個通過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(); })();
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104874.html
摘要:來這里看看的工程師如何進行持續集成與持續部署。主要介紹了豆瓣移動持續集成和測試相關實踐,用工具化自動化社會化測試來解決遇到的問題,將打包發布環節自動化。這期的持續集成實踐分享就到這里。 我們常看到許多團隊和開發者分享他們的持續集成實踐經驗,本期 fir.im Weekly 收集了 iOS,Android,PHP ,NodeJS 等項目搭建持續集成的實踐,以及一些國內外公司的內部持續集成...
閱讀 2832·2023-04-25 18:58
閱讀 977·2021-11-25 09:43
閱讀 1210·2021-10-25 09:46
閱讀 3494·2021-09-09 11:40
閱讀 1679·2021-08-05 09:59
閱讀 869·2019-08-29 15:07
閱讀 956·2019-08-29 12:48
閱讀 695·2019-08-29 11:19