39

Vue项目的自动化测试

说到自动化测试,许多开发团队都是听说过、尝试过,但最后都止步于尝试,不能将TDD(测试驱动开发)、BDD(行为驱动开发)的完整流程贯彻到项目中。思考其中的原因:终究还是成本抵不上收益。

很多后端开发人员可能写过很多自动化的单元测试代码,但是对前端测试一头雾水。这是因为相对于后端开发人员的自动化单元测试,前端的自动化测试成本更高。

自动化测试就是通过自动化脚本将一个又一个测试用例串起来,每个测试用例都要模拟环境、模拟输入、然后断言输出。前端自动化最难的地方就是模拟环境、模拟输入和断言输出了!
我们可以试想一下现实中的使用场景:

模拟环境:首先前端代码是跑在不同的终端环境上的,纯粹的使用某台机子的运行环境进行模拟是无法发现真正存在的问题。所以我们的测试用例必须跑在真实的环境下,这里面包括不同的机器:Android、ios、pc、macbook;不同的系统:window10、window8、linux、mac;不同的运行载体:IE、safari、chrome、firefox、Opera、Android webview、UIWebview、WKWebview;不同网络环境:WiFi、4G、3G、offline

模拟输入:前端的输入不好模拟,在PC上有鼠标click,double Click、drag、mouseDown、mouseOver、input等等,在mobile上有swipe、tap、scroll、摇一摇、屏幕翻转等。相对于后端的单元测试,前端的输入种类繁多,每一种模拟起来都十分复杂,而且很多bug隐藏在几种连贯的输入之后才会复现。

断言输出:前端的断言不是简单的判断值是否相等,很多情况是即使值相等、效果完全不一样。
很多展示效果更是不能通过简单的断言来检测,比如区域是否能滑动,输入时键盘是否正确弹起等。

当你跨越千山万水把上面的问题解决了,测试用例写好了,功能代码写好,完美!然后UE跑过来和你说那个这根线往左边移动一像素的时候,你会瞬间崩溃。可能这一像素你很多测试用例都得重写。所以前端自动化测试的成本真不一定抵得上收益。

但是困难不代表解决不了,部分场景不适合不代表所有场景都不适合!

正因为面临这么多的困难,我们的前端社区开发出了很多工具帮我们解决这些问题。本章节主要是结合Vue这个框架介绍前端自动化测试的一些工具和方法。

我们使用vue-cli去新建一个vue的新项目,在这个项目中开启默认的unit tests和e2e tests

bogon:work xiaorenhui$ vue init webpack vueExample

? Project name vue-example
? Project description A Vue.js project
? Author kukuv <kukuv>
? Vue build standalone
? Install vue-router? No
? Use ESLint to lint your code? No
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? Yes

下面列举下这个新项目中涉及到的一些开源项目:

  • karma

    • Karma是一个基于Node.js的JavaScript测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous integration)工具,也可和其他代码编辑器一起使用。这个测试工具的一个强大特性就是,它可以监控(Watch)文件的变化,然后自行执行,通过console.log显示测试结果。
  • Mocha

    • mocha是一款功能丰富的javascript单元测试框架,它既可以运行在nodejs环境中,也可以运行在浏览器环境中。
  • Nightwatch

    • Nightwatch是一套基于Node.js的测试框架,使用Selenium WebDriver API以将Web应用测试自动化。它提供了简单的语法,支持使用JavaScript和CSS选择器,来编写运行在Selenium服务器上的端到端测试。
  • phantomjs

    • 一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。
  • sinon-chai

    • sinon-chai是 sinon和chai这两个断言库的结合,提供丰富的断言方法

很多人看到这么多新名词一定头晕,心想一个单元测试咋需要懂这么多东西。情况是上面只是单元测试框架的一小部分、还有许多框架没有列出来。正因为前端的自动化测试面临着许多问题,所以我们才有这么多的框架来帮忙解决问题。

unit tests

我们先来分析一下这个项目中的unit tests,这里面用到了 Karma、Mocha、sinon-chai、phantomjs。项目中已经有一个默认的单元测试例子。karma作为测试执行过程管理工具把Mocha、sinon-chai、phantomjs等框架组织起来。Mocha用来描述测试用例、sinon-chai用来断言、然后使用phamtomjs作为运行环境来跑测试用例。

