前端自动化测试之单元测试(二)—— 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')
   });
});

代码参考

源码

ES6的核心语法与应用

一直以来都对ES6嗤之以鼻,本来灵活简单的Javascrit,非得为了提升B格,增加学习的成本,搞那么多鸡肋的语法。但是无奈俺们这些“老年jser”都被历史的车轮碾压了,现在如果不掌握ES6,估计很多代码都看不懂了。没有闲暇的午后时间来系统的学习ES6(其实还是有点抵触心理),但是为了跟上“年轻人”的步伐,随着用随着看随着学吧。力求以最简单的语言讲述。

模块

定义模块

语法为:

  • export function x () {}
  • export class
  • export default {}
// kittydar.js - 找到一幅图像中所有猫的位置
    export function detectCats(canvas, options) {
      var kittydar = new Kittydar(options);
      return kittydar.detectCats(canvas);
    }
    export class Kittydar {
      ... 处理图片的几种方法 ...
    }
    // 这个helper函数没有被export。
    function resizeCanvas() {
      ...
    }
    //默认的
    export default {
        xx: '111'
    }

引用模块

 import {detectCats} from "kittydar.js"; //引入某个方法
 import {detectCats, Kittydar} from "kittydar.js"; //引入并重命名
 import * as module from './module';//引入全部全部
 import helloWorld from './hello-world'; //引入默认
 function go() {
        var canvas = document.getElementById("catpix");
        var cats = detectCats(canvas);
        drawRectangles(canvas, cats);
    }

class用法

  • 基本语法 class A {}
  • 构造器 constructor {}
  • 继承 class A extends AParent {}
  • super()
  • 注意:类声明与函数声明不同,它不会被提升,所以先new 后class定义 会抛出异常
  • 静态变量:static compare(a, b) {}
//ES5
//使用Object.defineProperty实现可读属性make year
function Vehicle(make, year) {
  Object.defineProperty(this, 'make', {
    get: function() { return make; }
  });

  Object.defineProperty(this, 'year', {
    get: function() { return year; }
  });
}

Vehicle.prototype.toString = function() {
  return this.make + ' ' + this.year;
}

var vehicle = new Vehicle('Toyota Corolla', 2009);
console.log(vehicle.make); // Toyota Corolla
vehicle.make = 'Ford Mustang'; //静态属性
console.log(vehicle.toString()) // Toyota Corolla 2009
//ES6
class Vehicle {
  constructor(make, year) {
    this._make = make;
    this._year = year;
  }

  get make() {
    return this._make;
  }

  get year() {
    return this._year;
  }

  toString() {
    return 'xxx';
  }
}

var vehicle = new Vehicle('Toyota Corolla', 2009);

console.log(vehicle.make); // Toyota Corolla
vehicle.make = 'Ford Mustang';
console.log(vehicle.toString()) // Toyota Corolla 2009
//ES5的继承
function Motorcycle(make, year) {
  Vehicle.apply(this, [make, year]);
}

Motorcycle.prototype = Object.create(Vehicle.prototype, {
  toString: function() {
    return 'xxx';
  }
});

Motorcycle.prototype.constructor = Motorcycle;

//ES6
class Motorcycle extends Vehicle {
  constructor(make, year) {
    super(make, year);
  }

  toString() {
    return 'xxxx';
  }
}

箭头函数

  • 箭头函数的产生,主要由两个目的:更简洁的语法和与父作用域共享关键字this。
  • function和{}都消失了,所有的回调函数都只出现在了一行里。
  • 当只有一个参数时,()也消失了(rest参数是一个例外,如(…args) => …)。
  • 当{}消失后,return关键字也跟着消失了。单行的箭头函数会提供一个隐式的return(这样的函数在其他编程语言中常被成为lamda函数)。
  • 箭头函数没有它自己的this值,箭头函数内的this值继承自外围作用域。
  • 箭头函数与普通函数还有一个区别就是,它没有自己的arguments变量,但可通过rest参数获得。
function () { return 1; }
() => { return 1; }
() => 1

function (a) { return a * 2; }
(a) => { return a * 2; }
(a) => a * 2
a => a * 2

function (a, b) { return a * b; }
(a, b) => { return a * b; }
(a, b) => a * b

function () { return arguments[0]; }
(...args) => args[0]

