Vue 的单元测试探索(二)

1,105 阅读4分钟
原文链接: zhuanlan.zhihu.com

对于 .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 组件的单元测试,而且更简单、更快速。