VUE(十三)-单元测试

315 阅读6分钟

一、三个概念

BDD:行为驱动开发,你的所有开发时为了满足用户的某个行为

TDD:测试驱动开发,把产品经理的需求翻译成测试

assert:断言

二、chai

window自带一个很弱的断言库,我们采用chai库

JavaScript 中好像有断言工具函数,叫 console.assert(参数1,参数2)。

参数1:是断言条件,即应该发生的情况;

参数2:是不满足断言条件时,打印出来的提示信息。

当时提问忘了放出问题描述中我练习的断言小例子,很简单的,此处补充下:

console.assert(a === 3, "a 的值不是3!");

下面三种断言都可以,前两种是BDD,后一种是TDD,体会区别

Should

chai.should();

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
tea.should.have.property('flavors')
  .with.lengthOf(3);
                

Expect

var expect = chai.expect;

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(tea).to.have.property('flavors')
  .with.lengthOf(3);
                

Assert

var assert = chai.assert;

assert.typeOf(foo, 'string');
assert.equal(foo, 'bar');
assert.lengthOf(foo, 3)
assert.property(tea, 'flavors');
assert.lengthOf(tea.flavors, 3);

import chai from 'chai'
import Button from "./button";
import spies from 'chai-spies'
chai.use(spies)
const expect = chai.expect
// 单元测试
{
  const Constructor = Vue.extend(Button)
  const vm = new Constructor({
    propsData: {
      icon: 'settings'
    }
  })
  vm.$mount() //动态的挂钩一个组件
  let useElement = vm.$el.querySelector('use')
  let href = useElement.getAttribute('xlink:href')
  expect(href).to.eq('#i-settings')
  vm.$el.remove()
  vm.$destroy()
}
{
  const Constructor = Vue.extend(Button)
  const vm = new Constructor({
    propsData: {
      icon: 'settings',
      loading: true
    }
  })
  vm.$mount()
  let useElement = vm.$el.querySelector('use')
  let href = useElement.getAttribute('xlink:href')
  expect(href).to.eq('#i-loading')
  vm.$el.remove()
  vm.$destroy()
}
{
  const div = document.createElement('div')
  document.body.appendChild(div)
  const Constructor = Vue.extend(Button)
  const vm = new Constructor({
    propsData: {
      icon: 'settings'
    }
  })
  vm.$mount(div)
  let svg = vm.$el.querySelector('svg')
  let {order} = window.getComputedStyle(svg)
  expect(order).to.eq('1')
  vm.$el.remove()
  vm.$destroy()
}
{
  const div = document.createElement('div')
  document.body.appendChild(div)
  const Constructor = Vue.extend(Button)
  const vm = new Constructor({
    propsData: {
      icon: 'settings',
      iconPosition: 'right'
    }
  })
  vm.$mount(div)
  let svg = vm.$el.querySelector('svg')
  let {order} = window.getComputedStyle(svg)
  expect(order).to.eq('2')
  vm.$el.remove()
  vm.$destroy()
}
{
  // mock
  const Constructor = Vue.extend(Button)
  const vm = new Constructor({
    propsData: {
      icon: 'settings',
    }
  })
  let spy = chai.spy(function(){})

  vm.$on('click', spy)
  // 希望这个函数被执行
  let button = vm.$el
  button.click()
  expect(spy).to.have.been.called()
}

 我们原本是直接new Vue({}).$mount()的现在我们new Constructor({}).$mount(),template里面的代码和button里面的代码完全是一个东西

单元测试就是你输入一个东西,你在输出力看是否匹配,匹配就是成功了

把上面的代码try...catch,把错误打出来

三、Karma自动化单元测试

  1. Karma([ˈkɑrmə] 卡玛)是一个测试运行器,它可以呼起浏览器,加载测试脚本,然后运行测试用例
  2. Mocha([ˈmoʊkə] 摩卡)是一个单元测试框架/库,它可以用来写测试用例
  3. Sinon(西农)是一个 spy / stub / mock 库,用以辅助测试(使用后才能理解)

我们可以用karma库运行npm run test:

打包我们的js,打开浏览器,运行网页,运行完网页后自动关闭浏览器,把浏览器输出显示到终端

  • 安装karma

npm i -D karma karma-chrome-launcher karma-mocha karma-sinon-chai mocha 
sinon sinon-chai karma-chai karma-chai-spies

  • 创建 karma 配置 // 新建 karma.conf.js//如下
  • module.exports = function(config) {
      config.set({
        // base path that will be used to resolve all patterns (eg. files, exclude)
        basePath: "",
        // frameworks to use
        // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
        frameworks: ["mocha", "sinon-chai"],
        client: {
          chai: {
            includeStack: true
          }
        },
    
        // 要在浏览器打开什么文件,dist下的所有js和css文件
        files: ["dist/**/*.test.js", "dist/**/*.test.css"],
    
        // list of files / patterns to exclude
        exclude: [],
    
        // preprocess matching files before serving them to the browser
        // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
        preprocessors: {},
    
        // test results reporter to use
        // possible values: 'dots', 'progress'
        // available reporters: https://npmjs.org/browse/keyword/karma-reporter
        reporters: ["progress"],
    
        // web server port
        port: 9876,
    
        // enable / disable colors in the output (reporters and logs)
        colors: true,
    
        // level of logging
        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_INFO,
    
        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: true,
    
         //打开哪个浏览器
        browsers: ["Chrome"],
    
        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: false,
    
        // Concurrency level
        // how many browser should be started simultaneous
        concurrency: Infinity
      });
    };
    

  • 创建 test/button.test.js 文件(一会下面会写)

  • 创建测试脚本在 package.json 里面找到 scripts 并改写 scripts

     "scripts": {
         "dev-test": "parcel watch test/* --no-cache & karma start",
         "test": "parcel build test/* --no-cache --no-minify && karma start --single-run"
     },

