资讯专栏INFORMATION COLUMN

前端单元测试探索

陈江龙 / 1708人阅读

摘要:单元测试的首要目的不是为了能够编写出大覆盖率的全部通过的测试代码,而是需要从使用者调用者的角度出发,尝试函数逻辑的各种可能性,进而辅助性增强代码质量测试是手段而不是目的。

本文已发布在稀土掘金

转载请注明原文链接:https://github.com/ecmadao/Co...

虽然很多公司有自己的测试部门,而且前端开发大多不涉及测试环节,但鉴于目前前端领域的快速发展,其涉及面越来越广,前端开发者们必然不能止步于目前的状态。我觉得学会编写良好的测试,不仅仅有利于自己整理需求、检查代码,更是一个优秀开发者的体现。

首先不得不推荐两篇文章:

前端自动化测试探索

测试驱动开发(TDD)介绍中的误区

Intro

单元测试到底是什么?

需要访问数据库的测试不是单元测试

需要访问网络的测试不是单元测试

需要访问文件系统的测试不是单元测试

--- 修改代码的艺术

我们在单元测试中应该避免什么?

太多的条件逻辑

构造函数中做了太多事情

too many全局变量

too many静态方法

无关逻辑

过多外部依赖

TDD(Test-driven development)

测试驱动开发(TDD),其基本思路是通过测试来推动整个开发的进行。

单元测试的首要目的不是为了能够编写出大覆盖率的全部通过的测试代码,而是需要从使用者(调用者)的角度出发,尝试函数逻辑的各种可能性,进而辅助性增强代码质量

测试是手段而不是目的。测试的主要目的不是证明代码正确,而是帮助发现错误,包括低级的错误

测试要快。快速运行、快速编写

测试代码保持简洁

不会忽略失败的测试。一旦团队开始接受1个测试的构建失败,那么他们渐渐地适应2、3、4或者更多的失败。在这种情况下,测试集就不再起作用

IMPORTANT

一定不能误解了TDD的核心目的!

测试不是为了覆盖率和正确率

而是作为实例,告诉开发人员要编写什么代码

红灯(代码还不完善,测试挂)-> 绿灯(编写代码,测试通过)-> 重构(优化代码并保证测试通过)

大致过程

需求分析,思考实现。考虑如何“使用”产品代码,是一个实例方法还是一个类方法,是从构造函数传参还是从方法调用传参,方法的命名,返回值等。这时其实就是在做设计,而且设计以代码来体现。此时测试为红

实现代码让测试为绿

重构,然后重复测试

最终符合所有要求:

每个概念都被清晰的表达

Not Repeat Self

没有多余的东西

通过测试

BDD(Behavior-driven development)

行为驱动开发(BDD),重点是通过与利益相关者的讨论,取得对预期的软件行为的清醒认识,其重点在于沟通

大致过程

从业务的角度定义具体的,以及可衡量的目标

找到一种可以达到设定目标的、对业务最重要的那些功能的方法

然后像故事一样描述出一个个具体可执行的行为。其描述方法基于一些通用词汇,这些词汇具有准确无误的表达能力和一致的含义。例如,expect, should, assert

寻找合适语言及方法,对行为进行实现

测试人员检验产品运行结果是否符合预期行为。最大程度的交付出符合用户期望的产品,避免表达不一致带来的问题

测试的分类 & 测试工具 分类

API/Func UnitTest

测试不常变化的函数逻辑

测试前后端API接口

UI UnitTest

页面自动截图

页面DOM元素检查

跑通交互流程

工具

Mocha + Chai

PhantomJS or CasperJS or Nightwatch.js

selenium

with python

with js

mocha + chai的API/Func UnitTest

mocha是一套前端测试工具,我们可以拿它和其他测试工具搭配。

而chai则是BDD/TDD测试断言库,提供诸如expect这样的测试语法

initial

下面两篇文章值得一看:

Testing in ES6 with Mocha and Babel 6

Using Babel

setup
$ npm i mocha --save-dev
$ npm i chai --save-dev
Use with es6

babel 6+

$ npm install --save-dev babel-register
$ npm install babel-preset-es2015 --save-dev
// package.json
{
  "scripts": {
    "test": "./node_modules/mocha/bin/mocha --compilers js:babel-register"
  },
  "babel": {
    "presets": [
      "es2015"
    ]
  }
}

babel 5+

$ npm install --save-dev babel-core
// package.json
{
  "scripts": {
    "test": "./node_modules/mocha/bin/mocha --compilers js:babel-core/register"
  }
}
Use with coffeescript
$ npm install --save coffee-script
{
  "scripts": {
    "test": "./node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register"
  }
}
Use with es6+coffeescript

After done both...

{
  "scripts": {
    "test": "./node_modules/mocha/bin/mocha --compilers js:babel-core/register,coffee:coffee-script/register"
  }
}
# $ mocha
$ npm t
$ npm test
chai
import chai from "chai";