() => {} // undefined
() => ({}) // {}
//在之前的js中setInterval会把this指向window,
//使用箭头函数this使用外层的作用域所以不用保存this指针
$('.current-time').each(function () {
  setInterval(() => $(this).text(Date.now()), 1000);
});
//箭头函数的arguments,通过rest函数可以获得
function log(msg) {
  const print = (...args) => console.log(args[0]);
  print(`LOG: ${msg}`);
} 
log('hello'); // LOG: hello

作用域

javascript本身是没有块级作用域的,ES6新增的let语法替代var实现了块级作用域。

//before
function func(arr) {
    for (var i = 0; i < arr.length; i++) {
        // i ...
    }
    // 这里也可以访问到i
}
//ES6
function func(arr) {
    for (let i = 0; i < arr.length; i++) {
        // i ...
    }
    // 这里访问不到i
}

React on ES6

详情参考 这篇文章

定义组件

// The ES5 way
var Photo = React.createClass({
  handleDoubleTap: function(e) { … },
  render: function() { … },
});
// The ES6+ way
class Photo extends React.Component {
  handleDoubleTap(e) { … }
  render() { … }
}

componentWillMount关键字

// The ES5 way
var EmbedModal = React.createClass({
  componentWillMount: function() { … },
});
// The ES6+ way
class EmbedModal extends React.Component {
  constructor(props) {
    super(props);
    //实现componentWillMount内容的地方像dom操作
  }
}

state和props初始化

// The ES5 way
var Video = React.createClass({
  getDefaultProps: function() {
    return {
      autoPlay: false,
      maxLoops: 10,
    };
  },
  getInitialState: function() {
    return {
      loopsRemaining: this.props.maxLoops,
    };
  },
  propTypes: {
    autoPlay: React.PropTypes.bool.isRequired,
    maxLoops: React.PropTypes.number.isRequired,
    posterFrameSrc: React.PropTypes.string.isRequired,
    videoSrc: React.PropTypes.string.isRequired,
  },
});

// The ES6+ way
// static 实现只读的props
// 全局state
class Video extends React.Component {
  static defaultProps = {
    autoPlay: false,
    maxLoops: 10,
  }
  static propTypes = {
    autoPlay: React.PropTypes.bool.isRequired,
    maxLoops: React.PropTypes.number.isRequired,
    posterFrameSrc: React.PropTypes.string.isRequired,
    videoSrc: React.PropTypes.string.isRequired,
  }
  state = {
    loopsRemaining: this.props.maxLoops,
  }
}

react中的事件


class PostInfo extends React.Component { constructor(props) { super(props); // Manually bind this method to the component instance... this.handleOptionsButtonClick = this.handleOptionsButtonClick.bind(this); } handleOptionsButtonClick(e) { // this应指向实例 this.setState({showOptionsModal: true}); } } //箭头函数this指向外层的组件 class PostInfo extends React.Component { handleOptionsButtonClick = (e) => { this.setState({showOptionsModal: true}); } }

实例

使用ES6改写的React组件程序

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;

hadoop mapreduce set number of mappers

As a hadoop platform developer, my job is to maintain the hadoop platform , fix the potential bugs and develop the new feature to meet my company’s need.I don’t get involve in the specfic mr job development.But today,one of my college asked me if i know how to set the map number in mapreduce job, it make to search for a while.
We all know, we can set the reduce number by call setNumReduceTasks.However,when you use setNumMapTasks to set numbers of mappers,it does not work,because this configration is just a hint,the inputformat just ignore it and create the splits for the job.
So i just check the code in FileInputFormat.class, and found how to set number of mappers in mapreduce job.
1.For files which file size is larger then block size

ie. block size is 128MB,and file size is several GB, we can set the mapreduce.input.fileinputformat.split.minsize paramter in configuration class.
Configuraiton conf = getConf();
conf.set("mapreduce.input.fileinputformat.split.minsize",1374389534720);
this make split size to be 1GB.

2.For small files which file size is smaller then block size

ie the file size is several MB,we have to use the CombineTextInputFormat replace the FileInputFormat, and use mapreduce.input.fileinputformat.split.minsize with the inputformat together to set number of mappers.
job.setInputFormatClass(CombineSmallfileInputFormat.class);
Configuraiton conf = getConf();
conf.set("mapreduce.input.fileinputformat.split.minsize",1374389534720);
Then you can control the number of mappers in you mr programme.

Socket Read Timeout 引发的问题

线上一个流式向hadoop集群写入数据的系统需要保证写入exactly once,并且不能丢失数据。为了解决这一问题,我们采用了thrift+write ahead log来构建这一系统。在使用过程中一直效果很好,最近偶然间发现这个服务发送了重复的日志。
经过检查发现,原来是是thrift tsocket read timeout导致的问题。我们的系统逻辑是client端向该系统发送日志,如果抛出异常,会将日志落到本地,等待一段时间后从本地读出日志继续发送。检查日志后发现在client端抛出

java.net.SocketTimeoutException: Read timed out

这就解释了写入重复的问题,首先client通过thrift TSokcet向server发送日志,如果超时了,client端timeout,抛出异常,将该条日志写入本地,过一段时间重现发送。而server端由于处理繁忙,没有及时响应client,虽然client中断连接,但是Server接收到该日志,并进行了处理。过一段时间后client端重新发送就产生了重复日志。
通过脚本检查这一问题的严重性,大概一条最多只有一条,目前将client端的超时时间增大解决这一问题。

YARN-4493 ResourceManager moveQueue bug

When moving a running application to a different queue in resourceManger , the current implement don’t check if the app can run in the new queue before remove it from current queue. So if the destination queue is full, the app will throw exception, and don’t belong to any queue.
Here is the code

