前端自动化测试(三)- angular和protracor

protractor用于前端UI自动化测试,特别为angular程序定制

特点

  • 端对端(e2e)测试
  • 采用jasmine作为测试框架
  • 基于WebDriverJS,(selenium-webdriver)
  • 针对angular应用增加定位器,更加方便实用
  • 实现自动等待,告别sleep wait,变异步为同步
  • 支持测试代码的调试
  • 支持多浏览器的并行UI测试

使用方法

准备工作

  • 1 安装protractor: npm install -g protractor
  • 2 安装selenium-standlone: webdriver-manager update
  • 3 启动selenium服务器: webdriver-manager start

spec书写

spec.js是用于书写测试用例的文件, protractor默认使用jasmine作为测试框架,举最简单的例子来说,一个spec文件可以这样写,使用describe作为
测试程序”块”, it定义一个用例,expect作为断言,其中browser这个全局的变量,用于操作浏览器.

describe('Protractor Demo App', function() {
    it('should have a title', function() {
        browser.get('http://juliemr.github.io/protractor-demo/');
        expect(browser.getTitle()).toEqual('Super Calculator');
    });
});

运行

  • 配置conf.json
    在测试之前,我们需要建立一个conf.json的文件,在这个文件中,可以配置测试的相关内容,例如:
    • multiCapabilities:使用哪些浏览器测试
    • chromeOptions:chrome浏览器的运行参数(使用哪些插件等)
    • framework:使用哪种测试框架: cucumber macha 还是jasmine
    • specs:测试哪些文件
      详细的配置信息请参考
exports.config = {
   // directConnect: true,

    // Capabilities to be passed to the webdriver instance.
    //capabilities: {
    //    'browserName': 'chrome'
    //},
    multiCapabilities: [ {
        'browserName': 'chrome',
        //'chromeOptions': {
        //    'args': ['--load-extension=/opt/local/share/nginx/html/radar/tanxtag'],
        //}
    }],
    // Framework to use. Jasmine is recommended.
    framework: 'jasmine',

    // Spec patterns are relative to the current working directly when
    // protractor is called.
    specs: ['basic/demo_spec.js','basic/angular_spec.js'],

    // Options to be passed to Jasmine.
    jasmineNodeOpts: {
        defaultTimeoutInterval: 30000
    }
};
  • 运行测试程序
protractor conf.json

grunt运行测试

有时我们需要使用grunt来配置测试任务,下面就是使用grunt-concurrent 模块实现并行运行多浏览器(也可通过conf.json中配置multiCapabilities解决)测试程序的代码:

module.exports = grunt => {
    //This module will read the dependencies/devDependencies/peerDependencies/optionalDependencies in your package.json
    // and load grunt tasks that match the provided patterns.
    require('load-grunt-tasks')(grunt);
    grunt.initConfig({
        concurrent: {
            protractor_test: ['protractor-chrome', 'protractor-firefox', 'protractor-safari']
        },
        protractor: {
            options: {
                keepAlive: true,
                singleRun: false,
                configFile: "conf.js"
            },
            run_chrome: {
                options: {
                    args: {
                        browser: "chrome"
                    }
                }
            },
            run_firefox: {
                options: {
                    args: {
                        browser: "firefox"
                    }
                }
            },
            run_safari: {
                options: {
                    args: {
                        browser: "safari"
                    }
                }
            }
        }
    });

    grunt.registerTask('protractor-chrome', ['protractor:run_chrome']);
    grunt.registerTask('protractor-firefox', ['protractor:run_firefox']);
    grunt.registerTask('protractor-safari', ['protractor:run_safari']);
    grunt.registerTask('protractor-e2e', ['concurrent:protractor_test']);
};

调试测试程序

除了非常方便的运行机制,protractor还提供便捷的调试方式, 使用selenium-webdriver操纵浏览器的时候,调试是非常困难的,在这里protractor就提供调试方式
在代码中加入 browser.pause(); 并且在终端输入 “repl” 就可以使用WebDriver commands来调试程序了:

wd-debug> repl
> element
function (locator) {
    return new ElementArrayFinder(ptor).all(locator).toElementFinder_();
  }
> 

测试非angular的应用

protractor内置方法测试angular的程序,例如它会自动检测angular页面加载完毕才会执行测试程序,当测试非angular程序的时候需要:
– 1 使用 browser.driver 代替 driver
– 2 添加 browser.driver.ignoreSynchronization = true
参考

protractor的详细使用

在protractor中,有几大类用于测试代码,详情请见protractorAPI
– browser: 浏览器的操作
– element & by: 定位获取页面元素
– ExpectedConditions:用于页面操作的逻辑函数,一般同wait连用
– webdriver: selenium 原生的语法函数
– promise:selenium内置的promise方法

浏览器的操作-browser

常用操作代码如下:

  • browser.get:
  • browser.findElement
  • browser.switchTo().frame()
  • browser.executeScript:
  • browser.executeAsyncScript
  • browser.wait:
  • browser.sleep:

选择器- by & element

支持多源选择器
– by.css()
– by.id()
– by.xpath()
– by.name()
– by.tagName()
– by.model():angular专用
– by.binding():angular专用
– by.repeater():angular专用

通过element获取:element(by.id(‘frameId’))或者element.all(by.css(‘some-css’));
在非angular应用中使用browser.driver.findElement(by.id(‘frameId’))

ExpectedConditions

预定义了wait的条件,常用的有
– elementToBeClickable: 按钮可以点击
– presenceOf: 元素出现在dom中
– titleContains: title含有某个字符串
– visibilityOf: 某个元素显示

    var EC = protractor.ExpectedConditions;
    var button = $('#xyz');
    var isClickable = EC.elementToBeClickable(button);

    browser.get(URL);
    browser.wait(isClickable, 5000); //wait for an element to become clickable
    button.click();

综合实例


it ('test login error', function () { _driver.get('http://subway.simba.taobao.com/#!/login'); _driver.wait(protractor.until.elementLocated(by.css('.login-ifr')),1000).then(function (elem) { _driver.switchTo().frame(elem); _driver.findElement(by.name('TPL_username')).sendKeys('zhangmeng1986712'); _driver.findElement(by.name('TPL_password')).sendKeys('xxxxx'); _driver.findElement(by.id('J_SubmitStatic')).click(); _driver.sleep(1000); browser.driver.findElement(by.css('.error')).then(function (elem) { return elem.getInnerHtml().then(function(text) { expect(text).toMatch('密码和账户名不匹配'); }); }); }); });

page object pattern

page object的模式大家一定不陌生,通过合理的配置可以使测试代码更容易维护,举例来说可以这样:

//书写一个input操作类
var AngularHomepage = function() {
  var nameInput = element(by.model('yourName'));
  var greeting = element(by.binding('yourName'));

  this.get = function() {
    browser.get('http://www.angularjs.org');
  };

  this.setName = function(name) {
    nameInput.sendKeys(name);
  };

  this.getGreeting = function() {
    return greeting.getText();
  };
};
//测试代码
describe('angularjs homepage', function() {
  it('should greet the named user', function() {
    var angularHomepage = new AngularHomepage();
    angularHomepage.get();
    angularHomepage.setName('Julie');
    expect(angularHomepage.getGreeting()).toEqual('Hello Julie!');
  });
});

mobile端的测试

详情参考
这个例子是使用Appium作为server端进行测试的,由于selenium-webdriver不能直接联Appium, 所以需要使用wd-bridge进行折衷.

e2e测试程序设计准则

参考

参考代码

本文的参考代码见 Github

前端自动化测试之单元测试(二)—— react组件的测试工具jest

前面介绍了Polymer的测试框架web-components-tester, 今天来看看React团队出品的Jest.在此,特别感谢婆婆帮忙带宝宝才让我有时间继续书写文章.