const assert = chai.assert;
const expect = chai.expect;
const should = chai.should();
foo.should.be.a("string");
foo.should.equal("bar");
list.should.have.length(3);
obj.should.have.property("name");

expect(foo).to.be.a("string");
expect(foo).to.equal("bar");
expect(list).to.have.length(3);
expect(obj).to.have.property("flavors");

assert.typeOf(foo, "string");
assert.equal(foo, "bar");
assert.lengthOf(list, 3);
assert.property(obj, "flavors");
Test

测试的一个基本思路是,自身从函数的调用者出发,对函数进行各种情况的调用,查看其容错程度、返回结果是否符合预期。

import chai from "chai";
const assert = chai.assert;
const expect = chai.expect;
const should = chai.should();

describe("describe a test", () => {

  it("should return true", () => {
      let example = true;
      // expect
      expect(example).not.to.equal(false);
      expect(example).to.equal(true);
      // should
      example.should.equal(true);
      example.should.be.a(boolen);
      [1, 2].should.have.length(2);
  });
  
  it("should check an object", () => {
    // 对于多层嵌套的Object而言..
    let nestedObj = {
        a: {
          b: 1
      }
    };
    let nestedObjCopy = Object.assign({}, nestedObj);
    nestedObj.a.b = 2;
    
    // do a function to change nestedObjCopy.a.b 
    expect(nestedObjCopy).to.deep.equal(nestedObj);
    expect(nestedObjCopy).to.have.property("a");
  });
});
AsynTest

Testing Asynchronous Code with MochaJS and ES7 async/await

mocha无法自动监听异步方法的完成,需要我们在完成之后手动调用done()方法

而如果要在回调之后使用异步测试语句,则需要使用try/catch进行捕获。成功则done(),失败则done(error)

// 普通的测试方法
it("should work", () =>{
  console.log("Synchronous test");
});
// 异步的测试方法
it("should work", (done) =>{
  setTimeout(() => {
    try {
        expect(1).not.to.equal(0);
        done(); // 成功
    } catch (err) {
        done(err); // 失败
    }
  }, 200);
});

异步测试有两种方法完结:done或者返回Promise。而通过返回Promise,则不再需要编写笨重的try/catch语句

it("Using a Promise that resolves successfully with wrong expectation!", function() {
    var testPromise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve("Hello World!");
        }, 200);
    });

    return testPromise.then(function(result){
        expect(result).to.equal("Hello!");
    });
});
mock

mock是一个接口模拟库,我们可以通过它来模拟代码中的一些异步操作

React单元测试 Test React Component

React组件无法直接通过上述方法进行测试,需要安装enzyme依赖。

$ npm i --save-dev enzyme
#
$ npm i --save-dev react-addons-test-utils

假设有这样一个组件:

// ...省略部分import代码
class TestComponent extends React.Component {
  constructor(props) {
    super(props);
    let {num} = props;
    this.state = {
      clickNum: num
    }
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    let {clickNum} = this.state;
    this.setState({
      clickNum: clickNum + 1
    });
  }

  render() {
    let {clickNum} = this.state;
    return (
      
{clickNum} 点我加1
) } }

使用样例:

import React from "react";
import {expect} from "chai";
import {shallow} from "enzyme";

import TestComponent from "../components/TestComponent";

describe("Test TestComponent", () => {
  // 创建一个虚拟的组件
  const wrapper = shallow(
      /
  );

  /* 
  * 之后,我们可以:
  * 通过wrapper.state()拿到组件的state
  * 通过wrapper.instance()拿到组件实例,以此调用组件内的方法
  * 通过wrapper.find()找到组件内的子组件
  * 但是,无法通过wrapper.props()拿到组件的props
  */

  // 测试该组件组外层的class
  it("should render with currect wrapper", () => {
    expect(wrapper.is(".test_component")).to.equal(true);
  });

  // 测试该组件初始化的state
  it("should render with currect state", () => {
    expect(wrapper.state()).to.deep.equal({
      clickNum: 10
    });
  });

  // 测试组件的方法
  it("should add one", () => {
    wrapper.instance().handleClick();
    expect(wrapper.state()).to.deep.equal({
      clickNum: 11
    });
  });
});
Test Redux

redux身为纯函数,非常便于mocha进行测试

// 测试actions
import * as ACTIONS from "../redux/actions";

describe("test actions", () => {
  it("should return an action to create a todo", () => {
    let expectedAction = {
        type: ACTIONS.NEW_TODO,
        todo: "this is a new todo"
    };
     expect(ACTIONS.addNewTodo("this is a new todo")).to.deep.equal(expectedAction);
  });
});
// 测试reducer
import * as REDUCERS from "../redux/reducers";
import * as ACTIONS from "../redux/actions";