   private void executeMove(SchedulerApplication app, FSSchedulerApp attempt,
       FSLeafQueue oldQueue, FSLeafQueue newQueue) {
     boolean wasRunnable = oldQueue.removeApp(attempt);
     // if app was not runnable before, it may be runnable now
     boolean nowRunnable = maxRunningEnforcer.canAppBeRunnable(newQueue,
         attempt.getUser());
     if (wasRunnable && !nowRunnable) {
       throw new IllegalStateException("Should have already verified that app "
           + attempt.getApplicationId() + " would be runnable in new queue");

After that, the queue become orphane, can not schedule any resources. If you kill the app, the removeApp method in FSLeafQueue will throw IllealStateException of “Given app to remove app does not exist in queue …” exception.
So i think we should check if the destination queue can run the app before remove it from the current queue.
The patch is below:

   private void executeMove(SchedulerApplication app, FSSchedulerApp attempt,
       FSLeafQueue oldQueue, FSLeafQueue newQueue) {
-    boolean wasRunnable = oldQueue.removeApp(attempt);
     // if app was not runnable before, it may be runnable now
     boolean nowRunnable = maxRunningEnforcer.canAppBeRunnable(newQueue,
         attempt.getUser());
+    boolean wasRunnable = false;
+    if (nowRunnable) {
+        wasRunnable = oldQueue.removeApp(attempt);
+    }
     if (wasRunnable && !nowRunnable) {
       throw new IllegalStateException("Should have already verified that app "
           + attempt.getApplicationId() + " would be runnable in new queue");

脐带血的捐献

怀孕9个月的时候去产检,护士提到了关于脐带血的自存与捐赠。发现很多父母为了孩子一掷“万”金存储脐带血,但是这样做真的是有意义的么?作为一个准妈妈,为了给即将出生的孩子“有备无患”的人生,查询了国内外的很多资料,在这里给大家一个全面的讲解,帮助广大父母选择对脐带血的处理。

什么是脐带血

脐带血是胎儿娩出、脐带结扎并离断后残留在胎盘和脐带中的血液,通常是废弃不用的。近十几年的研究发现,脐带血中含有可以重建人体造血和免疫系统的造血干细胞,这些最原始的造血干细胞,拥有无限分化的潜力,是建造人体血液及免疫系统的基石,并有分化成为其他组织细胞的潜力。

脐带血的用处

世界范围内,目前脐血已经用于治疗各种类型白血病、再生障碍性贫血、淋巴瘤、多发性骨髓瘤、神经母细胞瘤、粘多糖病、地中海贫血、骨髓发育不良症候群、原发性免疫缺陷病、慢性肉芽肿等80多种疾病。
此外脐带血还具有很高的临床研究价值,医学研究发现,脐带血中的造血干细胞,具有复制、分化成为其他不同种类细胞的能力,包括骨骼、心脏、肌肉及神经细胞等,因此许多医疗研究,朝着脐带血再生医学运用潜力的方向进行,例如:老人失智症、糖尿病、心脏病、肝脏疾病、肌肉萎缩症、帕金森氏症、脊髓神经损伤及中风等。

取脐带血安全么

很多的人都会关心这个问题,取脐带血是否是安全的,会不会影响宝宝或者妈妈 例如 造成感染之类的,具体的采集流程如下图,脐血采集不同于传统的骨髓采集,不需要进行麻醉,无痛、无副作用,是在断脐以后进行的,因此对母亲和孩子没有任何不良影响,属于“废物利用,变废为宝”。以北新妇为例,采集会由具有专业资质的医护人员完成,在婴儿出生后,使用含有抗凝剂的密封式血袋收集脐血。

捐献脐带血为社会奉献一份爱心

既然上面说了脐带血有这么多好处,那么更应该建议广大家长为孩子买这份“保险“,为自己的孩子自存脐带血了,但是,其实自存脐带血而将来孩子出现疾病使用脐带血自体移植治疗不一定是最好的,这要看需要治疗的疾病种类而定。 如果医生是用干细胞来帮助自体修复,那么使用病人自己的细胞是最理想的,不会出现病人的身体排斥自体干细胞的现象。 不过,如果自体已经在制造错误细胞,比方说,如果疾病种类是癌症或者遗传性血液病,那就必须移植其他捐献者的细胞,不能再用自体干细胞了。这是因为,病人的干细胞很可能包含同样的缺陷,而正是这种缺陷引起了癌症或遗传性疾病。如果采用自体移植,无异是将疾病的种子又植回了病人体内。 从以下几个方面看,同样捐献脐带血的意义要比自体存储的意义更大,

  • 小概率:作为国家卫生部门批准的“特殊血站”,北京脐带血库,2002年起开始接收产妇自愿付费、为宝宝和家人存储脐带血。北京脐血库库存容量50万份,目前已存放超过19万份脐血。据北京脐带血库数据显示,目前北京脐带血库已有650份脐带血应用于临床,成功治疗患者610位,其中大部分来自公共库,自体库只有11例。所以,自存脐带血并应用于自身,一定程度上确实属于“概率事件”。
    • 有时限:目前,一份脐带血的入库标准是50毫升。这份脐带血容量偏低,一般只对30公斤的儿童有效。还要根据治疗疾病的种类及脐带血细胞数而定。不过只要细胞数量足够,也可以用于大体重的患者。
    • 有风险:脐带血并不能“包治百病”。一些遗传疾病由于自身基因缺陷所致,可能不适合用自己的脐带血治疗,如一些血液和免疫系统方面的遗传病。这种情况下应该选择公共库脐血。

脐带血是新生儿带给人类的一份“厚礼“,建议请不要轻易将宝宝的脐带血放弃,把它捐赠到公共库,这样可以帮助到更多需要造血干细胞移植的患者,只有公共库的建设强大了才有益于脐带血发挥更大作用,才能真正的造福我们的后代,在孩子出生之际就让他学会助人为乐和奉献的美德,而不是盲目的用金钱堆积他的人生。

前端自动化测试之单元测试(一)—— polymer组件的测试工具

单元测试的工具们

  • 单元测试框架
    • Qunit – jquery的单测工具
    • jasmine – 早期的测试框架
    • mocha – 常用框架 支持BDD和TDD
    • Cucumber – 语义化更好的测试工具
  • 断言
    • chai
  • 集成
    • Karma
    • jenkins
    • travis-ci
  • mock
    • sinon
    • supertest
  • 组件测试框架
    • polymer的单测工具:web-component-tester
    • react的单测工具:Jest

之前的博客对比较基础的测试工具都有所介绍,参考前端自动化测试基础篇,在这篇blog中,我们首先了解一下polymer组件的单元测试工具。
mocha
chai
sinon

why web-component-tester

web-component-tester是polymer组件用于单元测试的框架,主要是用作对于html文件的测试
– 它内部集成了mocha、sinon、sinon-chai、chai,方便使用
– 使用lodash作为工具函数
– async用作异步函数的测试
– test-fixture作为

API方法

基本API

  • WCT.loadSuites可以将
  • suite类似describe将测试进行归类
  • test类似it进行具体的测试
  • assert用于进行断言,断言同chai的assert的断言规则
suite('AwesomeLib', function() {
  test('is awesome', function() {
    assert.isTrue(AwesomeLib.awesome);//TDD模式
  });
});

特殊的方法

  • text-fixture 用来在测试过程中操作template中的dom元素,用法是在
<test-fixture id="simple">
  <template>
    <div></div>
  </template>
</test-fixture>
<script>
  suite('classList', function() {
    var div;
    setup(function() {
      div = fixture('simple');
    })
    test('foo', function() {
      div.classList.add('foo');
      assertSomethingOrOther(div);
    });
  });
</script>

关于template binding异步的测试

异步测试在javascript的世界中可谓最常见,对于polymer组件来说,数据驱动模板刷新很重要,
模板数据的刷新会调用Polymer.dom.flush, 它是个异步的过程,对此 web-components-tester 专门提供
flush函数处理此类异步。

suite('with two selected items', function() {
  // Clean up after ourselves.
  teardown(function(done) {
    s.clearSelection();
    s.multi = false;
    // Wait for observers to resolve before moving on to more tests.
    flush(done);
  });

  test('multi selects by index', function(done) {
    s.multi = true;
    //数据变化
    s.selected = [0, 2];
    flush(function() {
      //模板刷新
      assert.equal(s.selectedIndex, [0, 2]);
      assert(s.children[0].classList.contains('core-selected'));
      assert(!s.children[1].classList.contains('core-selected'));
      assert(s.children[2].classList.contains('core-selected'));
      done();
    });
  });

});

google map component测试实例

  • 代码参考,f2e test
  • 1、安装 web-components-tester: npm install -g web-component-tester
  • 2 建立test文件夹(默认地址)
    • 2.1 index.html
    • 2.2 google-map-marker.html
    • 2.3 marker.js

suite('markers default', function () { var map; setup(function () { map = document.querySelector('#map'); }); test('markers are initialized', function () { var markerEl = Polymer.dom(map).querySelector('google-map-marker'); assert.isUndefined(markerEl.marker); assert.isUndefined(markerEl.map); assert.isNull(markerEl.info); assert.equal(markerEl.latitude, 37.779); assert.equal(markerEl.longitude, -122.3892); }); test('markers are added to map', function () { map.addEventListener('google-map-ready', function () { var mapMarkerEl = Polymer.dom(map).querySelector('google-map-marker'); var firstMarker = map.markers[0]; expect(firstMarker).to.deep.equal(mapMarkerEl); assert.equal(map.markers.length, 3); }); }); test('markers position can be updated', function (done) { map.addEventListener('google-map-ready', function (e) { var markerEl = Polymer.dom(map).querySelector('google-map-marker'); markerEl.latitude = 37.79493; markerEl.longitude = -122.41942; markerEl.zIndex = 1; assert.equal(markerEl.map, map.map, "marker's map is not the google-map's"); //重新渲染 异步过程 Polymer.dom.flush(); async.nextTick(function () { var marker = markerEl.marker; assert.equal(marker.getPosition().lat(), markerEl.latitude); assert.equal(marker.getPosition().lng(), markerEl.longitude); assert.equal(marker.getZIndex(), markerEl.zIndex); done(); }); }); }); });
  • 3 运行测试脚本 wct 即可。

wct运行机制

代码:runner
– 建立webserver,模板参见index.html
– 读取wct.conf.json配置
– 内置selenium server 通过wd.js建立连接打开浏览器进行测试
– 并将结果通过socketIO返回显示在命令行
– 测试框架核心:browser.js, 源码内置chai mocha sinon socket等以及polymer测试的辅助函数

辅助工具

在测试中少不了点击事件的模拟,wct这个工具不具有这个功能,但是可以使用polymer的工具组件iron-test-helpers
它内置了MockInteraction可以实现各个事件的模拟,只需import iron-test-helpers.html 即可。使用方法如下:

test('can be triggered with space', function(done) {
  button.addEventListener('keydown', function() {
    done();
  });
  MockInteractions.pressSpace(button);
});

test('can be clicked', function(done) {
  button.addEventListener('click', function() {
    done();
  });
  MockInteractions.tap(button);
});

Change LogLevel For MRAppMaster

Sometimes we want to show the debug log in MRAppMaster, there are two methods to do it. The first one is to change the mapred-site.xml in your gateway where you submit the job, add this conf.

<property>
<name>yarn.app.mapreduce.am.log.level</name>
<value>DEBUG</value>
</property>

The second one is to add config in the submit command like this

hadoop jar /usr/local/hadoop-2.4.0/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.4.0.jar terasort -Dmapred.reduce.tasks=50 -Dmapreduce.map.speculative=false -Dmapreduce.reduce.speculative=false -Dyarn.app.mapreduce.am.log.level=DEBUG /test/1001 /test/1001_SORT14

前端自动化测试基础-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 测试代码

DataXceiver本地读异常bug说明(HDFS-11802)

现象描述

用户在读取文件的时候报三台DN都无法取得该文件对应的block,经过fsck检查后没有发现该文件有丢块现象,到对应的dn上去查看日志,发现三台机器已经都处于不可读状态,报错为

2015-11-25 00:01:55,999 WARN org.apache.hadoop.hdfs.server.datanode.DataNode: 10.39.5.160:50010:DataXceiverServer:
java.io.IOException: Xceiver count 4097 exceeds the limit of concurrent xcievers: 4096
    at org.apache.hadoop.hdfs.server.datanode.DataXceiverServer.run(DataXceiverServer.java:137)
    at java.lang.Thread.run(Thread.java:745)

很明显,这是超过了dataxceiver设置的最大的线程数4096,正常情况下是不可能超过的,所以说明dn有线程泄露的bug或者其它问题。
再检查日志,发现了以下的日志

Exception in thread "Thread-19" java.lang.IllegalStateException: failed to remove c53ce04928d1baa854f5dc1bfc8d565b
    at com.google.common.base.Preconditions.checkState(Preconditions.java:145)
    at org.apache.hadoop.hdfs.server.datanode.ShortCircuitRegistry.removeShm(ShortCircuitRegistry.java:115)
    at org.apache.hadoop.hdfs.server.datanode.ShortCircuitRegistry$RegisteredShm.handle(ShortCircuitRegistry.java:102)
    at org.apache.hadoop.net.unix.DomainSocketWatcher.sendCallback(DomainSocketWatcher.java:371)
    at org.apache.hadoop.net.unix.DomainSocketWatcher.access$1000(DomainSocketWatcher.java:52)
    at org.apache.hadoop.net.unix.DomainSocketWatcher$1.run(DomainSocketWatcher.java:511)
    at java.lang.Thread.run(Thread.java:745)

看到这个日志以后就大概知道了原因,是由于DomainSocketWatcher线程异常退出,导致本地读线程没有回收机制,占满了所有的dataxceiver slot导致的。

问题原因

DomainSocketWatcher线程负责对本地读线程建立的socket进行一些处理和清理等。出问题的代码为

<code>
      try {
        while (true) {
              doSomecleanup.......
        }
      } catch (InterruptedException e) {
        LOG.info(toString() + " terminating on InterruptedException");
      } catch (IOException e) {
        LOG.error(toString() + " terminating on IOException", e);
      } finally {
        lock.lock();
        try {
          kick(); // allow the handler for notificationSockets[0] to read a byte
          Iterator<Entry> iter = entries.values().iterator();
          while(iter.hasNext()) {
            sendCallback("close", iter, fdSet);
          }
          entries.clear();
          fdSet.close();
        } finally {
          lock.unlock();
        }
      }
</code>

正常情况下代码不会走入到finally,而是一直在while中loop。而报出的异常则是remove一个共享内存对象的时候失败,而导致的运行时异常。
经过jira查询和我们集群中机器日志查看,发现原因为在于Client向DataNode申请本地读时候,DataNode建立共享内存对象以及File Descriptor出现异常,导致分配失败,日志为

2015-11-06 04:52:41,080 INFO org.apache.hadoop.hdfs.server.datanode.DataNode.clienttrace: cliID: DFSClient_attempt_1435099124107_5925361_m_000028_0_1777694543_1, src: 127.0.0.1, dest: 127.0.0.1, op: REQUEST_SHORT_CIRCUIT_SHM, shmId: n/a, srvID: 01f352c6-4e63-4158-8ead-3e8146103b6f, success: false

而在DataXceiver的requestShortCircuitShm代码中,如果失败则close连接

      if ((!success) && (peer == null)) {
        // If we failed to pass the shared memory segment to the client,
        // close the UNIX domain socket now.  This will trigger the 
        // DomainSocketWatcher callback, cleaning up the segment.
        IOUtils.cleanup(null, sock);
      }

但是,所有close操作都是通过回调DomainSocketWatcher来做的,这样,当DomainSocketWatcher再次close的时候内存中的共享内存对象由于已经close被释放,而报runtimeerror,这样DomainSocketWatcher线程异常退出,本地读没有清理线程,慢慢占满了slot,最后导致了DataNode不可用。

解决方法

DataXceiver不负责close连接,而只是负责将与client连接shutdown,使client能够快速反应读异常,同时增加更多的catch,如果后续还有异常能够找到原因。