Open CommanderXL opened 6 years ago
最近将内部测试框架的底层库从mocha迁移到了AVA,迁移的原因之一是因为AVA提供了更好的流程控制。
mocha
AVA
我们从一个例子开始入手:
有A,B,C,D4个case,我要实现A -->> B -->> (C | D),A最先执行,B等待A执行完再执行,最后是(C | D)并发执行,使用ava提供的API来完成case就是:
A
B
C
D
A -->> B -->> (C | D)
(C | D)
ava
case
const ava = require('ava') ava.serial('A', async () => { // do something }) ava.serial('B', async () => { // do something }) ava('C', async () => { // do something }) ava('D', async () => { // do something })
接下来我们就来具体看下AVA内部是如何实现流程控制的:
在AVA内实现了一个Sequence类:
Sequence
class Sequence { constructor (runnables) { this.runnables = runnables } run() { // do something } }
这个Sequence类可以理解成集合的概念,这个集合内部包含的每一个元素可以是由一个case组成,也可以是由多个case组成。这个类的实例当中runnables属性(数组)保存了需要串行执行的case或case组。一个case可以当做一个组(runnables),多个case也可以当做一组,AVA用Sequence这个类来保证在runnables中保存的不同元素的顺序执行。
runnables
顺序执行了解后,我们再看下AVA内部实现的另外一个控制case并行执行的类:Concurrent:
Concurrent
class Concurrent { constructor (runnables) { this.runnables = runnables } run () { // do something } }
可以将Concurrent可以理解为组的概念,实例当中的runnables属性(数组)保存了这个组中所有待执行的case。这个Concurrent和上面提到的Sequence组都部署了run方法,用以runnables的执行,不同的地方在于,这个组内的case都是并行执行的。
run
具体到我们提供的实例当中:A -->> B -->> (C | D),AVA是如何从这2个类来实现他们之间的按序执行的呢?
在你定义case的时候:
ava.serial('A', async () => { // do something }) ava.serial('B', async () => { // do something }) ava('C', async () => { // do something }) ava('D', async () => { // do something })
在ava内部便会维护一个serial数组用以保存顺序执行的case,concurrent数组用以保存并行执行的case:
serial
concurrent
const serial = ['A', 'B']; const concurrent = ['C', 'D']
然后用这2个数组,分别实例化一个Sequence和Concurrent实例:
const serialTests = new Sequence(serial) const concurrentTests = new Concurrent(concurrent)
这样保证了serialTests内部的case是顺序执行的,concurrentTests内部的case是并行执行的。但是如何保证这2个实例(serialTests和concurrentTests)之间的顺序执行呢?即serialTests内部case顺序执行完后,再进行concurrentTests的并行执行。
serialTests
concurrentTests
同样是使用Sequence这个类,实例化一个Sequence实例:
const allTests = new Sequence([serialTests, concurrentTests])
之前我们就提到过Sequence实例的runnables属性中就维护了串行执行的case,所以在这里的具体体现就是,serialTests和concurrentTests之间是串行执行的,这也对应着:A -->> B -->> (C | D)。
接下来,我们就具体看下对应具体的流程实现:
allTests是所有这些case的集合,Sequence类上部署了run方法,因此调用:
allTests
allTests.run()
开始case的执行。在Sequence类的run方法当中:
class Sequence { constructor (runnables) { this.runnables = runnables } run () { // 首先获取runnables的迭代器对象,runnables数组保存了顺序执行的case const iterator = this.runnables[Symbol.iterator]() let activeRunnable // 定义runNext方法,主要是用于保证case执行的顺序 // 因为ava支持同步和异步的case,这里也着重分析下异步case的执行顺序 const runNext = () => { // 每次调用runNext方法都初始化一个新变量,用以保存异步case返回的promise let promise // 通过迭代器指针去遍历需要串行执行的case for (let next = iterator.next(); !next.done; next = iterator.next()) { // activeRunnable即每一个case或者是case的集合 activeRunnable = next.value // 调用case的run方法,或者case集合的run方法,如果activeRunnable是一个case,那么就会执行这个case,而如果是case集合,调用run方法后,还是对应于sequence的run方法 // 因此在调用allTests.run()的时候,第一个activeRunnable就是'A',‘B’2个case的集合(sequence实例)。 const passedOrPromise = activeRunnable.run() // passedOrPromise如果返回为false,即代表这个同步的case执行失败 if (!passedOrPromise) { // do something } else if (passedOrPromise !== true) { // !!!注意这里,如果passedOrPromise是个promise,那么会调用break来跳出这个for循环,进行到下面的步骤,这也是sequence类保证case顺序执行的关键。 promise = passedOrPromise break; } } if (!promise) { return this.finish() } // !!!通过then方法,保证上一个promise被resolve后(即case执行完后),再进行后面的步骤,如果then接受passed参数为真,那么继续调用runNext()方法。再次调用runNext方法后,通过迭代器访问的数组:iterator迭代器的内部指针就不会从这个数组的一开始的起始位置开始访问,而是从上一次for循环结束的地方开始。这样也就保证了异步case的顺序执行 return promise.then(passed => { if (!passed) { // do something } return runNext() }) } return runNext() } }
具体到我们提供的例子当中:
allTests这个Sequence实例的runnables属性保存了一个Sequence实例(A和B)和一个Concurrent实例(C和D)。
在调用allTests.run()后,在对allTesets的runnables的迭代器对象进行遍历的时候,首先调用包含A和B的Sequence实例的run方法,在run内部递归调用runNext方法,用以确保异步case的顺序执行。
allTesets
runNext
具体的实现主要还是使用了Promise迭代链来完成异步任务的顺序执行:每次进行异步case时,这个异步的case会返回一个promise,这个时候停止迭代器对象的遍历,而是通过在promise的then方法中递归调用runNext(),来保证顺序执行。
Promise
promise
then
runNext()
return promise.then(passed => { if (!passed) { // do something } return runNext() })
当A和B组成的Sequence执行完成后,才会继续执行由C和D组成的Conccurent,接下来我们看下并发执行case的内部实现:同样在Concurrent类上也部署了run方法,用以开始需要并发执行的case:
Conccurent
class Concurrent { constructor(runnables, bail) { if (!Array.isArray(runnables)) { throw new TypeError('Expected an array of runnables'); } this.runnables = runnables; } run () { // 所有的case是否通过 let allPassed = true; let pending; let rejectPending; let resolvePending; // 维护一个promise数组 const allPromises = []; const handlePromise = promise => { // 初始化一个pending的promise if (!pending) { pending = new Promise((resolve, reject) => { rejectPending = reject; resolvePending = resolve; }); } // 如果每个case都返回的是一个promise,那么首先调用then方法添加对于这个promise被resolve或者reject的处理函数,(这个添加被reject的处理,主要是用于下面Promise.all方法来处理所有被resolve的case)同时将这个promise推入到allPromises数组当中 allPromises.push(promise.then(passed => { if (!passed) { allPassed = false; if (this.bail) { // Stop if the test failed and bail mode is on. resolvePending(); } } }, rejectPending)); }; // 通过for循环遍历runnables中保存的case。 for (const runnable of this.runnables) { // 调用每个case的run方法 const passedOrPromise = runnable.run(); // 如果是同步的case,且执行失败了 if (!passedOrPromise) { if (this.bail) { // Stop if the test failed and bail mode is on. return false; } allPassed = false; } else if (passedOrPromise !== true) { // !!!如果返回的是一个promise handlePromise(passedOrPromise); } } if (pending) { // 使用Promise.all去处理allPromises当中的promise。当所有的promise被resolve后才会调用resolvePending,因为resolvePending对应于pending这个promise的resolve方法,也就是pending这个promise也被resolve,最后调用pending的then方法中添加的对于promise被resolve的方法。 Promise.all(allPromises).then(resolvePending); // 返回一个处于pending态的promise,但是它的then方法中添加了这个promise被resolve后的处理函数,即返回allPassed return pending.then(() => allPassed); } // 如果是同步的测试 return allPassed; } } }
具体到我们的例子当中:Concurrent实例的runnables属性中保存了C和D2个case,调用实例的run方法后,C和D2个case即开始并发执行,不同于Sequence内部通过iterator遍历器来实现的case的顺序执行,Concurrent内部直接只用for循环来启动case的执行,然后通过维护一个promise数组,并调用Promise.all来处理promise数组的状态。
iterator
for
Promise.all
以上就是通过一个简单的例子介绍了AVA内部的流程控制模型。简单的总结下:
在AVA内部使用Promise来进行整个的流程控制(这里指的异步的case)。
串行:
Sequence类来保证case的串行执行,在需要串行运行的case当中,调用Sequence实例的runNext方法开始case的执行,通过获取case数组的iterator对象来手动对case(或case的集合)进行遍历执行,因为每个异步的case内部都返回了一个promise,这个时候会跳出对iterator的遍历,通过在这个promise的then方法中递归调用runNext方法,这样就保证了case的串行执行。
iterator对象
case(或case的集合)
并行:
Concurrent类来保证case的并行执行,遇到需要并行运行的case时,同样是使用for循环,但是不是通过获取数组iterator迭代器对象去手动遍历,而是并发去执行,同时通过一个数组去收集这些并发执行的case返回的promise,最后通过Promise.all方法去处理这些未被resolve的promise,当然这里面也有一些小技巧,我在上面的分析中也指出了,这里不再赘述。
iterator迭代器
resolve
关于文中提到的Promise进行异步流程控制具体的应用,可以看下这2篇文章:
Promise 异步流程控制 《Node.js设计模式》基于ES2015+的回调控制流
我什么时候才能像你这样优秀,大佬
最近将内部测试框架的底层库从
mocha
迁移到了AVA
,迁移的原因之一是因为AVA
提供了更好的流程控制。我们从一个例子开始入手:
有
A
,B
,C
,D
4个case,我要实现A -->> B -->> (C | D)
,A
最先执行,B
等待A
执行完再执行,最后是(C | D)
并发执行,使用ava
提供的API来完成case
就是:接下来我们就来具体看下
AVA
内部是如何实现流程控制的:在
AVA
内实现了一个Sequence
类:这个
Sequence
类可以理解成集合的概念,这个集合内部包含的每一个元素可以是由一个case组成,也可以是由多个case组成。这个类的实例当中runnables
属性(数组)保存了需要串行执行的case或case组。一个case可以当做一个组(runnables
),多个case也可以当做一组,AVA
用Sequence
这个类来保证在runnables
中保存的不同元素的顺序执行。顺序执行了解后,我们再看下
AVA
内部实现的另外一个控制case
并行执行的类:Concurrent
:可以将
Concurrent
可以理解为组的概念,实例当中的runnables
属性(数组)保存了这个组中所有待执行的case
。这个Concurrent
和上面提到的Sequence
组都部署了run
方法,用以runnables
的执行,不同的地方在于,这个组内的case都是并行执行的。具体到我们提供的实例当中:
A -->> B -->> (C | D)
,AVA
是如何从这2个类来实现他们之间的按序执行的呢?在你定义case的时候:
在ava内部便会维护一个
serial
数组用以保存顺序执行的case,concurrent
数组用以保存并行执行的case:然后用这2个数组,分别实例化一个
Sequence
和Concurrent
实例:这样保证了
serialTests
内部的case
是顺序执行的,concurrentTests
内部的case
是并行执行的。但是如何保证这2个实例(serialTests
和concurrentTests
)之间的顺序执行呢?即serialTests
内部case
顺序执行完后,再进行concurrentTests
的并行执行。同样是使用
Sequence
这个类,实例化一个Sequence
实例:之前我们就提到过
Sequence
实例的runnables
属性中就维护了串行执行的case
,所以在这里的具体体现就是,serialTests
和concurrentTests
之间是串行执行的,这也对应着:A -->> B -->> (C | D)
。接下来,我们就具体看下对应具体的流程实现:
allTests
是所有这些case
的集合,Sequence
类上部署了run
方法,因此调用:开始
case
的执行。在Sequence
类的run
方法当中:具体到我们提供的例子当中:
allTests
这个Sequence
实例的runnables
属性保存了一个Sequence
实例(A
和B
)和一个Concurrent
实例(C
和D
)。在调用
allTests.run()
后,在对allTesets
的runnables的迭代器对象进行遍历的时候,首先调用包含A
和B
的Sequence
实例的run
方法,在run
内部递归调用runNext
方法,用以确保异步case的顺序执行。具体的实现主要还是使用了
Promise
迭代链来完成异步任务的顺序执行:每次进行异步case时,这个异步的case
会返回一个promise
,这个时候停止迭代器对象的遍历,而是通过在promise
的then
方法中递归调用runNext()
,来保证顺序执行。当A和B组成的
Sequence
执行完成后,才会继续执行由C和D组成的Conccurent
,接下来我们看下并发执行case的内部实现:同样在Concurrent
类上也部署了run
方法,用以开始需要并发执行的case:具体到我们的例子当中:
Concurrent
实例的runnables
属性中保存了C
和D
2个case
,调用实例的run
方法后,C
和D
2个case
即开始并发执行,不同于Sequence
内部通过iterator
遍历器来实现的case
的顺序执行,Concurrent
内部直接只用for
循环来启动case的执行,然后通过维护一个promise
数组,并调用Promise.all
来处理promise
数组的状态。以上就是通过一个简单的例子介绍了
AVA
内部的流程控制模型。简单的总结下:在
AVA
内部使用Promise
来进行整个的流程控制(这里指的异步的case)。串行:
Sequence
类来保证case
的串行执行,在需要串行运行的case
当中,调用Sequence
实例的runNext
方法开始case的执行,通过获取case
数组的iterator对象
来手动对case(或case的集合)
进行遍历执行,因为每个异步的case
内部都返回了一个promise
,这个时候会跳出对iterator
的遍历,通过在这个promise
的then
方法中递归调用runNext
方法,这样就保证了case
的串行执行。并行:
Concurrent
类来保证case
的并行执行,遇到需要并行运行的case
时,同样是使用for
循环,但是不是通过获取数组iterator迭代器
对象去手动遍历,而是并发去执行,同时通过一个数组去收集这些并发执行的case返回的promise
,最后通过Promise.all
方法去处理这些未被resolve
的promise
,当然这里面也有一些小技巧,我在上面的分析中也指出了,这里不再赘述。关于文中提到的Promise进行异步流程控制具体的应用,可以看下这2篇文章:
Promise 异步流程控制 《Node.js设计模式》基于ES2015+的回调控制流