Jest的功能

  • 内置jasmine
  • 内置mock函数 可以mock模块
  • 基于jsdom
  • 同步化书写异步代码
  • 真心赞一下简洁明了的API定义和用法 以及清晰的文档,确实让书写单元测试不再痛苦
  • 适用于commonJS模块的单元测试
  • 运行速度较慢

Jest用法

  • 安装: npm install jest-cli(需要node版本大于4)
  • 配置 package.json 如下
  • 运行: npm test
  • 调试(使用node-debug再浏览器中调试):node-debug –nodejs –harmony ./node_modules/jest-cli/bin/jest.js –runInBand tests/getUser-test.js
  • 运行单个文件 ./node_modules/jest-cli/bin/jest.js tests/getUser-test.js
{
  "name": "jest-test-examples",
  "version": "0.0.1",
  "dependencies": {},
  "scripts": {
    "test": "jest"
  }
}

下面就具体介绍一下使用jest进行测试的方法,采用的例子为jest的官方实例

一个简单的测试

jest.dontMock('../src/sum');
describe('sum', function() {
    it('adds 1 + 2 to equal 3', function() {
        var sum = require('../src/sum');
        expect(sum(1, 2)).toBe(3);
    });
});

describe和it还有expect都使用了jasmine的语法, jest会自动mock所有的依赖模块,对模块中所有的输出方法予以遍历并进行mock,对于要测试的模块使用jest.dontMock标识,jest就不会去mock.

异步的单元测试实例

异步是javascript的灵魂, 所以异步的测试也是极其重要的,下面看看jest关于异步程序的测试,假如有这样个ajax程序,获取数据并进行解析,对其进行测试

  • 1 ajax的数据获取是否正确
  • 2 parseUserJson是否正确
    在第二个测试用例中使用了genMockFunction,用来对回调函数进行mock,在jest中有两种方式进行函数的mock
    • 1 使用xFunc = require(‘xx’);
    • 2 使用xFunc = jest.genMockFunction();
      使用后,会在xFunc.calls中存储有关函数的调用信息,例如
    • //mock.calls.length 记录了函数被调用了几次
    • //mock.calls[0][0] 被调用函数的第一个参数
    • //mock.calls[0][1] 第二个参数
      代码如下: $.ajax是一个被mock的函数,callback也被mock,getUser(callback)调用后,可以通过检测传递的参数判断是否正确.
var $ = require('jquery');
function parseUserJson(userJson) {
    return {
        loggedIn: true,
        fullName: userJson.firstName + ' ' + userJson.lastName
    };
}
function fetchCurrentUser(callback) {
    return $.ajax({
        type: 'GET',
        url: 'http://example.com/currentUser',
        success: function(userJson) {
            callback(parseUserJson(userJson));
        }
    });
}
module.exports = fetchCurrentUser;
jest.dontMock('../src/getUser');
describe('getUser', function() {
    //test right params
    it('calls into $.ajax with the correct params', function() {
        var $ = require('jquery');
        var getUser = require('../src/getUser');
        function dummyCallback() {}
        getUser(dummyCallback);
        // Now make sure that $.ajax was properly called during the previous
        // 2 lines
        expect($.ajax).toBeCalledWith({
            type: 'GET',
            url: 'http://example.com/currentUser',
            success: jasmine.any(Function)
        });
    });

    //test callback function
    it('calls the callback when $.ajax requests are finished', function() {
        var $ = require('jquery');
        var getUser = require('../src/getUser');

        //create mock function
        var callback = jest.genMockFunction();
        getUser(callback);

        //xfunc.mock have interactions information
        //mock.calls.length call times
        //mock.calls[0][0] first param
        //mock.calls[0][1] second param
        //https://facebook.github.io/jest/docs/mock-functions.html

        //emulate the params pass to success
        $.ajax.mock.calls[0][0].success({
            firstName: 'Bobby',
            lastName: '");DROP TABLE Users;--'
        });

        expect(callback.mock.calls[0][0]).toEqual({
            loggedIn: true,
            fullName: 'Bobby ");DROP TABLE Users;--'
        });

    });
});