describe("todos", () => {
  let todos = [];
  it("should add a new todo", () => {
      todos.push({
        todo: "new todo",
        complete: false
    });
    expect(REDUCERS.todos(todos, {
        type: ACTIONS.NEW_TODO,
        todo: "new todo"
    })).to.deep.equal([
      {
          todo: "new todo",
          complete: false
      }
    ]);
  });
});
// 还可以和store混用
import { createStore, applyMiddleware, combineReducers } from "redux";
import thunk from "redux-thunk";
import chai from "chai";
import thunkMiddleware from "redux-thunk";
import * as REDUCERS from "../redux/reducers";
import defaultState from "../redux/ConstValues";
import * as ACTIONS from "../redux/actions"

const appReducers = combineReducers(REDUCERS);
const AppStore = createStore(appReducers, defaultState, applyMiddleware(thunk));
let state = Object.assign({}, AppStore.getState());

// 一旦注册就会时刻监听state变化
const subscribeListener = (result, done) => {
  return AppStore.subscribe(() => {
    expect(AppStore.getState()).to.deep.equal(result);
    done();
  });
};

describe("use store in unittest", () => {
  it("should create a todo", (done) => {
    // 首先取得我们的期望值
      state.todos.append({
        todo: "new todo",
        complete: false
    });
    
    // 注册state监听
    let unsubscribe = subscribeListener(state, done);
    AppStore.dispatch(ACTIONS.addNewTodo("new todo"));
    // 结束之后取消监听
    unsubscribe();
  });
});
基于phantomjs和selenium的UI UnitTest

PhantomJS是一个基于webkit的服务器端JavaScript API,即相当于在内存中跑了个无界面的webkit内核的浏览器。通过它我们可以模拟页面加载,并获取到页面上的DOM元素,进行一系列的操作,以此来模拟UI测试。但缺点是无法实时看见页面上的情况(不过可以截图)。

Selenium是专门为Web应用程序编写的一个验收测试工具,它直接运行在浏览器中。Selenium测试通常会调起一个可见的界面,但也可以通过设置,让它以PhantomJS的形式进行无界面的测试。

open 某个 url

监听 onload 事件

事件完成后调用 sendEvent 之类的 api 去点击某个 DOM 元素所在 point

触发交互

根据 UI 交互情况 延时 setTimeout (规避惰加载组件点不到的情况)继续 sendEvent 之类的交互

Getting started with Selenium Webdriver for node.js

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80389.html

相关文章

  • 程序员,软件测试知多少?

    摘要:单元测试过后,机器状态保持不变。单元测试应该产生可重复一致的结果。然并卵都说国内很多程序员是不写单元测试的,甚至从来都不写,笔者当年做的时候也没写过几次捂脸。回归测试在单元测试的基础上,我们就能够建立关于这一模块的回归测试。 showImg(https://segmentfault.com/img/bVPMPd?w=463&h=312); 送给初级程序员的测试认知文 作为开发同学,一些...

    libxd 评论0 收藏0
  • 软件测试的概括及流程

    摘要:单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。随机测试随机测试是根据测试说明书执行用例测试的重要补充手段,是保证测试覆盖完整性的有效方式和过程。 一、什么是软件测试?         软甲测试就是用来确认一个程序的品质或性能是...

    darryrzhong 评论0 收藏0
  • 数据驱动模式借助react的实践探索

    摘要:之前谈到过很多次数据驱动的理解,这次通过实际项目检验了一下自己的想法。对于数据驱动这种模式,至少从数据层,可以规避,做一层数据变化的效验这个和写服务端单侧差不多。数据驱动和有点类似,只是借用在单页面上实现了。 之前谈到过很多次数据驱动的理解,这次通过实际项目检验了一下自己的想法。 相关文件 《前端数据驱动的价值》 《前端数据驱动的陷阱》 项目详设 详设的重要性 对于复杂一点的项目,...

    jone5679 评论0 收藏0
  • 关于前端接口测试探索和挖坑

    摘要:本文主要关注的是接口测试。所谓接口测试,就是检查系统提供的接口是否符合事先撰写的接口文档。作为接口测试的解决方案,我们必须具备通用性与易用性。 开始 最近几年,前端测试渐渐被人重视,相关的框架和方法已经比较成熟。断言库有should, expect, chai。 单元测试框架有mocha, jasmine, Qunit。 模拟浏览器测试环境有Phantomjs, Slimerjs。 集...

    Crazy_Coder 评论0 收藏0
  • 关于前端接口测试探索和挖坑

    摘要:本文主要关注的是接口测试。所谓接口测试,就是检查系统提供的接口是否符合事先撰写的接口文档。作为接口测试的解决方案,我们必须具备通用性与易用性。 开始 最近几年,前端测试渐渐被人重视,相关的框架和方法已经比较成熟。断言库有should, expect, chai。 单元测试框架有mocha, jasmine, Qunit。 模拟浏览器测试环境有Phantomjs, Slimerjs。 集...

    zxhaaa 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<