响应式设计的自动化测试

响应式设计介绍

响应式设计(RWD)从2010年开始就逐渐进入人们的视线,虽然由于网速和网络的制约,目前国内都是采用针对pc和移动分别开发站点的策略,例如淘宝网的首页在pc端网页 ,在移动端的网页为网页,使用的是基于REM的布局设计。但是没人能否认响应式设计的重要性和简便性。响应式设计其实简单来说就是利用media query针对不同的设备和分辨率采用不同的css样式,用以达到网站在各个设备上的兼容性,再结合“移动优先”的策略,使得响应式设计更加的具有优势。如下图所示,就是一个简单的购物网站在不同设备上的响应式设计。

一个响应式设计的实现

下面就举一个简单的响应式网站的例子
代码见f2e-testing

  • 首页welcome页面
  • 登陆页面
  • notes列表页面
  • 新建note页面



公共头尾和菜单的响应式实现

这个属于很经典的响应式菜单和标题设计:
– 首页中如果是mobile或者ipad 则只显示标题的主要部分
– 菜单中如果是大屏,则一行显示菜单,mobile下用两行的菜单
– css完整实现:参见


<div id="header"> <div class="middle-wrapper"> <img id="header-logo" src="../src/assets/header-icon.png"> <h1> Sample Website <span class="not-on-mobile not-on-tablet">for Galen Framework</span></h1> </div> </div>
#menu {
    background: url("images/menu-background.png");
    margin: 0;
    color: white;
}
#menu ul {
    margin: 0;
    padding: 0;
    list-style: none;
}
#menu li {
    display: inline-block;
}
#menu ul:after {
    clear: both;
}
#menu li a {
    min-width: 100px;
    font-size: 1.2em;
    color: white;
    padding: 20px;
    display: inline-block;
}

@media (max-width: 500px) {
    .not-on-mobile {
        display: none;
    }

    #menu {
        width: 100%;
    }
    #menu li {
        width: 49%;
    }
    #menu li a {
        width: 100%;
    }
}
@media (max-width: 800px) {
    .not-on-tablet {
        display: none;
    }
}

welcome页面

welcome页面:使用的是bootstrap的jumbotron的布局,这款响应式布局
主要用在simple marketing or informational website。它具有一个通知的大型“布告栏”(jumbotron)和三栏式布局。
主要的实现代码:

    <div class="middle-wrapper">
        <div id="content">
            <div id="welcome-page" class="jumbotron">
                <h1>Welcome to our test page!</h1>

                <p>This app is used as a playground for <a href="http://galenframework.com/">Galen Framework</a></p>

                <p>
                    <button class="btn btn-lg btn-primary button-login" type="button" onclick="App.showLoginPage();">
                        Login
                    </button>
                </p>
                <p>To log in this website use the email <b>testuser@example.com</b> and password <b>test123</b>
                </p></div>
        </div>
    </div>