npm install 将依赖的库都安装好,这里面phantomjs的依赖会比较难装,如果你之间没有安装过phantom,因为phantom比较大,而且加上国内的网络环境等原因。如果phantomjs装不上可以尝试使用chrome作为运行环境,这需要安装 "karma-chrome-launcher",需要修改配置文件。

然后 npm run unit 跑一下unit tests,如果提示权限问题就 使用sudo 来提升下权限。跑完后我们看一下目录结构

└── unit
    ├── coverage  代码覆盖率报告,src下面的index.html可以直接用浏览器打开
    │   ├── lcov-report
    │   │   ├── base.css
    │   │   ├── index.html
    │   │   ├── prettify.css
    │   │   ├── prettify.js
    │   │   ├── sort-arrow-sprite.png
    │   │   ├── sorter.js
    │   │   └── src
    │   │       ├── App.vue.html
    │   │       ├── components
    │   │       │   ├── Hello.vue.html
    │   │       │   └── index.html
    │   │       └── index.html
    │   └── lcov.info
    ├── index.js 运行测试用例前先加载的文件,方便统计代码覆盖率
    ├── karma.conf.js karma的配置文件
    └── specs 所有的测试用例都放在这里
        └── Hello.spec.js
// 加载所有的测试用例、 testsContext.keys().forEach(testsContext)这种写法是webpack中的加载目录下所有文件的写法

const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

// 加载所有代码文件,方便统计代码覆盖率
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)
config.set({
    // 在几个环境里跑你的测试用例
    // browsers: ['PhantomJS','Chrome'], 
    browsers: ['Chrome'],
    // 默认加载几个框架
    frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
    // 使用那些汇报框架
    reporters: ['spec', 'coverage'],
    // 预加载文件
    files: ['./index.js'],
    // 预处理
    preprocessors: {
      './index.js': ['webpack', 'sourcemap']
    },
    // webpack 配置
    webpack: webpackConfig,
    webpackMiddleware: {
      noInfo: true
    },
    // coverage 配置
    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    }
  })

上面使用的插件例如 mocha、spec、coverage除了karma默认自带的都需要你在npm
上安装对应的插件,例如以下

    "karma": "^1.4.1",
    "karma-chrome-launcher": "^2.2.0",
    "karma-coverage": "^1.1.1",
    "karma-mocha": "^1.3.0",
    "karma-phantomjs-launcher": "^1.0.2",
    "karma-phantomjs-shim": "^1.4.0",
    "karma-sinon-chai": "^1.3.1",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-spec-reporter": "0.0.31",
    "karma-webpack": "^2.0.2",
> vue-exampl@1.0.0 unit /Users/xiaorenhui/work/vueExample
> cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run

07 09 2017 12:08:13.004:INFO [karma]: Karma v1.7.0 server started at http://0.0.0.0:9876/
07 09 2017 12:08:13.007:INFO [launcher]: Launching browser Chrome with unlimited concurrency
07 09 2017 12:08:13.015:INFO [launcher]: Starting browser Chrome
07 09 2017 12:08:15.475:INFO [Chrome 60.0.3112 (Mac OS X 10.12.3)]: Connected on socket qDaxr51TuQCfQBcVAAAA with id 73077049
INFO LOG: 'Download the Vue Devtools extension for a better development experience:
https://github.com/vuejs/vue-devtools'
LOG LOG: 'data'

  Hello.vue
    ✓ should render correct contents

Chrome 60.0.3112 (Mac OS X 10.12.3): Executed 1 of 1 SUCCESS (0.024 secs / 0.011 secs)
TOTAL: 1 SUCCESS


=============================== Coverage summary ===============================
Statements   : 60% ( 3/5 )
Branches     : 50% ( 1/2 )
Functions    : 0% ( 0/1 )
Lines        : 60% ( 3/5 )
================================================================================

我修改了一下Hello.vue这个组件,可以看到coverage 里精确的显示了测试代码的覆盖率,下面是我做的修改

