Vue单元测试

背景

从大的方面说前端应用本质上是一种特殊的GUI应用,GUI软件测试传送门。它的测试用例、覆盖率统计、测试方法等等都与API测试有着很大的不同。因此,在这个大前提下,我们来看前端测试的特殊性。

对于前端来说,业务基础代码比较稳定,比如通用组件、通用的工具类和通用的业务组件等,可以针对这些建立复杂一些的API和GUI测试用例来保证质量。剩下的部分不是很稳定,每天都在迭代,针对他们维护case的成本非常高。业界通用的方法其实就是我们看到的一般测试人员的点点。

我们如何保证基础代码的稳定及可靠呢?这就是我们要讨论的单元测试以及测试的覆盖率

单元测试

单元测试框架选择mocha,断言库选择chai,关于mochachai的介绍以及如何安装我就不科普啦!我们介绍下他俩的用法。

基本测试用例的编写

mocha的用法

describe('一组测试的描述', function() {
  before(function() {
    // 所有的测试用例执行前的钩子函数
  });

  after(function() {
    // 所有的测试用例执行后的钩子函数
  });

  beforeEach(function() {
    // 所有的测试用例执行前每次都会执行的钩子函数
  });

  afterEach(function() {
    // 所有的测试用例执行后每次都会执行的钩子函数
  });
  it('具体的测试用例',function () {

  })
}

chai的用法

var expect = require('chai').expect;
// 相等或不相等
expect(4 + 5).to.be.equal(9);
expect(4 + 5).to.be.not.equal(10);
expect(foo).to.be.deep.equal({ bar: 'baz' });

// 布尔值为true
expect('everthing').to.be.ok;
expect(false).to.not.be.ok;

// typeof
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
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');

// empty
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;

// match
expect('foobar').to.match(/^foo/);

下面我们通过简单的一个加法函数来举例说明

// 假设文件名为add.js
function add (a,b) {
   return a + b 
}

// 测试文件
const add = require('../src/add.js')
const expect = require('chai').expect
describe('add', function() {
  it('返回结果是数字', () => {
    expect(add(2,3)).to.be.a('number');
  })
  it('4+4返回结果是8', () => {
    expect(add(4,4)).to.equal(8)
  })
});

异步测试编写

其实异步是我们日常比较常见的场景,如果要测试异步函数,我们要传入的函数需要带一个参数,通常命名为done,测试异步函数需要在函数内部手动调用done()表示测试成功,done(err)表示测试出错。

it('异步测试', function (done) {
  fs.readFile('path', function (err, data) {
    if (err) {
      done(err);
    } else {
      done();
    }
  });
});
// promise
it('promise的测试', function(done) {
  return new Promise(function(resolve) {
    assert.ok(true);
    resolve();
  }).then(done);
});
// async await
it('async await', async function() {
  const users = await db.find({type: 'User'});
  users.should.have.length(3);
});

接口测试

SuperTest 是 SuperAgent 的一个扩展,一个轻量级的 HTTP AJAX 请求库。SuperTest 提供了用于测试 node.js API 终端响应的高阶抽象,使得断言便于理解。

describe('用户信息接口测试', function() {   
  it('/device api test',function(done) {
    request(app)
      .post('api/v2/user/create')
      .set('Content-Type','application/json')
      .set('Authorization', 'Bearer ' + accessToken)
      .send({
        user: username,
        password: password
      })
      .expect(200) //断言希望得到返回http状态码
      .end(function(err, res) {
        console.info(res.body); //得到返回我们可以用2.2中的断言对返回结果进行判断。
        done();
      });
  });
});

vue中测试用例的编写

我们就以一个button组件为例来说明吧。

const expect = require('chai').expect
import Vue from 'vue'
import Button from '../src/button'
describe('Button', () => {
  it('确定按钮存在', () => {
    const Constructor = Vue.extend(Button)
    const vm = new Constructor({
      propsData: {
        type: 'primary'
      }
    }).$mount()
    let buttonElm = vm.$el
    expect(buttonElm.classList.contains('el-button-primary')).to.be.true;
    vm.$destory()
  })

  it('可以设置icon', () => {
    const Constructor = Vue.extend(Button)
    const vm = new Constructor({
      propsData: {
        icon: 'settings'
      }
    }).$mount()
    let buttonElm = vm.$el
    expect(buttonElm.querySelector('.el-icon-settings')).to.be.ok;
      vm.$destory()
    })

  it('可以设置loading.', () => {
    const Constructor = Vue.extend(Button)
    const vm = new Constructor({
      propsData: {
        icon: 'settings',
        loading: true
      }
    }).$mount()
    let buttonElm = vm.$el
    expect(buttonElm.querySelector('.el-icon.loading')).to.be.ok
    vm.$destroy()
  })

  it('点击 button 触发 click 事件', () => {
    let result;
    vm = new Vue({
      template: `<el-button @click="handleClick"></el-button`,
      methods: {
        handleClick (evt) {
          result = evt;
        }
      }
    }).$mount()
    vm.$el.click()
    setTimeout(() => {
      expect(result).to.exist
      done()
    }, 20)
  })
})

测试环境的搭建

前面聊了如何做单元测试,以及介绍了如何写测试用例,如何根据我们的前端项目搭建一套测试框架呢?我们列出所要用到的技术。

  • karma 该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous integration)工具,也可和其他代码编辑器一起使用。
  • mocha 前端测试框架,支持node和浏览器端,断言库自由化,注意mocha只提供了测试套件,具体的断言工具还需要在挑选,这里我们选用chai
  • chai 断言库,提供了should,expect,assert多种断言。
  • istanbul javascript代码覆盖率工具,用来导出代码覆盖率数据。

