前端自动化测试基础-sinon篇章

sinon用途

  • 在测试领域 Test double是很重要的一个概念。Test double主要用在自动化测试领域,会使用简单的对象或者流程模拟对应的行为减少测试的复杂性。
  • 用于 JavaScript 的测试监视(spy)、桩(stub)和仿制(mock)功能。不依赖其他类库,兼容任何单元测试框架。

sinon spy(最常用)

  • test spy 是这样的一类函数,它可以记录自己被调用的情况,包括传入的参数、返回结果、this 指向和抛出的错误(如果有的话)。test spy 可以是一个匿名函数,也可以是对一个已有函数进行的封装。
  • 用于测试callback函数
  • 用于spy已知行为的方法
  • 创建spy
    //创建一个匿名的函数用于记录调用的参数、返回值、以及异常
    var spy = sinon.spy();
    var spy = sinon.spy(myFunc);
    //对对象的方法增加spy 用于替换原有方法的行为,可以通过调用object.method.restore()实现恢复设置
    var spy = sinon.spy(object, "method");
  • sinon API用法
    • 判断某函数调用了某些参数:spy.withArgs(arg1[, arg2, …]); e.g. assert(spy.withArgs(42).calledOnce);
    • 某函数调用的次数:spy.callCount
    • spy.called
    • spy.calledTwice spy.calledThrice
    • spy.firstCall spy.secondCall spy.thirdCall
    • 判断是否在另一个spy之前(后)被调用 spy.calledBefore(anotherSpy);spy.calledAfter(anotherSpy);
    • 至少有一次被某个参数调用,参数可以部分匹配:spy.calledWith(arg1, arg2, …);
    • 至少有一次抛出异常:spy.threw();
  • sinon spy实例
        //以backbone的Event单测为例
        var eventer = _.extend({}, Backbone.Events),
            spy = sinon.spy();

        // Set up the spy.
        eventer.on("foo", spy);
        expect(spy.called).to.be.false;

        // Fire event.
        eventer.trigger("foo", 42);

        // Check number of calls.
        expect(spy.calledOnce).to.be.true;
        expect(spy.callCount).to.equal(1);

        // Check calling arguments.
        expect(spy.firstCall.args[0]).to.equal(42);
        expect(spy.calledWith(42)).to.be.true;

sinon stub

  • stub(桩)其实是最抽象最难理解的,Test stubs是一类预编码行为的函数(也是一种 spy)。除了改变stub对象的行为之外,它还支持所有的 spy API。同spy一样,stubs 可以是匿名函数,或者包装已有函数。当使用 stub 包装一个已有函数时,原函数将不会被调用。
  • stub用于:
    • 在测试中控制一个方法的行为,以强制代码沿特定路径执行。例如测试错误处理时,可以强制一个方法抛出错误。
    • 当你希望阻止一个方法被直接调用时(可能是因为这个方法触发了干扰行为,例如 XHR 请求之类的)。
  • 创建stub

    • 创建一个匿名的 stub 函数。var stub = sinon.stub();
    • 使用一个 stub 函数替代 object.method。原函数可以通过调用 object.method.restore() (或 stub.restore())方法来还原。如果 object.method 不是一个函数,则会抛出一个异常来帮助你避免类型错误。var stub = sinon.stub(object, “method”);
    • 使用 func 来替换 object.method,并且被包装在一个 spy 中。object.method.restore() 可以恢复原方法。var stub = sinon.stub(object, “method”, func);
    • stub 该对象的所有方法。var stub = sinon.stub(obj);
  • stub API,详情
    • stub.withArgs(arg1[, arg2, …]);
    • stub.returns(obj);
    • stub.throws(); 例: var callback = sinon.stub(); callback.withArgs(1).throws(“TypeError”);
    • stub.yieldsTo(property, [arg1, arg2, …])
  • sinon stub实例