.jumbotron { padding: 30px; margin-bottom: 30px; color: inherit; background-color: #eee; } .jumbotron h1, .jumbotron .h1 { color: inherit; } .jumbotron p { margin-bottom: 15px; font-size: 21px; font-weight: 200; } .container .jumbotron { border-radius: 6px; } .jumbotron .container { max-width: 100%; } //大于768px @media screen and (min-width: 768px) { .jumbotron { padding-top: 48px; padding-bottom: 48px; } .container .jumbotron { padding-right: 60px; padding-left: 60px; } .jumbotron h1, .jumbotron .h1 { font-size: 63px; } } @media (max-width: 500px) { button { width: 100%; margin-top: 10px; } }

login登陆页面


@media (min-width: 501px) {
    .dialog-panel {
        width: 400px;
        border: 1px solid #ccc;
        padding: 20px;
        margin: auto;
        border-radius: 10px;
        box-shadow: 1px 3px 3px #ddd;
    }
}

响应式兼容尺寸

  • 合理的使用viewport

<meta name="viewport" content="width=device-width, initial-scale=1"> <!--或者使用如下的 --> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  • 使用hack兼容低版本浏览器的media query

<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> <![endif]-->
  • 参考bootstrap 3 它优先使用“移动优先”原则,详情:参考
    • Extra small devices ~ Phones (< 768px) col-xs-
    • Small devices ~ Tablets (>= 768px) col-sm-
    • Medium devices ~ Desktops (>= 992px) col-md-
    • Large devices ~ Desktops (>= 1200px) col-lg-

/*========== Mobile First Method ==========*/ /* RWD is – Desktop -> Tablet -> Mobile */ /* Mobile First RWD is – Mobile -> Tablet -> Desktop */ /* Custom, iPhone Retina */ @media only screen and (min-width : 320px) { } /* Extra Small Devices, Phones */ @media only screen and (min-width : 480px) { } /* Small Devices, Tablets */ @media only screen and (min-width : 768px) { } /* Medium Devices, Desktops */ @media only screen and (min-width : 992px) { } /* Large Devices, Wide Screens */ @media only screen and (min-width : 1200px) { } /*========== Non-Mobile First Method ==========*/ /* Large Devices, Wide Screens */ @media only screen and (max-width : 1200px) { } /* Medium Devices, Desktops */ @media only screen and (max-width : 992px) { } /* Small Devices, Tablets */ @media only screen and (max-width : 768px) { } /* Extra Small Devices, Phones */ @media only screen and (max-width : 480px) { } /* Custom, iPhone Retina */ @media only screen and (max-width : 320px) { }

响应式设计的自动化测试框架 – galenframework

介绍

  • 用于响应式设计的开源UI自动化测试框架
  • 测试spec “语义化友好”,通过位置信息准确定位各个元素的位置
  • 测试用例API兼容java和javascript
  • pc端和无线端多尺寸兼容,支持selenium appium saucelab browserstack多服务器测试
  • 可自定义输出的测试 html report

安装

  • 下载二进制代码
  • 执行 ./install.sh
  • galen -v 显示如下命令行 表明安装成功
Galen Framework
Version: 2.1.2
JavaScript executor: Rhino 1.7 release 5 2015 01 29

测试环境建立

  • 执行 galen config:生成config文件用于配置初始化文件,具体参数配置 详情参见
  • 文件结构
    • tests文件夹:用于装载测试脚本
      • init.js: 用于配置测试的设备和尺寸
      • pages文件夹: ui自动化测试的Page Object页面
      • login.page.test.js(默认是以.test.js后缀作为测试文件,如果有特殊要求可以在config文件中配置)
    • specs文件夹: 用于装载响应式设计的规则spec文件
      • common.spec文件:
      • loginPage.spec文件等等
    • config文件:配置文件
    • reports目录:用于生成自动化测试的html结果

构建测试服务

  • appium作为mobile的测试服务器,android真机测试的服务搭建,参考
  • selenium作为pc端的测试服务器
#server端:8002端口启动三星galaxy SIII设备的测试服务器;8001端口启动IPAD模拟器;启动chromepc端的测试服务器
node . -a 127.0.0.1 -p 8002 -U 4df752b06833bfd3 --browser-name Chrome --no-reset
node . -a 127.0.0.1 -p 8001 --command-timeout 50000  --no-reset
selenium-standalone start

#客户端:测试 并且测试完成后浏览器打开测试结果

Galen的命令行运行,参考

  • galen check:运行spec
  • galen test: 运行测试用例
  • galen dump:生成可视化spec-
galen test mytest01.test
    --htmlreport "htmlreport-dir"
    --testngreport "report/testng.xml"
    --jsonreport "jsonreport-dir"
    --parallel-tests 4

galen test tests/ --htmlreport reports   

galen check homepage.gspec
    --url "http://example.com"
    --size "640x480"
    --javascript "some.js"
    --include "mobile,all"
    --exclude "toexclude"
    --htmlreport "htmlreport-dir"
    --testngreport "report/testng.xml"
    --jsonreport "jsonreport-dir"

galen dump "specs/homepage.gspec"
    --url "http://galenframework.com"
    --size "1024x768"
    --export "dumps/homepage-dump" 
    --max-width "200" 
    --max-height "200"    

测试流程

createGridDriver建立对服务器的链接,并启动driver


var driver = createGridDriver('http://127.0.0.1:8001/wd/hub',{ desiredCapabilities: { browserName: 'Safari', 'platformVersion': '9.1', 'platformName': 'iOS', 'app': 'safari', deviceName:"iPad Air", size: '600x800', safariInitialUrl: 'about:blank' } }); driver.get("http://test.xxxxx.com");

checkLayout连接spec文件和.test.js测试文件

  • 编写测试脚本
  • 编写spec文件

检查spec文件是否符合预期

//定义test
test("Simplest test", function () {
    // here goes a test code
});

//[] spec中 @on的tag名称
checkLayout(driver, "specs/welcomePage.spec", ['desktop']);

使用 Page Object Model

PageObject在selenium中是常见的设计模式,它可以快速的将测试用例和测试主体相互分开,通过复用,减少代码;同时可以把测试过程变化的参数在统一的地方配置,减少改动的成本。关于 Page Object我会再开文介绍,这里只为大家介绍在galenframework中我们可以如何快捷的定义我们的PageObject,以登陆页为参考:

$page(pageName, primaryFields, [ secondaryFields ])

this.LoginPage = $page("Login page", {
  email: "input.email", // css locator
  password: "xpath: //input[@class='password']", // xpath locator
  submitButton: "id: submit", // id locator

  load: function () {
    this.open("http://example.com/login");
    return this.waitForIt();
  },
  loginAs: function (userName, password) {
    this.email.typeText(userName);
    this.password.typeText(password);
    this.submitButton.click();
  }
});
// now you can use it like this
var loginPage = new LoginPage(driver).load();
loginPage.loginAs("testuser@example.com", "password");

页面的webdriver操作函数

参考 GalenPage.js

  • 针对于$page这个对象
    • open 打开页面
    • waitForIt 等到primaryFields的元素都
    • wait({}).untilAll({}); 等
    • getAllLocators:把
    • findChild 定位元素
    • findChildren 批量定位元素
  • 针对pageElement(primaryFields还有secondaryFields中的元素)
    • attribute 获取属性
    • cssValue 获得css属性值
    • typeText input输入内容
    • click 点击按钮
    • clear 清空input
    • getText 获得输入的内容
    • hover
    • getWebElement
    • findChild
    • findChildren
    • isDisplayed 元素是否展现

操作并输出到report中

  • logged(text, callback)
  • loggedFunction(textExpression, callback)

//${_1} ${_2} 代表arguments loggedFunction("Log-in as ${_1} with password ${_2}", function (email, password) { this.emailTextfield.typeText(email); this.passwordTextfield.typeText(password); this.submitButton.click(); });

spec文件编写

spec文件是用于描述元素css之间的各种关系,符合语义化的要求,详情 参考

  • 定义Objects:@objects
  • tags和sections:= Main section =
  • 变量 @set
  • import其他的spec文件规则:@import header.spec
  • forEach Loop:循环
  • near – checks that object is located near another object
  • below – checks that an element is located below other object
  • above – checks that an element is located above other object
  • left-of and right-of – checks that an element is located above other object
  • inside – checks that object is located inside another object
  • width – checks the width of object
  • height – checks the height of object
  • aligned – checks horizontal or vertical alignment of object with other objects on page
  • text – checks the text that is visible on page
    • text is – checks that text is exactly as expected
    • text contains -checks element contains expected text
    • text starts – element should start with expected text
    • text ends – element should end with expected text
    • text matches – verifies that text matches Java Regular Expression
  • centered – checks that object is centered inside another object
  • absent – checks that object is either missing on page or is not visible
  • contains – checks that object visually contains other objects inside it
  • on – checks that object is visually located on other object
  • component – runs a subset of specs from another file within the given object context
  • color-scheme – checks the color distribution in the given object area


@objects search-panel id search-bar search-panel-input xpath //div[@id='search-bar']/input[@type='text'] search-panel-button css #search-bar a menu-item-* css #menu li a = Main section = @on * menu: height 70px @on mobile login-button: width 100px @on mobile, desktop menu: height 300 px @set commonHeaderMargin 10 to 20px contentMargin ~ 20px # Approximate = Header = header-icon: inside header ${commonHeaderMargin} top left textfield: near button 5 to 15px left # By top edge menu-item-1: aligned horizontally top menu-item-2 # iframe中定义spec @objects banner-frame css iframe#banner = Main section = banner-frame: component frame banner.spec # color scheme login-form: color-scheme 10% white, 4 to 5 % black, < 30% #f845b7 # image menu-item-1: image file imgs/menu-item-1.png, error 4%, tolerance 80 # 循环 = Main section = @forEach [menu-item-*] as itemName, prev as previousItem ${itemName}: right-of ${previousItem} 10px @for [ 1, 2 ] as index menu-item-${index}: above menu-item-${index + 2} 0 to 5px

完整的例子



Check the md5sum for the whole folder

Sometimes we want to check if the specified svn tag code is the same as the trunk code. It is impossible for us to md5 by zip the whole folder, because after the zip, timestamp will involve in the zip file.
We can use find,grep,md5 together to solve this problem.
Here is the script i use to md5sum the whole folder.

find ./ -type f -name *.java |grep -v .svn|xargs md5  |md5

you should be caution that you should not check the parents folder name, or the md5sum will be differernt.
For example, if you hava tag folder name 20150923, and trunk name trunk. You have to use the script seperately for two folers.

cd trunk
find ./ -type f -name *.java |grep -v .svn|xargs md5  |md5
cd 20150923
find ./ -type f -name *.java |grep -v .svn|xargs md5  |md5

and compare each one.

前端自动化测试基础-断言篇:chai和chai插件的用法

chai

概念

测试技术的断言框架。

特点

  • 支持多种BDD/TDD断言语法
    • BDD:should
    • BDD:expect
    • TDD:assert
  • 可用在browser端和node端。
  • 可以和很多测试框架结合例如mocha jasmine等进行单元和UI测试。

安装

    npm install chai

用法

browser端

  <script src="//cdn.bootcss.com/chai/3.4.0/chai.js"></script>
  <script>
    //expect为全局的函数
     expect(foo).to.not.equal('bar');
  </script>

node端

var chai = require('chai'),
    expect = chai.expect;
chai.should();

expect用法


//------------------ 连接词用法 ----------------- //not用法 expect().not.to. expect(foo).to.not.equal('bar'); expect(goodFn).to.not.throw(Error); expect({ foo: 'baz' }).to.have.property('foo').and.not.equal('bar'); //deep用法 expect(foo).to.deep. 通常和equal连用,判断object的相等需要用deep expect(foo).to.deep.equal({ bar: 'baz' }); //any用法 用在keys的判断上 expect({ foo: 1, bar: 2 }).to.have.any.keys('foo', 'baz'); // all用法 用在keys的判断上 expect(foo).to.have.all.keys('bar', 'baz'); expect({ foo: 1, bar: 2 }).to.have.all.keys(['bar', 'foo']); //a 判断typeof 或者 language chain // typeof expect('test').to.be.a('string'); expect({ foo: 'bar' }).to.be.an('object'); expect(null).to.be.a('null'); expect(undefined).to.be.an('undefined'); // language chain expect(foo).to.be.an.instanceof(Foo); //---------------------判断bool---------------------- //bool // 1 truthy expect('everthing').to.be.ok; expect(1).to.be.ok; expect(false).to.not.be.ok; expect(undefined).to.not.be.ok; expect(null).to.not.be.ok; //2 true expect(true).to.be.true; expect(1).to.not.be.true; //3 false expect(false).to.be.false; expect(0).to.not.be.false; //4 null expect(null).to.be.null; expect(undefined).not.to.be.null; // 5 undefined expect(undefined).to.be.undefined; expect(null).to.not.be.undefined; //6 exist var foo = 'hi' , bar = null , baz; expect(foo).to.exist; expect(bar).to.not.exist; expect(baz).to.not.exist; //7 expty expect([]).to.be.empty; expect('').to.be.empty; expect({}).to.be.empty; //------------------------判断函数参数--------------------------- // arguments function test () { expect(arguments).to.be.arguments; } //------------------------判断相等和大小关系-------------------------------- // equal if the deep flag is set, // attention: asserts that the target is deeply equal to value. expect('hello').to.equal('hello'); expect(42).to.equal(42); expect(1).to.not.equal(true); expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' }); expect({ foo: 'bar' }).to.deep.equal({ foo: 'bar' }); // eql: 判断值等 expect({ foo: 'bar' }).to.eql({ foo: 'bar' }); expect([ 1, 2, 3 ]).to.eql([ 1, 2, 3 ]); //.above:大于 expect(10).to.be.above(5); expect('foo').to.have.length.above(2); expect([ 1, 2, 3 ]).to.have.length.above(2); //least 至少 expect('foo').to.have.length.of.at.least(2); expect([ 1, 2, 3 ]).to.have.length.of.at.least(3); //below 低于 expect(5).to.be.below(10); expect('foo').to.have.length.below(4); expect([ 1, 2, 3 ]).to.have.length.below(4); //most 最大为 expect(5).to.be.at.most(5); expect('foo').to.have.length.of.at.most(4); expect([ 1, 2, 3 ]).to.have.length.of.at.most(3); //.within(start, finish)在什么区间内 expect(7).to.be.within(5,10); expect('foo').to.have.length.within(2,4); expect([ 1, 2, 3 ]).to.have.length.within(2,4); //.closeTo(expected, delta) expect(1.5).to.be.closeTo(1, 0.5); //------------------正则--------------- //match(regexp) expect('foobar').to.match(/^foo/); //-----------------字符串------------- //string 判断含有某字符串 expect('foobar').to.have.string('bar'); //----------------throw--------------- var err = new ReferenceError('This is a bad function.'); var fn = function () { throw err; } expect(fn).to.throw(ReferenceError); expect(fn).to.throw(Error); expect(fn).to.throw(/bad function/); expect(fn).to.not.throw('good function'); expect(fn).to.throw(ReferenceError, /bad function/); expect(fn).to.throw(err); expect(fn).to.not.throw(new RangeError('Out of range.')); //------------------------object相关判断------------------------- //deep & property属性 expect(foo).to.deep.equal({ bar: 'baz' }); expect({ foo: { bar: { baz: 'quux' } } }).to.have.deep.property('foo.bar.baz', 'quux'); // typeof expect('test').to.be.a('string'); expect({ foo: 'bar' }).to.be.an('object'); expect(null).to.be.a('null'); expect(undefined).to.be.an('undefined'); // language chain 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'); // members 判断数组成员 expect([1, 2, 3]).to.include.members([3, 2]); expect([1, 2, 3]).to.not.include.members([3, 2, 8]); expect([4, 2]).to.have.members([2, 4]); expect([5, 2]).to.not.have.members([5, 2, 1]); expect([{ id: 1 }]).to.deep.include.members([{ id: 1 }]); //respondTo(method) 判断是否是原型方法 Klass.prototype.bar = function(){}; expect(Klass).to.respondTo('bar'); expect(obj).to.respondTo('bar'); Klass.baz = function(){}; expect(Klass).itself.to.respondTo('baz'); //itself和respondTo结合起来判断是否是原型链的方法还是自身的方法 function Foo() {} Foo.bar = function() {} Foo.prototype.baz = function() {} expect(Foo).itself.to.respondTo('bar'); expect(Foo).itself.not.to.respondTo('baz'); //change 判断函数是否改变了对象的属性值 var obj = { val: 10 }; var fn = function() { obj.val += 3 }; var noChangeFn = function() { return 'foo' + 'bar'; } expect(fn).to.change(obj, 'val'); expect(noChangFn).to.not.change(obj, 'val') //increase(function) 函数是否升高了属性值 var obj = { val: 10 }; var fn = function() { obj.val = 15 }; expect(fn).to.increase(obj, 'val'); //.decrease(function) 函数是否降低了属性值 var obj = { val: 10 }; var fn = function() { obj.val = 5 }; expect(fn).to.decrease(obj, 'val'); //keys.判断是否object含有某项属性 //Note, either any or all should be used in the assertion. If neither are used, the assertion is defaulted to all. expect({ foo: 1, bar: 2 }).to.have.any.keys('foo', 'baz'); expect({ foo: 1, bar: 2 }).to.have.any.keys('foo'); expect({ foo: 1, bar: 2 }).to.contain.any.keys('bar', 'baz'); expect({ foo: 1, bar: 2 }).to.contain.any.keys(['foo']); expect({ foo: 1, bar: 2 }).to.contain.any.keys({'foo': 6}); expect({ foo: 1, bar: 2 }).to.have.all.keys(['bar', 'foo']); expect({ foo: 1, bar: 2 }).to.have.all.keys({'bar': 6, 'foo', 7}); expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys(['bar', 'foo']); expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys([{'bar': 6}}]);

should用法

同chai的差别详情参考

var chai = require('chai');
chai.should();
    //语法: 基本是 expect().to.xx 相当于 ().should.xx ****
    foo.should.be.a('string'); //expect(foo).to.be.a('string');
    foo.should.equal('bar'); //expect(foo).to.equal('bar');
    //省略用法,见expect

注意:should在IE9下有问题

assert

assert为TDD用法,现在一般都是用基于BDD的测试,所以省略,详情请参考 Assert

chai as promise用法 **

  • 将promise和chai结合起来,用于在某种异步的条件下形成的断言判断
  • attention: Chai as Promised is only compatible with modern browsers (IE ≥9, Safari ≥6, no PhantomJS)
  • 具体用法:参见

doSomethingAsync().then( function (result) { result.should.equal("foo"); done(); }, function (err) { done(err); } ); //安装: npm install chai-as-promised //引用chai as promise后可以写作 should.eventually.xxx var chai = require("chai"); var chaiAsPromised = require("chai-as-promised"); chai.use(chaiAsPromised); var should = chai.should(); return doSomethingAsync().should.eventually.equal("foo"); //在ui测试中可以写作 return driver.getAttribute(input, 'type').should.eventually.equal(fieldModel.type); return promise.should.be.fulfilled; return promise.should.eventually.deep.equal("foo"); return promise.should.become("foo"); // same as `.eventually.deep.equal` return promise.should.be.rejected; return promise.should.be.rejectedWith(Error); // other variants of Chai's `throw` assertion work too. // 通过覆盖chaiAsPromised.transferPromiseness方法将assertion赋予then的链式调用功能 // 应用例子 wd.js中 chaiAsPromised.transferPromiseness = wd.transferPromiseness; chaiAsPromised.transferPromiseness = function (assertion, promise) { assertion.then = promise.then.bind(promise); // this is all you get by default assertion.finally = promise.finally.bind(promise); assertion.done = promise.done.bind(promise); };

sinon-chai用法 **

  • sinon-chai 用于对Sinon.JS中的spy, stub, and mocking framework进行断言
  • 具体用法,参见
  • API为:
Sinon.JS property/method Sinon–Chai assertion
called spy.should.have.been.called
callCount spy.should.have.callCount(n)
calledOnce spy.should.have.been.calledOnce
calledTwice spy.should.have.been.calledTwice
calledThrice spy.should.have.been.calledThrice
calledBefore spy1.should.have.been.calledBefore(spy2)
calledAfter spy1.should.have.been.calledAfter(spy2)
calledWithNew spy.should.have.been.calledWithNew
alwaysCalledWithNew spy.should.always.have.been.calledWithNew
calledOn spy.should.have.been.calledOn(context)
alwaysCalledOn spy.should.always.have.been.calledOn(context)
calledWith spy.should.have.been.calledWith(…args)
alwaysCalledWith spy.should.always.have.been.calledWith(…args)
calledWithExactly spy.should.have.been.calledWithExactly(…args)
alwaysCalledWithExactly spy.should.always.have.been.calledWithExactly(…args)
calledWithMatch spy.should.have.been.calledWithMatch(…args)
alwaysCalledWithMatch spy.should.always.have.been.calledWithMatch(…args)
returned spy.should.have.returned(returnVal)
alwaysReturned spy.should.have.always.returned(returnVal)
threw spy.should.have.thrown(errorObjOrErrorTypeStringOrNothing)
alwaysThrew spy.should.have.always.thrown(errorObjOrErrorTypeStringOrNothing)

//安装 npm install sinon-chai //用法 var chai = require("chai"); var sinonChai = require("sinon-chai"); chai.should(); chai.use(sinonChai); function hello(name, cb) { cb("hello " + name); } describe("hello", function () { it("should call callback with correct greeting", function () { var cb = sinon.spy(); hello("foo", cb); cb.should.have.been.calledWith("hello foo"); //if expect expect(cb).to.have.been.calledWith("hello foo"); }); });

chai和mocha结合的测试用例

DataNode RPC to NameNode timeout

在启动Standby NameNode的后经常发现DataNode端报socket异常,具体日志是

java.net.SocketTimeoutException: Call From dn to ns2 failed on socket timeout exception: java.net.SocketTimeoutException: 60000 millis timeout while waiting for channel to be ready for read. ch : java.nio.channels.SocketChannel[connected local=/dn:58211 remote=ns2]; For more details see:  http://wiki.apache.org/hadoop/SocketTimeout
Caused by: java.net.SocketTimeoutException: 60000 millis timeout while waiting for channel to be ready for read. ch : java.nio.channels.SocketChannel[connected local=/dn:58211 remote=ns2]

并且,timeout只发生在DN向Standby发送Heartbeat的时候,而跟Active的通讯都是正常的。
初步怀疑是Standby 拉取Edit导致的full gc问题,但是相应时间standby的gc日志是完全正常的。随后jstack看到所有的时间都花费在updateCountForQuota方法上。updateCountForQuota方法就是从root节点开始递归的更新所有quota。
这步操作在Standby每次拉取editlog之后都会执行,我在测试集群生成1亿个INodeDir以后就可以稳定出现这一情况。
针对这个问题,我发了一个jira,地址是HDFS-9143,随后社区有个duplicate的jira,地址是HDFS-6763
我的解决方案就是每次editlog tail结束的时候不去调用updateCountForQuota方法,transition to active的时候调用一次就好了。类似于6763 V1方案,而V2,V3方案则对ActiveNameNode启动时候updateCountForQuota操作进行了优化。
这个改进将会消除Standby与DN之间经常出现的SocketTimeout同时也会加快Standby启动时间。

Appium的源码编译安装

Appium是现在比较活跃的开源自动化测试平台,因为更新速度很快,建议编译安装,了解其更多有意思的功能。
Appium支持ios android selendroid的自动化测试。在mac下配置ios环境还是相对简单的,但是android真机的配置就不是那么简单了,在此详细记录基于源码的编译安装。

准备工作 node

  • git clone https://github.com/appium/appium.git
  • 安装好node环境(brew安装最好)
  • 安装 mocha 和grunt-cli
npm install -g mocha
npm install -g grunt-cli

android真机配置

因为android虚拟器跑起来非常慢,如果不是专业的android的开发,安装跑andorid studio环境也没有必要
有对应的apk和sdk使用真机就能跑我们的测试脚本了。

准备工作:

  • 安装java jdk 配置JAVA_HOME
  • 安装android jdk,可以在线安装(国内速度超慢),所以快捷的方式是下载adt-bundle,解压后直接可用,下载地址
  • 配置ANDROID_HOME
  • 环境变量的配置代码见下方:
  • 执行环境检测 bin/appium-doctor.js –android 出现如下结果证明android环境配置成功
# ~/.bash_profile的配置内容
# 修改完之后source ~/.bash_profile生效

export ANDROID_HOME=/Users/zhangmeng/Documents/adt-bundle-mac-x86_64-20131030/sdk
export PATH=/Users/zhangmeng/Documents/adt-bundle-mac-x86_64-20131030/sdk/platform-tools:$PATH
export PATH=/Users/zhangmeng/Documents/adt-bundle-mac-x86_64-20131030/sdk/tools:$PATH
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH

配置手机

  • 开启开发者选项,设置-{}开发者选项,如果没有找到,参考
  • 打开USB调试(如下图)
  • 部分手机需要在 连接USB的时候选用 MTP媒体模式才会生效
  • 在命令行执行如下指令,能够列出后(如果不行, 重新插拔一下usb,还可以尝试方法

adb kill-server
adb devices

其中list出来的就是手机的udid,用于后面的测试使用,如下图

执行初始化脚本

按照上面的步骤执行完成之后,运行命令./reset.sh –andorid –verbose即可。
在没有读这个reset.sh脚本的时候真的是被各种的环境搞的头晕脑胀,各种报错,包括:
基本都是有命令运行不通造成的,所以在这里大概介绍一下在appium reset android中的到底做了些什么,帮助大家理解这个启动脚本,以便配合自己的应用解决编译的问题,这个也是源码编译的好处之一,可以及时的解决更新服务。

  • android API 不匹配
  • Device chrome not configured yet
  • uninstall io.appium.android.ime卡住不再运行

reset.sh分析


reset_android() { echo "RESETTING ANDROID" require_java echo "* Configuring Android bootstrap" run_cmd rm -rf build/android_bootstrap run_cmd "$grunt" configAndroidBootstrap echo "* Building Android bootstrap" run_cmd "$grunt" buildAndroidBootstrap reset_unlock_apk reset_unicode_ime reset_settings_apk if $include_dev ; then reset_apidemos reset_toggle_test if $npmlink ; then link_appium_adb fi fi echo "* Setting Android config to Appium's version" run_cmd "$grunt" setConfigVer:android reset_chromedriver }
  • 配置Android bootstrap
    • 删除下build/android_bootstrap目录
    • 执行grunt configAndroidBootstrap:配置UiAutomation需要的编译文件 appium/lib/devices/android/bootstrap/build.xml project.properties local.properties
      • 生成AppiumBootstrap的编译文件:用于运行 android create uitest-project -n AppiumBootstrap -t android-19 -p xx/appium/lib/devices/android/bootstrap/
  • 编译 Android bootstrap
    • grunt buildAndroidBootstrap:使用ant编译AppiumBootstrap.jar,放置到appium/build/android_bootstrap/下
  • 编译apk文件(build目录下)
    • 编译 unlock apk: 唤醒和解锁andorid手机或是虚拟器详情
    • 编译 unicode ime apk: android对ASCII码的支持不好,所以会安装这个utf7的输入法,将sendKeys中的输入转为unicode识别的编码,详情
    • 编译 appium-settings apk:用于控制android系统 详情
  • 如果开启了测试模式 –dev参数
    • 编译sample-code下的app:ToggleTest apiDemos
  • 更新 appium-adb模块:运行./bin/npmlink.sh -l appium-adb
  • 更新appium的版本号
  • reset_chromedriver 详情参考

运行测试用例

  • node . -U 4df752b06833bfd3 (显示下面的提示证明Appium Server能够正常启动)
  • 详细的运行参数参考
  • 运行测试用例 : mocha wd-android-helloworld.js (wd.js
  • 其中支持原生的browser、chrome、还有apk的测试

var wd = require("wd");
var driver = wd.promiseChainRemote({
    host: 'localhost',
    port: 4723
});

driver
    .init({
        browserName: 'Chrome',//Chrome or Browser(原生,默认主页是google建议最好翻墙不然卡住)
        platformName: 'Android',
        platformVersion: '4.4.4',
        deviceName: 'Android Emulator'
      //,app: '/Users/zhangmeng/Downloads/com.taobao.taobao-5.3.1-121.apk' //如果选择测试app的内容 browserName设置为'';
      //执行app后会把对应的apk安装到真机中
    })
    .get('http://www.baidu.com')
    .sleep(5000)
    .title().then(function (title){
        console.log('this is the website title', title)
    })
    .quit()
    .done();

ios 虚拟器配置

配置和启动服务

$ git clone https://github.com/appium/appium.git
$ cd appium
$ ./reset.sh --ios --verbose
$ sudo ./bin/authorize-ios.js # for ios only 修改权限
$ node .

测试脚本

参见 safari-wd-search-test.js

参考

  • https://github.com/appium/appium/blob/master/docs/en/contributing-to-appium/appium-from-source.md
  • https://github.com/appium/appium/blob/master/docs/en/contributing-to-appium/grunt.md
  • http://university.utest.com/android-ui-testing-uiautomatorviewer-and-uiautomator/
  • http://developer.android.com/tools/help/shell.html

前端自动化测试基础-mocha篇

安装

    npm install -g mocha

命令行用法

常用的命令行为:

   mocha -u bdd -R spec -t 5000 --recursive
  • -u:测试方式 bdd|tdd|exports
  • -R:选择报表的展现方式,报表展现方式,默认为spec,附加的 例如mocha-lcov-reporter(需要自己安装)
  • -t:超时时间设置,当测试中有异步的时候如果超过设定时间会退出测试,默认2s
  • –recursive:默认会把test文件夹和子文件夹中的所有的测试文件执行一遍

详解参考官网Usage

describe it hook

初次接触mocha的人,常常会觉得这几个概念很抽象,用简单的语言概括来说:

  • describe:用于将测试分类,可以嵌套,范围从大到小
  • it:真正包裹测试断言的作用域

  • hook:before beforeEach after afterEach 为测试做辅助的作用域,例如 before中可以执行数据库的初始化,或者检测活动;after中用于清除使用的变量等。

mocha和BDD测试

mocha支持bdd和tdd的测试,支持should/expect的断言方式,常和chai结合在一起使用


//npm install chai之后 var chai = require('chai'); var expect = chai.expect; var Person = function (name) { this.name = name; }; var zhangmeng = new Person('zhangmeng'); describe('zhangmeng attribute', function () { it ('zhangmeng should be a person ', function () { expect(zhangmeng).to.be.an.instanceof(Person); }) });

异步的处理

在javascript的世界 测试异步程序是特别常见的,例如文件的读写、数据库的访问等等,mocha对异步的支持也特别好,你只需要在最里面的函数中增加对应的回调即可,此外mocha是支持promise的

/**
 * @fileOverView mocha-async-demo
 * @author zhangmeng on 15/10/12
 */

//使用异步callback的方式

var fs = require('fs');
var fileName = '/opt/local/share/nginx/html/my-git/f2e-testing/basic/files/name.json';

var chai = require('chai');
var expect = chai.expect;
var Q = require('q');


//使用回调的方式测试

describe('file content validation through callback', function () {
    //读取文件内容
    var fileObj = {};
    before(function (done) {
        //async
        fs.readFile(fileName, 'utf-8', function (err, data) {
            if (err) {
                throw err;
            }
            fileObj = JSON.parse(data);
            done();
        });
    });

    it ('expect name to be zhangmeng', function () {
        var name = fileObj.name;
        expect(name).to.equal("zhangmeng");
    });

    it ('expect name to be zhangmeng', function () {
        var age = fileObj.age;
        expect(age).to.equal('29');
    });
});



//使用promise的方式例子
describe('file content validation through promise', function () {
    var fileObj = {};
    var readFilePromise = function(path, encoding) {
        var encoding = encoding || 'utf-8';
        var deferred = Q.defer();
        fs.readFile(path, encoding, function(err, text) {
            if(err) {
                deferred.reject(new Error(err));
            } else {
                deferred.resolve(text);
            }
        });
        return deferred.promise;
    };

    before('read name.json', function () {
        //return 支持promise的异步
        return readFilePromise(fileName).then(function(data) {
            try {
                fileObj = JSON.parse(data);
            } catch(err) {
                console.log(err);
            }
        })
    });

    it ('name should be zhangmeng', function () {
       var name = fileObj.name;
       expect(name).to.equal('zhangmeng');
    });

    it ('age should be 29', function () {
        var age = fileObj.age;
        expect(age).to.equal('29');
    });
});

执行顺序

关于it和hook之间的顺序,有时非常容易混淆,先上结论:

  • beforeEach会对当前describe下的所有子case生效。
  • before和after的代码没有特殊顺序要求。
  • 同一个describe下可以有多个before,执行顺序与代码顺序相同。
  • 同一个describe下的执行顺序为before, beforeEach, afterEach, after(*),见下例。
  • 当一个it有多个before的时候,执行顺序从最外围的describe的before开始,其余同理。
  • 当没有it的时候,before还有beforeEach的内容都不会执行(*)
  • it的内容是按照顺序执行的 即使前面的it的内容完成的时间偏后,也会按照顺序执行(*)

describe('earth', function(){ beforeEach(function(){ console.log('see.. this function is run EACH time, before each describe()') }) describe('singapre', function(){ before(function () { console.log('it will happen before beforeEach and only once') }) it('birds should fly', function(){ /** ... */ }) it('horse should gallop', function(){ /** ... */ }) }) describe('malaysia', function(){ it('birds should soar', function(){ /** ... */ }) }) }) //执行结果 //earth //singapre //it will happen before beforeEach and only once //see.. this function is run EACH time, before each describe() //✓ birds should fly //see.. this function is run EACH time, before each describe() //✓ horse should gallop //malaysia //see.. this function is run EACH time, before each describe() //✓ birds should soar

源码

前端UI自动化测试

测试手段

UI测试目前主要有方式:

  • record-and-replay: 主要是指利用录制工具去记录用户的行为,并且把这种“行为“存储到脚本中,以便将来用于检测程序或应用是否能够产出预期的效果。常用的record-and-replay工具有:微软的RPF以及google早期出品的abite
  • e2e测试(end-to-end testing):这种测试方式不光可以测试UI层,还可以将整个系统的功能进行测试。通常这种测试会使用第三方的测试工具作为测试doubles层以提升测试效率。

测试内容

没人可以否认UI测试是耗时且昂贵的,所以在写测试的时候一定要慎重的选择使用UI测试的case,下图就是一种比较“聪明”的UI测试架构。我们可以将UI层进行拆分:视图层还有UI逻辑层。如果大家知道 MVX 这种架构,就会知道,UI逻辑层更像是 MVX 中的Controller层和Model层,视图层是比较难以测试和描述的,因此不建议将对视图层的内容作为UI测试的重点,当然我们也可以使用简单的spec来描述视图层的内容,或是对于视图的样式等使用 galenframework类似的框架进行测试 (后面的blog会专门介绍这个框架,它脱离了phantomCss的检测方式,使用特殊的spec方式来描述case,对于前端来说,非常值得学习)。

因此我们更多的测试会围绕UI逻辑层进行。UI逻辑层主要的用途如下,因此我们的case就围绕着对这两部分功能的测试进行编写。

  • 用户和浏览器的交互(操作和更新html)
  • 监听html的事件并且将信息通过request传递给后台

测试框架

UI测试框架主要由两部分构成:客户端的Test环境和测试服务,测试框架的基本原理很简单,本着经济有效的原则,设计了这款使用开源技术的UI测试框架,跨平台、支持多语言、且支持PC端和mobile端的测试方案,本人是前端,所以下例都是基于Nodejs/javascript书写。

UI测试服务端的构建

对于UI测试的服务端平台来说,非常欣赏BrowserStack这个测试平台。实时的、Web-based、多语言,多浏览器、多机型支持,API和接口全面丰富的基于云端的测试平台,除了价格比较贵($39/month),绝对是最完的测试利器。
对于UI测试来说,浏览器宿主环境是非常重要的,而服务端的Hub架构就是通过代理服务器的方式帮你操纵各种类型的浏览器进行自动化测试。在此我们选择了selenium-standalone来实现pc端的server(内置Jetty服务器);appium这个node服务器作为mobile端的server hub。

Selenium-standalone

selenium-standalone支持node安装方式,通过下列脚本可以安装执行,同时可以配置对应的hub信息。

    npm install selenium-standalone@latest -g
    selenium-standalone start -- -role node -hub http://localhost:4444/grid/register -port 5556
  • selenium默认支持的浏览器为Firefox和phantom,如果要使用它操纵其他的浏览器参考如下方式安装对应驱动:
  • chrome:selenium-standalone install –drivers.chrome.version=2.15 –drivers.chrome.baseURL=http://chromedriver.storage.googleapis.com
  • safari:下载,并在safari中安装SafariDriver.safariextz插件
  • ie:selenium-standalone install –drivers.chrome.version=2.15 –drivers.chrome.baseURL=http://chromedriver.storage.googleapis.com

Appium

####简介
mobile端的开发越来越火热,为了保证开发质量,也有很多针对移动端的测试工具应运而生。Appium就是其中很活跃的开源框架。本质上它包括两部分内容:

  • 基于express的server用于发送/接收client端的协议命令
  • 作为bootstrap客户端用于将命令传递给对应的UIAutomator/UIAutomation/Google’s Instrumentation

Appium最大的特色就是支持ios/android/firefoxos多种平台的测试,native、h5、hybrid都支持,以及所有支持jsonWireProtocal协议的脚本语言:python,java,nodejs ruby都可以用来书写用例

####安装

因为Appium的社区发展的很快,建议使用源码编译使用,而不是使用AppiumGUI(它本身是由第三方社区维护,并不属于appium的核心产品 所以很多bug更新的并不及时,例如测试h5页面的时候页面会出现),此外还可以根据自己的要求修改源码和调试,下面就简要介绍一下源码安装的方法, 安装详细方法 请见 Running Appium from Source

  • 配置IOS环境
    • xcode安装好
  • 配置Andorid环境
    • java jdk 配置好并设置好JAVA_HOME
    • android sdk安装并配置好ANDROID_HOME
    • 建议在真机下进行测试(模拟器启动速度慢),参见executing_test_on_real_devices
  • 运行下方代码
  • 以IOS为例:编译安装并启动的结果如下:
     git clone https://github.com/appium/appium.git
     cd appium
     ./reset.sh --verbose #感谢g*f*w 安装过程痛苦而漫长,使用--verbose显示日志吧,至少知道在哪里卡住
     sudo ./bin/authorize-ios.js # for ios only modify /etc/authorization
     node .

如果需要详细的server启动配置,请参考Appium server arguments,例如 只想实现针对safari进行h5页面的自动化测试,配置参数为:

    node . --safari

UI测试客户端框架

前面提到了jsonWireProtcal协议,主要用于客户端的Testcase中定义对浏览器的操作,实现了这个协议的框架和语言有很多,这个大家自行选择。协议形如

GET /session/:sessionId/screenshot
Take a screenshot of the current page.

个人比较欣赏wd.js这个框架,它是一个webdriver/selenium 2的node端实现,各种异步promise支持,自定义方法非常方便,同时支持mocha和chai的无缝嵌入。

简单用法

var wd = require("wd");
var driver = wd.promiseChainRemote({
    protocol: 'http:',
    hostname: '127.0.0.1',
    port: '4444',
    path: '/wd/hub'
});

driver
    .init({browserName: 'safari'})
    .get('http://www.baidu.com')
    .sleep(5000)
    .title().then(function (title){
        console.log('this is the website title', title)
    })
    .quit();

chain和promise的写法

将异步转化为Q chain的链式调用方式,内置Q
支持自定义的promise,代码如下所示,详细代码见github

/**
 * @fileOverView wd-promise wd 链式调用实例
 * @author zhangmeng on 15/10/4
 */

var wd = require("wd");
//内置Q chain
var Q = wd.Q;
var browser = wd.promiseChainRemote({
    protocol: 'http:',
    hostname: '127.0.0.1',
    port: '4444',
    path: '/wd/hub'
});

/**
 * 自定义链式调用用于实现drag 和 drop的操作
 * @param fromElm cssSelector
 * @param toElm cssSelector
 * @returns {Function} browser
 */
var dragNdrop = function (fromElm, toElm) {
    return function () {
        return Q.all([
            browser.elementByCssSelector(fromElm),
            browser.elementByCssSelector(toElm)
        ]).then(function (els) {
            console.log(els);
            return browser
                    .moveTo(els[0])
                    .buttonDown()
                    .moveTo(els[1])
                    .buttonUp();
        });
    }
};

browser
    .init({browserName:'chrome'})
    .get('http://localhost:63342/my-git/f2e-testing/ui-wd-tests/test-html/test-dragNdrop.html')
    //chain link
    .then(dragNdrop('.dragable','.dropable'))
    .sleep(1000)
    .fin(function() { return browser.quit(); })
    .done();

Asserter用法和自定义Asseter

wd.js内置了基本的Asserter,同时支持自定义的断言。多数结合waitfor“句式“使用。这个在实际中经常应用,例如当页面中某个元素出现特定状态的时候去做某事,或者是判断某异步的加载完成的时候执行某操作等。

内置的判断包括
– nonEmptyText
– isDisplayed
– isNotDisplayed
– textInclude
– jsCondition
– isVisible
– isHidden
– jsCondition(常用)

waitfor包括:

  • waitFor
  • waitForElementByCss(elem, asserter, timeout, pollFreq, callback)(常用,判定当某元素存在,且满足某asserter的时候调用回调)
  • waitForConditionInBrowser(jsExpression) 需要设置异步超时时间,setAsyncScriptTimeout

如果上述都不满足还可以自定义Asserter,下面是对应的例子,使用多种方法判断ajax加载完成后进行测试内容,详情见wd-asserter.js

//自定义方法
var tableHasBeenLoaded = new Asserter(
    function(browser, cb) {
        var jsConditionExpr = '($("#tbody tr").length > 0) ? true: false';
        var _eval = browser.eval;
        _eval.apply( browser , [jsConditionExpr, function(err, res) {
            if(err) {return cb(err);}
            cb(null, res, res);
        }]);
    }
);
browser
    .init({browserName: 'chrome'})
    .setAsyncScriptTimeout(30000)
    .get('http://localhost:63342/my-git/f2e-testing/ui-wd-tests/test-html/test-assert.html')
    //------------- case2 jsCondition  waitForConditionInBrowser new Asserter waitForAjaxLoaded -----
    .elementByCss('#getBtn')
    .click() //click to trigger ajaxloading
    //.waitFor(tableHasBeenLoaded, 4000)
    .execute('alert("ajax finished")')
    .sleep(2000)
    .fin(function () {
        return browser.quit();
    })
    .done();

自定义操作方法

使用wd.PromiseChainWebdriver.prototype可以将自定义的方法chain到链式调用中去,同时还可以使用promise来实现,例如上面dragNdrop的例子

//method1 of self-defined method
wd.PromiseChainWebdriver.prototype.waitForAjaxLoaded = function (timeout) {
    //this为browser内容
    return this.waitFor(tableHasBeenLoaded, timeout)
}
//method2

function selfDefinedFunction() {
    return browser.xxxxx
}

browser.init().get().selfDefinedFunction().xx

插入js代码

在测试的实际应用中,经常需要引入需要的类库或者辅助代码来实现测试的目的,那么应该怎么操作呢,wd.js按照jsonWireProtocal是支持执行js代码的,一般通过下面两个方法。最常见的是要测的代码中是没有对应的类库的 如果要使用,例如jquery kissy,那么需要预先inject对应的代码,类似js bookmark书签,或者chrome的插件中的content_script代码。具体代码参见wd-jsinject.js
– execute():执行同步代码
– executeAsync():执行的内容中含有异步的内容

//load.js 用于load javascript类库
var loadScript = function (scriptUrl, callback) {
    var script = document.createElement('script');
    var head = document.getElementsByTagName('head')[0];
    var done = false;
    script.onload = script.onreadystatechange = (function() {
        if (!done && (!this.readyState || this.readyState == 'loaded'
            || this.readyState == 'complete')) {
            done = true;
            script.onload = script.onreadystatechange = null;
            head.removeChild(script);
            callback();
        }
    });
    script.src = scriptUrl;
    head.appendChild(script);
};
loadScript = loadScript(arguments[0], arguments[arguments.length - 1]);
//loadScript('//cdn.bootcss.com/jquery/2.1.4/jquery.js');

//dom.js 判断类库是否正确引入,设置
Fn = {};
var appendChild = setTimeout(function() {
    $("#i_am_an_id").append('<div class="child">I am the child</div>')
}, arguments[0]);

var removeChildren = function () {
    $("#i_am_an_id").empty();
};

Fn = {
    appendChild: appendChild,
    removeChildren: removeChildren
};

//定义object方便链式操作中调用
window.Fn = Fn;

//wd-jsInject.js

var jsFileToString = function (filePath) {
    var file = fs.readFileSync(filePath, "utf8");
    return file;
};
//读取本地的代码
var codeUrl = '/opt/local/share/nginx/html/my-git/f2e-testing/ui-wd-tests/scripts/dom.js';
//加载jquery等类库
var loadUrl = '/opt/local/share/nginx/html/my-git/f2e-testing/ui-wd-tests/scripts/load.js';

//读取js代码(自动转化为jsExpression)
var executeStr = jsFileToString(codeUrl);
var loadScriptStr = jsFileToString(loadUrl);

browser
      .init({browserName:'chrome'})
      .get('http://localhost:63342/my-git/f2e-testing/ui-wd-tests/test-html/test-injectjs.html')
      //inject jquery
      .setAsyncScriptTimeout(30000)
      .executeAsync(loadScriptStr, ["//cdn.bootcss.com/jquery/2.1.4/jquery.js"])
      .execute(executeStr)
      //测试jquery是否正常引入
      .execute('Fn.appendChild', [1000])
      .execute('Fn.removeChildren()')
      .sleep(2000)
      .fin(function() { return browser.quit(); })
      .done();

结合mocha和chai

mocha是用于测试的框架,chai用于辅助断言,wd.js支持两者的无缝接入,可以使ui测试变得像单元测试一样简单。参考下面的demo,就是把三者结合在一起,通过wd对appium访问ios虚拟机,对手机淘宝搜索结果页进行UI测试的例子,代码详见Github F2E-testing UI test

require('../helpers/setup');
var wd = require("wd");
var serverConfig = require('../helpers/server').appium;
var desired = require('../helpers/caps').ios90s;
var begin_page_url = 'http://s.m.taobao.com/h5?search-btn=&event_submit_do_new_search_auction=1&_input_charset=utf-8&topSearch=1&atype=b&searchfrom=1&action=home%3Aredirect_app_action&from=1';

describe('test page of taobao search', function () {
    this.timeout(300000);
    var driver;
    before(function () {
        driver = wd.promiseChainRemote(serverConfig);
        require("../helpers/logger").configure(driver);//显示日志
        return driver.init(desired);
    });

    after(function () {
        return driver.quit();
    });

    //1打开淘宝搜索页面
    //2点击搜索框
    //3进入到搜索结果页面
    it("should open iphone+6s search page", function () {
        var inputValue = 'iphone 6s';
        return driver
              .get(begin_page_url)
              .sleep(1000)
              .waitForElementByName('q', 2000)
              .sendKeys(inputValue)
              .waitForElementByName('search')
              .tap()
              .sleep(5000)
              .eval('window.location.href')
              .should.eventually.include('q=iphone+6s')
    });


});

browserify和webpack快速配置入门教程

随着前端的工程越来越复杂,快速的模块化构建、部署前端app也就变得更加的重要,最近比较火爆的工具有browserify和webpack。真的是非常好用,本文的目的就是教会大家怎么使用这两个工具,因为强大所以配置也非常复杂,但是我们常用的核心功能非常简单,下面我们就从实战的角度,告诉大家怎么能用其快速的构建应用,本文中的打包代码基本是 即拷贝即用。

browserify

简介

browserify 简单概括来说就是:按照依赖(require)打包你的js文件。并让它(node端代码)跑在浏览器环境下。
浏览器兼容程度如下:

快速使用方法


npm install -g browserify browserify main.js -o bundle.js

假设main.js是你的node模块代码,且main.js依赖了 basicA.js basicB.js。你可以通过上述命令快速的产出bundle.js文件,在浏览器端使用,在bundle.js中会实现如下功能,所以最终代码就直接引用bundle.js即可。

  • 增加对node的require和exports的支持,使得main.js的内容能够在浏览器端执行
  • 分析出main.js的依赖模块basicA.js basicB.js并将其打包在bundle.js中
  • 上述只是最简单的使用方法,详情请参考 browserify-handbook

gruntfile版本react工程最简配置(支持文件修改自动部署)

我们知道react是支持使用node模块和页面内嵌jsx,但是一般来说,react应用还是需要打包的步骤,将jsx的语法解析成对应的js执行。browsify支持react项目的打包,只需要引入对应的reactify 插件即可。最简单的配置如下:

//package.json
{
  "name": "react-app",
  "version": "0.0.1",
  "dependencies": {
    "grunt": "^0.4.5",
    "grunt-browserify": "^3.3.0",
    "grunt-contrib-watch": "^0.6.1",
    "reactify": "^1.0.0"
  }
}
//gruntfile文件配置
//其中所有的jsx组件放到src下,而最终的入口文件为app.js
//开发的时候执行grunt watch就可以监控src中所有jsx模板将其翻译成对应的js模块,在最终的html中引入bundle.js即可
module.exports = function(grunt) {
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    watch: {
      browserify: {
        files: ['src/**/*.js*', 'app.js'],
        tasks: ['browserify']
      }
    },
    browserify: {
      example : {
        src: ['app.js'],
        dest: 'bundle.js',
        options: {
          transform: ['reactify']
        }
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-browserify');
  grunt.registerTask('bundle-example', ['browserify:example']);
};  

webpack

简介

webpack 也是一个强大的模块管理工具,它将所有的资源都算作一个模块,如下图。

和前面提到的browsify相比,browsify只是支持js的打包,webpack更加的智能,主要体现:
– 支持CommonJs和AMD模块。
– 支持模块加载器和插件机制,可对模块灵活定制,比如babel-loader加载器,有效支持ES6。
– 可以通过配置,打包成多个文件。有效利用浏览器的缓存功能提升性能。
– 将样式文件和图片等静态资源也可视为模块进行打包。配合loader加载器,可以支持sass,less等CSS预处理器。
– 内置有source map,即使打包在一起依旧方便调试。

快速使用方法

npm install -g webpack
##支持的命令行参数有:-d:支持调试;-w支持实时的编辑打包;-p支持压缩
webpack -d
webpack -w
webpack -p

webpack的默认文件名为:webpack.config.js,下面就介绍一个简单的工程所使用的webpack配置。

//单入口文件的打包
var path = require("path");
module.exports = {
    entry: './src/search.js', //单入口文件
    output: {
        path: path.join(__dirname, 'out'),  //打包输出的路径
        filename: 'bundle.js',              //打包后的名字
        publicPath: "./out/"                //html引用路径,在这里是本地地址。
    }
};
//多入口文件
module.exports = {
    entry: {
        bundle_page1: "./src/search.js",
        bundle_page2: "./src/login.js"
    },
    output: {
        path: path.join(__dirname, 'out'),
        publicPath: "./out/",
        filename: '[name].js'//最后产出的文件为 out/bundle_page1.js out/bundle_page2.js
    }
};

webpack加载器和插件

webpack最大的特色就是支持很多的loader,这些loader为复杂的应用构建提供了便利的部署环境,而不仅仅局限于node文件的浏览器环境打包而已。常用的加载器有哪些呢,这里会介绍这几个的用法。

  • babel-loader:不仅可以做ES6-ES5的loader还可以用来实现jsx的编译
  • less-loader:用于将less编译成对应的css
  • css-loader:加载css文件
  • style-loader:转化成内置的
  • json-loader
  • url-loaderimage sprite 的替代方案,会将制定的图片文件合并加载,有limit参数
  • extract-text-webpack-plugin:项目中如果不采用按需加载,而是要将所有的css打包成一个文件,并使用link加载,这个插件就有所帮助了。

webpack实战一个工程配置

有了上述的loader,我们就可以做很多的项目配置了,假设我们有个实际的项目, 基本的操作都包括如下这些环节,我们该如何使用webpack实现这个功能配置呢?

  • JS编译与加载:loader + react模板开发
  • CSS编译与加载:less编译
  • JS与CSS压缩
//package.json
{
  "name": "order-view",
  "version": "0.0.1",
  "dependencies": {
    "babel-loader": "",
    "less-loader": "",
    "css-loader": "",
    "style-loader": "",
    "autoprefixer-loader": "",
    "extract-text-webpack-plugin":""
  }
}

// webpack.config.js
// 执行webpack -p即可完成压缩
var path = require("path");
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
    //多文件入口
    entry: {
        bundle_page1: "./src/search.js",
        bundle_page2: "./src/login.js"
    },
    //指定文件的输出
    output: {
        path: path.join(__dirname, 'out'),
        publicPath: "./out/",
        filename: '[name].js'
    },
    module: {
        loaders: [
            //处理react模板的配置
            {
                test: /.jsx?$/,
                exclude: /(node_modules|bower_components)/,
                loader: 'babel'
            },
            //生成内置的样式<style>
            //{
            //    test: /.less$/,
            //    exclude: /(node_modules|bower_components)/,
            //    loader: "style!css!less"
            //},
            //将依赖打包成一个css文件
            {
                test: /.less$/,
                exclude: /(node_modules|bower_components)/,
                test: /.less$/,
                loader: ExtractTextPlugin.extract(
                    'css?sourceMap&-minimize!' + 'autoprefixer-loader!' + 'less?sourceMap'
                )

            },
            //图片自动合并加载
            {
                test: /.(jpg|png)$/,
                loader: "url?limit=8192"
            }
        ]
    },
    plugins: [
        new ExtractTextPlugin('[name].css')
    ]
};

短链服务

       发布一个简单的短链服务,也可用于加密。使用方法,dj1211.com:8899/?word=www.youtube.com    word后面参数是链接,随后返回一个链接 www.dj1211.com:8899/88r ,发给别人,自动跳转。