需要安装的包名如下:

  • karma:提供测试所需的浏览器环境、监测代码改变自动重测、整合持续集成等功能
  • mocha:测试框架,运行测试
  • chai:断言库,提供多种断言,与测试框架配合使用
  • sinon:测试辅助工具,提供 spy、stub、mock 三种测试手段,帮助捏造特定场景
  • karma-webpack:karma 中的 webpack 插件
  • karma-mocha:karma 中的 mocha 插件
  • karma-sinon-chai:karma 中的 sinon-chai 插件
  • sinon-chai:karma 中的 chai 插件
  • karma-sourcemap-loader:karma 中的 sourcemap 插件
  • karma-chrome-launcher:karma 中的模拟chrom插件
  • karma-spec-reporter:在终端输出测试结果
  • babel-plugin-istanbul:babel插件,es6代码产生instanbul覆盖率
  • karma-coverage:Karma插件,生成代码覆盖率

目录结构

vue-template-test
  src
    components
      Button.vue
    utils
      utils.js
    test
      unit
        specs
          button.spec.js
        index.js
        karma.conf.js
  .babelrc
  package.json 在package.json配置测试运行的环境
"scripts": {
  "BABEL_ENV=test karma start test/unit/karma.conf.js --single-run"
}

karma.conf.js配置详解

module.exports = function(config) {
  const configuration = {
    // 设置默认打开的浏览器
    browsers: ['ChromeHeadless'],
    // 选择测试套件库和断言
    frameworks: ['mocha', 'sinon-chai'],
    // 设置测试覆盖率输出插件
    reporters: ['spec', 'coverage'],
    // 测试入口文件
    files: ['./index.js'],
    // 用webpack解析,同时显示测试文件路径
    preprocessors: {
      './index.js': ['webpack', 'sourcemap']
    },
    webpack: {
      module: {
        loaders: [
          {
            test: /\.js$/,
            loader: 'babel-loader',
            exclude: /node_modules/
          },
          {
            test: /\.vue$/,
            loaders: [{
              loader: 'vue-loader',
            }]
          },
        ]
      },
      // 是否打印webpack打包信息
      webpackMiddleware: {
        noInfo: true
      },
      // karma-coverage配置,配置测试覆盖率的输出目录及格式
      coverageReporter: {
        dir: './coverage',
        reporters: [
          { type: 'lcov', subdir: '.' },
          { type: 'text-summary' }
        ]
      },
      client: {
        mocha: {
          timeout: 4000
        }
      }
    }
  };
  config.set(configuration);
};

入口文件index.js的配置

// load 所有的测试用例文件
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

// load 所有的资源文件,及src目录下的除了main.js文件的所有文件, 即要覆盖的所有代码
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354