//basic usage
    var obj = {
      multiply: function (a, b) { return a * b; },
      error: function (msg) { throw new Error(msg); }
    };

    it("stubs multiply", function () {
      // Stub with a hard-coded return value.
      sinon.stub(obj, "multiply").returns(5);
      expect(obj.multiply(1, 2)).to.equal(5);
      obj.multiply.restore();

      // Stub with a function.
      sinon.stub(obj, "multiply", function (a, b) {
        return a + b;
      });
      expect(obj.multiply(1, 2)).to.equal(3);
      obj.multiply.restore();
    });

    it("stubs error", sinon.test(function () {
      this.stub(obj, "error");
      expect(obj.error).to.not.throw();
    }));
  });

//use yieldsTo
    it("stubs with yieldsTo", function () {
            var obj = {
                    async: function (opts) {
                        opts.success("a", "b");
                    }
                },
                spyObj = {
                    failure: sinon.spy(),
                    success: sinon.spy()
                };

            sinon.stub(obj, "async").yieldsTo("success", 1, 2);

            // Call on object with callback spies.
            obj.async(spyObj);

            expect(spyObj.failure).to.have.not.have.been.called;
            expect(spyObj.success)
                .to.have.been.calledOnce.and
                .to.have.been.calledWith(1, 2);
        });

##sinon mock

  • 用于给出expectation然后验证某个object的method是否是正确的
  • 同spy的区别,mock出的object收到了数据或是调用并没有真正执行,一切针对mock的调用都是假的。所以mock可以用来测试具有side effect的函数,这里的side effect泛指和外部对象有数据交互或者是调用,比如调用外部对象的方法、向server发送数据、和UI对象有交互、写日志等等。
  • API
    • 创建mock:var mock = sinon.mock(obj);
    • 给出expectation mock.expects(“method”);
    • 校验是否正确: mock.verify();
    • 重置:mock.restore();
  • sinon mock 例子

describe("Sinon.JS mocks", function () {
  // Object literal with two methods.
  var obj = {
    multiply: function (a, b) { return a * b; },
    error: function (msg) { throw new Error(msg); }
  };

  it("mocks multiply", function () {
    // Create the mock.
    var mock = sinon.mock(obj);

    // The multiply method is expected to be called:
    mock.expects("multiply")
      .atLeast(2)    // 2+ times,
      .atMost(4)     // no more than 4 times, and
      .withArgs(2);  // 2 was first arg on *all* calls.

    // Make 3 calls to `multiply()`.
    obj.multiply(2, 1);
    obj.multiply(2, 2);
    obj.multiply(2, 3);

    // Verify **all** of the previous expectations.
    mock.verify();

    // Restore the object.
    mock.restore();
  });

});

sinon Fake XMLHttpRequest/ Fake Server

  • fake server用法
    {
        setUp: function () {
            this.server = sinon.fakeServer.create();//创建server
        },

        tearDown: function () {
            this.server.restore();
        },

        "test should fetch comments from server" : function () {
            this.server.respondWith("GET", "/some/article/comments.json",
                [200, { "Content-Type": "application/json" },
                 '[{ "id": 12, "comment": "Hey there" }]']);

            var callback = sinon.spy();
            myLib.getCommentsFor("/some/article", callback);
            this.server.respond();

            sinon.assert.calledWith(callback, [{ id: 12, comment: "Hey there" }]);
        }
    }

  • sinon可以用作实现request的模拟,现在更多的使用supertest
    用于HTTP的测试
describe('GET /user', function(){
  it('user.name should be an case-insensitive match for "tobi"', function(done){
    request(app)
      .get('/user')
      .set('Accept', 'application/json')
      .expect(function(res) {
        res.body.id = 'some fixed id';
        res.body.name = res.body.name.toUpperCase();
      })
      .expect(200, {
        id: 'some fixed id',
        name: 'TOBI'
      }, done);
  });
});

sinon 测试代码

前端自动化测试基础-断言篇: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结合的测试用例

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


});