export default {
  name: 'hello',
  data () {
      console.log('data');
      function aa() {

      }
      if(false){
          console.log('data aa');
      }
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  methods:{
      aa(){
          console.log('methods aa');
      }
  }
}


打开reporter下面的index.html我们可以看到代码覆盖的具体情况。
点开Hello.vue更有直观的方式展示哪些代码被覆盖了,哪些没有。
图片描述

e2e测试

既然我们已经有了单元测试,那e2e测试有和单元测试有什么区别呢?Nightwatch是前端e2e测试的一个有代表性的框架。单元测试TDD的粒度很细,我们会为许多函数、方法去写单元测试,而e2e更接近BDD。直白点说,就是TDD的测试单元是一个个函数、方法,而BDD测试的单元是一个个预期的行为表现。e2e做的事情就是打开浏览器,并且真正的访问我们最终的页面,然后在这个真实的浏览器、真实的页面中我们去做各种断言,而单元测试不会要去我们去访问最终的页面,单元测试要保证的是一个个单元是没有问题的,但这些单元组合起来跑在页面上是否有问题,不是单元测试能够保证的,尤其是在前端这种模拟环境、模拟输入非常复杂的领域中,这是单元测试的短板,而e2e测试就是用来解决这些短板的。

我们来看看项目中使用Nightwatch来进行e2e测试的例子

首先看一下目录

├── e2e
│   ├── custom-assertions
│   │   └── elementCount.js 自定义的断言方法
│   ├── nightwatch.conf.js nightwatch的配置文件
│   ├── reports 
│   │   ├── CHROME_60.0.3112.101_Mac\ OS\ X_test.xml
│   │   └── CHROME_60.0.3112.113_Mac\ OS\ X_test.xml
│   ├── runner.js  bootstrap文件,起我们的页面server和nightwatch文件
│   └── specs
│       └── test.js 测试用例

图片描述
selenium是一个用java写的e2e测试工具集,它的API被纳入 w3c的webDriver Api中, nightWatch是对selenium的一个nodejs封装。所有我们需要再配置文件中配置selenium。

  src_folders: ['test/e2e/specs'],
  output_folder: 'test/e2e/reports',
  custom_assertions_path: ['test/e2e/custom-assertions'],
    // 对selenium的配置
  selenium: {
    start_process: true,
    server_path: require('selenium-server').path,
    host: '127.0.0.1',
    port: 4444,
    cli_args: {
      'webdriver.chrome.driver': require('chromedriver').path
    }
  },
    // 测试环境的配置
  test_settings: {
    default: {
      selenium_port: 4444,
      selenium_host: 'localhost',
      silent: true,
      globals: {
        devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
      }
    },

    chrome: {
      desiredCapabilities: {
        browserName: 'chrome',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    },

    firefox: {
      desiredCapabilities: {
        browserName: 'firefox',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    }
  }

下面的runner需要先起一个我们的网页服务然后再起nightWatch服务

var server = require('../../build/dev-server.js')

server.ready.then(() => {
  // 2. run the nightwatch test suite against it
  // to run in additional browsers:
  //    1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
  //    2. add it to the --env flag below
  // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
  // For more information on Nightwatch's config file, see
  // http://nightwatchjs.org/guide#settings-file
  var opts = process.argv.slice(2)
    console.log(opts);
  if (opts.indexOf('--config') === -1) {
    opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
  }
  if (opts.indexOf('--env') === -1) {
    opts = opts.concat(['--env', 'chrome,firefox'])
  }

  var spawn = require('cross-spawn')
  var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })

  runner.on('exit', function (code) {
    server.close()
    process.exit(code)
  })

  runner.on('error', function (err) {
    server.close()
    throw err
  })
})

sudo npm run e2e后

> node test/e2e/runner.js

> Starting dev server...

Starting to optimize CSS...
> Listening at http://localhost:8080

[]
Starting selenium server... started - PID:  74459

[Test] Test Suite
=====================

Running:  default e2e tests
 ✔ Element <#app> was visible after 81 milliseconds.
 ✔ Testing if element <.hello> is present.
 ✔ Testing if element <h1> contains text: "Welcome to Your Vue.js App".
 ✔ Testing if element <img> has count: 1

OK. 4 assertions passed. (3.951s)

在控制台上我们能看到各种断言的结果


kukuv2
1.1k 声望54 粉丝