具体含义parcel build test/*,不缓存不压缩打包test下面的所有一级文件,打包到dist目录下,karma start --single-run"启动karma只运行一次

dev-test:每次代码修改后自动测试

  • 运行成功

dist\button.test.js.map    ‼  1.34 MB    319ms
dist\button.test.js         877.61 KB    7.58s
dist\button.test.css          1.12 KB    6.93s
15 04 2020 21:34:44.378:INFO [karma-server]: Karma v5.0.1 server started at http://0.0.0.0:9876/
15 04 2020 21:34:44.384:INFO [launcher]: Launching browsers ChromeHeadless with concurrency unlimited
15 04 2020 21:34:44.396:INFO [launcher]: Starting browser ChromeHeadless
15 04 2020 21:34:47.863:INFO [Chrome Headless 72.0.3626.121 (Windows 10)]: Connected on socket oAtefySifMYrIYfBAAAA with id 67609423
Chrome Headless 72.0.3626.121 (Windows 10): Executed 5 of 5 SUCCESS (0.003 secs / 0.013 secs)
TOTAL: 5 SUCCESS
  • 小bug,由于我的g-icon组件在全局注册(index.js),我在button组件里,都是直接用g-icon,所以测试button.test的时候没有可见的g-icon依赖,所以test失败,尽量改成局部注册组件

四、Mocha和Chai的单元测试

在刚才我们只用Chai做测试

{
  const Constructor = Vue.extend(Button)
  const vm = new Constructor({
    propsData: {
      icon: 'settings'
    }
  })
  vm.$mount() //动态的挂钩一个组件
  let useElement = vm.$el.querySelector('use')
  let href = useElement.getAttribute('xlink:href')
  expect(href).to.eq('#i-settings')
  vm.$el.remove()
  vm.$destroy()
}

重要两点

1.用块把每个测试隔开,防止变量冲突,chai是用{}分隔的,mocha用it函数隔开的,如下第一个参数是名字可读性比较好,第二个是代码

2. 写一个assert

describe("Button", () => {
      it("存在.", () => {
        expect(Button).to.be.ok;
      });
      it("可以设置icon.", () => {
        const Constructor = Vue.extend(Button);
        const vm = new Constructor({
          propsData: {
            icon: "setting"
          }
        }).$mount();
        const useElement = vm.$el.querySelector("use");
        expect(useElement.getAttribute("xlink:href")).to.equal("#i-setting");
        vm.$destroy();
      })
})

描述了一个Button,它有的各种东西,这就很BDD,行为驱动

 it("点击 button 触发 click 事件", () => {
  const Constructor = Vue.extend(Button);
   const vm = new Constructor({
     propsData: {
       icon: "setting"
     }
    }).$mount();

   const callback = sinon.fake();
   vm.$on("click", callback);
   vm.$el.click();
   expect(callback).to.have.been.called;
});

由于我们很难判断一个函数是否被调用,所以我们用sinon库来判断,sinon.fake()知道自己有没有被调用,和chai.spy()作用一样

chai的很多语法都是虚词,具体看文档,例如两个数组比较时分清楚深拷贝和浅拷贝

1. karma库,自动化

2. chai库,具体assert

3. mocha库,作用域隔离,BDD描述风格

4. sinon库,spy函数

五、TravisCI持续集成

我们一开始要自己打开 Chrome 测试我们的代码,后来使用 Karma 可以做到一行命令测试我们的代码。再自动化一点,就是持续集成-持续测试,持续交付,持续部署

Travis CI 提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 Github 上面的项目,只要有新的代码,就会自动抓取。然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器。

持续集成指的是只要代码有变更,就自动运行构建和测试,反馈运行结果。确保符合预期以后,再将新代码"集成"到主干。

持续集成的好处在于,每次代码的小幅变更,就能看到运行结果,从而不断累积小的变更,而不是在开发周期结束时,一下子合并一大块代码。

1. 创建.travis.yml

2. 

language: node_js
node_js:
  -"8"
addons:
  chrome: stable
sudo: required
before_script:
  -"sudo chown root /opt/google/chrome/chrome-sandbox"
  -"sudo chmod 4755 /opt/google/chrome/chrome-sandbox"

3. 每次push就会测试

六、npm

1. npm adduser

2. npm publish

3. 新开项目,npm install自己的包

4. x-link把包和项目连接起来,这样不用反复的下载install

使用过程中我们发现报错说 import 语法有问题,那时因为 node 目前确实不支持 import,我们需要用 babel 转译 import

  1. 你可以要求使用者自己用 babel 来转译
  2. 你也可以转义好了再给他用
  3. npx parcel build index.js --no-minify (本来不应该加 --no-minify 的,奈何不加会有 bug,HTML 压缩把 slot 标签全删了),打包到dist下
  4. index.js
    
    import Button from './src/button'
    import ButtonGroup from './src/button-group'
    import Icon from './src/icon'
    
    export {Button,ButtonGroup,Icon}

  5. 将 package.json 的 main 改为 dist/index.js,用户下载dist下的文件

 

使用 npm link 或者 yarn link 来加速调试你每次修改源码之后,有两条路让别人得到最新效果

1. 更新 package.json 里的 version,然后 npm publish。别人通过 npm update xxx 来更新。

2. 如果你只是为了本地调试,可以在项目目录使用 npm link,然后在使用之处运行 npm link xxx,就是最新了

七、约定

测试框架都有一个约定,不执行异步语句,默认把所有代码当成同步执行,或者加done参数