Vue 单元测试中,对 watch 的思考。

引言

以下 watch 部分摘自 vue 官方文档

1
2
3
4
5
6
7
watch: {
// 如果 question 发生改变,这个函数就会运行
question: function (newQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.getAnswer()
}
}

第一个思路

如果要对这个模块进行测试,通常第一个想到的流程是

  • 新建这个 Vue 实例
  • 更改 question 的值,以触发这个回调函数

伪代码:

1
2
3
4
5
6
7
8
9
10
let vue = new Vue({
data: data,
watcher: {
data () {
// callback
}
}
})
vue.data = 'another value'
assertFunctionCall()

在实际编写测试用例的过程中遇到了问题,对数据的更改操作,是在下一轮事件循环中才会被 watch 发现,换言之,如果在同一个事件循环中,数据发生了多次变化,被检测到的只有这一轮事件循环里面的最后一次变化

而当测试框架没有做特别的处理,断言是不会像所预期的那样子工作,因为在 assertFunctionCall 的时候,watcherFunction 还没有被调用。

vue 官方提供了 $nextTick 函数,这个函数作用类似于 setTimeout,在下一轮事件循环的末尾加入一个任务,以下写法,断言是在 watch 函数调用之后执行的。

1
2
3
4
5
6
7
8
9
10
11
12
let vue = new Vue({
data: data,
watcher: {
data () {
// callback
}
}
})
vue.data = 'another value'
vue.$nextTick(() => {
assertFunctionCall()
})

这种写法的用例,如果没有使用 async/await 关键字,需要使用 Promise 或者 done 函数来让测试用例以异步函数的形式来运行(即下一个用例等待这个用例的断言结果)

另外一种思路

在 vue 的运行机制中,会根据构造函数中传入的 watch 对象建立一些 watcher 实例,

  • watcher 可以用过 expression 字段找到,如对 question 字段进行监听,则 expression 字段值为 ‘question’
  • watcher 有一个 run 方法,以下是去掉了注释的源码,在编写测试用例的时候,可以在改变数据以后,直接调用 watcher.run,然后断言回调函数有没有被正确调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Watcher.prototype.run = function run () {
if (this.active) {
var value = this.get();
if ( value !== this.value || isObject(value) || this.deep) {
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};

测试用例类似这样,如此便屏蔽了事件循环,done 函数等概念。

1
2
3
4
5
6
7
8
9
10
11
12
let vue = new Vue({
data: data,
watcher: {
data () {
// callback
}
}
})
vue.data = 'another value'
let watcher = getWatcherByExpression(vue, 'data')
watcher.run()
assertFunctionCall()