对于 .vue 单文件组件的单元测试,一般的方法使用 Karma,配合 Webpack,headless browser,再加上你喜欢的测试框架(mocha、ava、jasmime、node-tap)。这个过程配置繁琐,而且需要 Webpack 把整个项目编译打包,再在 browser 上跑测试,执行过程也很慢。
  而我期望中的测试方法应该像 JS 单元测试一样,构造组件实例,调用组件方法或属性进行测试,比如这样一个组件:
// a.vue
<template>
<div>
<button @click="minus" class="minus">-</button>
<input :value="result" disabled />
<button @click="plus" class="plus">+</button>
</div>
</template>
<script>
export default {
data() {
return { result: 0 }
},
methods: {
minus() {
this.result--;
},
plus() {
this.result++;
}
}
};
</script>
<style></style>
  期望的测试方法是这样的:
const test = require('ava');
const Vue = require('vue');
let a = require('a.vue');
let vm = new Vue(a);?
test(t => {
t.is(vm.result, 0);
vm.plus();
vm.minus();
...
});
  那么如何实现?
  直接引用 .vue 文件会报错,因为 .vue 文件一般的结构包括 template、script、style 三部分,文件内容并不符合 JS 语法。
  const a = require('a.vue');
  <template>
  SyntaxError: Unexpected token <
  第一个问题,如何引入 .vue 文件?
  在这之前,需要先了解一下 Node 的模块加载机制。
  首先是 Module 构造函数,用于生成模块实例,其中模块 id 是文件路径,作为模块索引。
  // Module.js
  // 模块构造函数
  function Module(id, parent) {
  this.id = id;  // filename,路径,作为索引
  this.exports = {};
  ...
  }
  接着是 Module 两个比较重要的属性,后面会讲到,一个是模块缓存,缓存已加载的模块;另一个是模块扩展,预定义不同文件类型的加载方式。
  Module._cache = Object.create(null);  // 模块缓存
  Module._extensions = Object.create(null);
  后是 Module 加载模块用到的关键方法:
  Module.prototype._load
  Module.prototype._compile
  Module.prototype.require
  现在来看模块加载过程
  1. Module.prototype._load 过程
  Module.prototype._load = (request, parent, isMain) => {
  1. 检查 Module._cache 中有没有缓存;
  2. 如果没有, new Module(), 并缓存;
  3. 执行一些检查;
  4. 根据文件后缀类型, 执行 Module. _extensions 中预设方法;
  5. 预设方法会读取文件, 执行 Module.prototype._compile;
  6. 返回 module.exports
  };
  2. Module.prototype._compile 过程
  Module.prototype._compile = (content, filename) => {
  1. 构造 require 方法
  ?    // Module.prototype.require 是对 Module.prototype._load 简单包装
  require = Module.prototype.require;
  2. 为 require 方法附加属性:
  ...
  require.cache = Module._cache;?
  require.extensions = Module._extensions;
  3. 将模块包装为一个包装方法?
  (function(exports, require, module, __filename, __dirname) {
  // 模块内容
  })
  4. 运行方法
  };
  总结一下,基本的过程是这样的:
  1. 加载模块,检查缓存;
  Module.prototype._load();
  2. 根据预定义文件后缀处理方法,编译模块,将模块打包为包装方法,并传入参数(require, module, exports…)
  3. 执行包装方法,加载模块内的引用
  (function(exports, require, module, __filename, __dirname) {
  // 模块内容
  require(sth) -> Module.prototype._load()
  });
  4. 返回模块 module.exports
  回到第一个问题,怎么引入 .vue 文件?方法是在 require.extensions 中增加 .vue 文件的处理方法。
  require.extensions['.vue'] = function(module, filename) {
  var file = fs.readFileSync(filename, 'utf8');
  // 解析 .vue 文件内容,输出符合 JS 语法的字符串
  var script = extract(file);
  module._compile(script, filename);
  };
  第二个问题,怎么解析 .vue 文件,输出符合 JS 语法的字符串?
  .vue 单文件组件中,测试需要的是 template 和 script 两个部分,期望输出的是带 template 属性的 JS 对象字符串。解析方法可以使用正则,匹配标签,抽出 template 和 script 的内容,再进行拼接。
  这里有个小问题,template 内字符串抽出来了以后,怎么加到 JS 对象中?其实很简单,模块输出的内容本身是 JS 对象,给这个对象加一个 template 属性可以了:
var scriptStr = "module.exports = {
data() {
return {}
},
methods: ...
};"
var templateStr = "module.exports.template = " + JSON.stringify(templateStr) + ";"
var content = scriptStr + templateStr;
  其他可能遇到的问题:
  1. ES6 module,目前 Node 并没有实现 ES6 module,而代码中可能已经用上了,这个问题可以用 babel 转换。
  babel.transform(js, babelrc).code;
  2. Vue 引入,因为需要编译 template 部分,所以测试时需要引入完整版 Vue。
  const Vue = require('vue/dist/vue.common.js');
  3. Webpack resolve.alias,一般配置 Webpack 时会把几个常用目录做 alias 配置,而现在不经过 Webpack 预处理,所以 alias 相关路径引用会出现问题,可以使用 module-alias 这个库,在引入测试模块前修改关键路径,或者直接改写一下 Module._resolveFilename 方法。
  4. DOM 操作,可以使用 jsdom-global 简单模拟 DOM 结构和事件。
  require('jsdom-global')();
  问题解决,测试方法:
require('vue-tester'); // 使用上述方法写的工具,处理 .vue 文件?require(‘jsdom-global’)();?
const Vue = require('vue/dist/vue.common.js');?
const ava = require('ava');??
const a = require('a.vue').default;
?let vm = new Vue(a);
vm.$mount();
?test(t => {?
t.is(vm.result, 0);
vm.$el.querySelector('button.plus').click();
t.is(vm.result, 1);
vm.$el.querySelector('button.minus').click();
t.is(vm.result, 0);?});
});
  小结
  根据 Node 模块引用原理,预定义 .vue 文件读取规则,再通过正则方式将 .vue 文件内容解析为符合 JS 语法的字符串,拿到解析后的组件对象进行属性、方法等测试。这种方法已经可以完成一般 .vue 组件的单元测试,而且更简单、更快速。