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(); })();