React组件的单元测试实例

假如我们有这样一个checkbox react组件, 如下, react的es6写法请参考我的blog-ES6的核心语法与应用
原理非常简单,点击checkbox切换label的状态.我们的测试代码如下, 使用了react-addons-test-utils这个模块, 模块的renderIntoDocument用于将react组件渲染到document中,
并且支持产生模拟事件:TestUtils.Simulate.change.

import React from 'react';
class Checkbox extends React.Component {
    constructor(props) {
        super(props);
        this.state = {isChecked: false};
        this.changeState = this.changeState.bind(this);
    }
    changeState () {
        this.setState({isChecked: !this.state.isChecked})
    }
    render() {
        return (<label>
                <input type="checkbox" checked={this.state.isChecked} onChange={this.changeState} />
                {this.state.isChecked ? this.props.labelOn : this.props.labelOff}
               </label>)
    }

}
export default Checkbox;
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import ReactDom from  'react-dom';
jest.dontMock('../src/checkbox');
const Checkbox = require('../src/checkbox');

describe('test react checkbox component', () => {
    it('change the label after click', () => {
        //1 render component
        //2 get node label get default value off
        //3 simulate click
        //4 expect value equal on

        //TestUtils.renderIntoDocument method refers to https://facebook.github.io/react/docs/test-utils.html#renderintodocument
        //TestUtils.findRenderedDOMComponentWithTag https://facebook.github.io/react/docs/test-utils.html#findrendereddomcomponentwithtag
        //ReactDom API:findDOMNode render unmountComponentAtNode server-side:renderToString renderToStaticMarkup

        var checkbox = TestUtils.renderIntoDocument(<Checkbox labelOn="On" labelOff="Off" />);
        var checkboxNode = ReactDom.findDOMNode(checkbox);
        //https://facebook.github.io/jest/docs/api.html#expect-value
        expect(checkboxNode.textContent).toEqual('Off');
        TestUtils.Simulate.change(TestUtils.findRenderedDOMComponentWithTag(checkbox, 'input'));
        expect(checkboxNode.textContent).toEqual('On');
    });
});

运行的时候我们需要通过babel预处理一下,通过如下的方式配置package.json即可运行:

  "scripts": {
    "test": "jest"
  },
  "jest": {
    "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
    "unmockedModulePathPatterns": [
      "<rootDir>/node_modules/react",
      "<rootDir>/node_modules/react-dom",
      "<rootDir>/node_modules/react-addons-test-utils"
    ],
    "modulePathIgnorePatterns": [
      "<rootDir>/node_modules/"
    ]
  }

手动mock

经常我们需要模拟某个模块中的方法(此方法实现非常复杂依赖第三方的模块)用来测试另一个模块的输入输出是否正确,jest就提供非常方便的mock机制,例如,我们在A模块中依赖jquery的fx方法
而fx方法又依赖于其他方法, 因为我们只关心fx的输出,所以我们就可以直接用来模拟,方法如下:

  • 建立mocks文件夹
  • 新建jquery模块:jquery.js
  • 使用genMockFromModule和mockImplementation API 如下
// mock the module of real jquery

var jqueryMocks = jest.genMockFromModule('jquery');
var mock_fx = function () {
    return 'mockValue';
};
//using mock_fx to mock the function of real fx
jqueryMocks.fx.mockImplementation(mock_fx);
module.exports = jqueryMocks;

这样就可以在测试代码中直接引用已经模拟好的fx函数进行测试了,直接对模块的输入控制,减少了依赖,实现测试的”解耦”.

describe('jest mocks', function () {
   it('jquery mock getEnv value', function(){
       var value = require('jquery').fx();
       expect(value).toEqual('mockValue')
   });
});

代码参考

源码

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