前端单元测试入门1

1. 单元测试1.1 TDD 测试驱动开发TDD指的是Test Drive Development,很明显的意思是测试驱动开发,也就是说我们可以从测试的角度来检验整个项目。大概的流程是先针对每个功能点抽象出接口代码,然后编写单元测试代码,接下来实现接口,运行单元测试代码,循环此过程,直到整个单元测试都通过。1.2 BDD 行为驱动开发BDD指的是Behavior Drive Development,也就是行为驱动开发。在TDD中,我们并不能完全保证根据设计所编写的测试就是用户所期望的功能。BDD将这一部分简单和自然化,用自然语言来描述,让开发、测试、BA以及客户都能在这个基础上达成一致。2. 测试工具单元测试框架 mocha http://mochajs.org断言 chai http:/chaijs.com代理HTTP请求 supertest https://npmjs.com/package/supertest2.1 使用 mocha 完成一个简单的测试用例2.1.1 安装Chai允许开发人员选择最舒适的接口进行测试语句的编写。具有链式功能的BDD风格提供了富有表现力的语言和可读风格,如expect或者should;而TDD断言风格则提供了更为经典的感觉,如assert。 npm i mocha chai -S -D2.1.2 页面结构 2.1.3 add.jsfunction add(x, y) { return x + y;}module.exports = add;2.1.4 在测试文件mocha.js中先引入add方法和chai的expect方法var add=require('../src/add.js');var expect=require('chai').expect;2.1.5 然后书写测试脚本describe('add_test1',function(){ it('1+1=2',function(){ expect(add(1,1)).to.be.equal(2); }); it('1+1!=3',function(){ expect(add(1,1)).to.be.not.equal(3); })})上面这段代码,就是测试脚本,它可以独立执行。测试脚本里面应该包括一个或多个describe块,每个describe块应该包括一个或多个it块。 describe块称为"测试套件",表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称(“add_test1”),第二个参数是一个实际执行的函数。 it块称为"测试用例",表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称(“1+1=2”),第二个参数是一个实际执行的函数 2.1.6 在package.json的scripts中添加mochascripts: { "test": "mocha"}2.1.7 运行test文件npm run mocha运行了上述代码后,会自动运行 /test/*.test.js 文件,得到如下结果

September 8, 2019 · 1 min · jiezi

前端单元测试(未完。。)

安装测试用例基础describe块称为"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称(“加法函数的测试”),第二个参数是一个实际执行的函数。 describe钩子:describe('hooks', function() { before(function() { // 在本区块的所有测试用例之前执行 }); after(function() { // 在本区块的所有测试用例之后执行 }); beforeEach(function() { // 在本区块的每个测试用例之前执行 }); afterEach(function() { // 在本区块的每个测试用例之后执行 }); // test cases});it块称为"测试用例"(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称,第二个参数是一个实际执行的函数。describe(‘加法函数的测试’, function() { it(‘1 加 1 应该等于 2’, function() { expect(add(1, 1)).to.be.equal(2); });});

February 21, 2019 · 1 min · jiezi

react项目中使用mocha结合chai断言库进行单元测试

react项目中使用mocha结合chai断言库进行单元测试git地址:https://github.com/yancekang/…如果对你有所帮助,感谢start项目搭建create-react-app react-mocha-test 创建一个名称为 react-mocha-test 的react项目进入 react-mocha-test 安装 Mocha 为了操作的方便,请在全面环境也安装一下Mochanpm install -g mocha编写测试脚本1.进入src目录,新建tool.js文件存放我们的需要测试的函数,函数的具体作用这里就不需要解释了。function checkSex (idcard) { if (idcard === undefined || idcard === null) { return ‘男’ } if (parseInt(idcard.substr(16, 1)%2, 10) === 1) { return ‘男’ } else { return ‘女’ }}function add(x, y) { return x + y}function getParameterByName(name, url) { if (!url) url = window.location.href; /*eslint no-useless-escape: / name = name.replace(/[[]]/g, “\$&”); var regex = new RegExp("[?&]" + name + “(=([^&#])|&|#|$)”), results = regex.exec(url); if (!results) return null; if (!results[2]) return ‘’; return decodeURIComponent(results[2].replace(/+/g, " “));}module.exports = { checkSex, add, getParameterByName}2.在根目录中 test 中建立测试脚本文件,列如: index.js在文件中引入我们要测试的函数let {checkSex, add, getParameterByName} = require(’../src/tool/tool.js’)我们还用到了chai断言库,详情请了解 chai所谓"断言”,就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误。使用npm install chai安装通过let expect = require(‘chai’).expect 引入接下来我们就开始写断言测试,这里只写一种测试第一个函数checkSexdescribe(‘根据身份证号码验证用户性别’, function() { it(‘110101199003072615 男’, function() { expect(checkSex(‘110101199003072615’)).to.be.equal(‘男’) }) it(‘110101199003072156 男’, function() { expect(checkSex(‘110101199003072156’)).to.be.equal(‘男’) }) it(‘15010219900307442X 女’, function() { expect(checkSex(‘15010219900307442X’)).to.be.equal(‘女’) }) it(‘150102199003075385 女’, function() { expect(checkSex(‘150102199003075385’)).to.be.equal(‘女’) })})这里举例四种测试用例,根据身份证号码辨别该用户的性别和我们预期的是否一致。基本上,expect断言的写法都是一样的。头部是expect方法,尾部是断言方法,比如equal、a/an、ok、match等。两者之间使用to或to.be连接。如果expect断言不成立,就会抛出一个错误。事实上,只要不抛出错误,测试用例就算通过。在项目根目录执行npm test进行单元测试,可以看到测试结果测试通过测试异常 ...

February 21, 2019 · 1 min · jiezi

使用karma+mocha+chai+sinon+@vue/test-utils为你的组件库增加单元测试

项目github地址:https://github.com/yuanalina/installAsRequired这里必须要提前说明,前端项目的单元测试不是必须的,特别是业务型项目,增加单元测试反而会成为累赘,增加开发成本且无意义,业务型的项目需求常常变动,UI也经常更改,增加单元测试,需要在开发过程中不断更新开发测试用例,增加开发成本。但是,项目中的一些公共封装,比如公共的组件、公用的功能模块等是可以使用单元测试的。为什么组件库需要使用单元测试搭建完组件库的环境后,进入开发阶段,当开发完成组件,在说明文档中调试完毕后,到正式在项目中使用组件时,发现没有合适的项目或者说合适的方法去确保组件本身功能是没有问题了,再引用到项目中。毕竟组件是通过发布为npm包的形式为其他项目所使用,如果组件本身就有很多bug,那调试过程将是很繁琐的,需要不断的重复发布npm包,项目更新引用npm包,繁琐的操作浪费本就宝贵的开发时间。因此特为组件库引入单元测试,目的在于能减少组件的bug,避免重复的发布不必要的npm包。技术栈组件库单元测试,使用的各技术为:karma+mocha+chai+sinon+@vue/test-utils。下面做简单介绍,并贴上个人觉得简单有效的学习链接作为参考。karmakarma是一个测试运行器,为开发者提供高效的测试环境,主要作用是将项目运行在各种主流Web浏览器进行测试。关于karma的学习,建议看官方文档。组件库项目是通过vue-cli搭建的,项目生成时karma相关配置就已经设置好了,关于karma,可以先作为了解即可。mochamocha是一个测试框架,兼容多种断言库,mocha的学习可以看阮一峰老师的测试框架 Mocha 实例教程进行了解。chaichai是一个测试断言库,所谓断言,就是对组件做一些操作,并预言产生的结果。如果测试结果与断言相同则测试通过。chai的学习可以参阅Chai.js断言库API中文文档sinonsinon是一个测试工具,可以使用sinon来进行模拟http等异步请求操作,作为间谍监听回调函数调用等功能来帮助我们更轻松实现测试。sinon学习参阅:sinon入门,关于模拟http请求:利用SinonJS测试 AJAX 请求例子@vue/test-utils@vue/test-utils是vue官方推荐的vue测试工具,使用这个工具我们可以让我们更方便的测试vue项目。官方文档:vue-test-utils环境搭建在用vue-cli构建项目时,Set up unit test输入y(yes),Pick a test runner 选择Karma and mocha即可生成单元测试开发环境什么?你的项目生成时Set up unit test输入的是n(no)?别着急,跟着下面步骤来,搭建环境。1、首先安装全部单元测试需要的依赖npm i -D karma karma-webpack phantomjs-prebuilt karma-phantomjs-launcher karma-phantomjs-shim karma-chrome-launcher karma-sourcemap-loader mocha karma-mocha sinon chai sinon-chai karma-sinon-chai kbaocunrma-spec-reporter karma-coverage @vue/test-utils2、将vue-cli中关于单元测试的相关文件copy到项目相应位置3、修改package.json,增加单元测试启动命令"unit": “cross-env BABEL_ENV=test karma start test/unit/karma.conf.js –single-run"4、修改、增加chrome运行环境安装chrome相关依赖npm i -D chromedriver karma-chrome-launcher修改karma.conf.js文件到这里环境就搭建完毕了,在src/components目录中增加一个HelloWorld.vue,执行npm run unit命令就可以将单元测试跑起来啦目录结构:运行结果,看见一片飘绿就是成功了测试用例开发示例环境搭建完成就可以进行测试用例的开发了,这里以button组件为例示范测试用例如何开发在test/unit/specs目录中创建一个以.spec.js结尾的文件,在文件中引入需要测试的.vue文件即可运行结果:踩过的坑不得不说,从搭建单元测试到开发环境到完成测试用例开发,真的是踩坑无数。。。这里做一个小小汇总,希望当你开发中遇到类似问题能对你有所帮助,也作为我个人的一个记录1、karma.conf.js中的browsers参数需要改成Chrome,并安装chrome相关依赖;2、要测试的vue组件有依赖其他第三方插件,需要在@vue/test-utils中引入localVue,并将第三方插件注册到localVue中,mount挂载组件生成wrapper时,将localVue作为参数传递;3、要测试的组件引入element-ui,除了要在localVue中注册外,还需引入@vue/test-utils的config,并进行配置: config.stubs.transition = false config.stubs[’transition-group’] = false4、使用了element-ui的按钮等元素,绑定原生事件(比如点击事件)时,加上.native:@click.native=“click"5、有异步的内容,比如延时定时器,不要忘记done(),否则不会被捕获;还有很多不知为何会发生的错误,也许是我的打开方式不对?小伙伴们开发中有好的方法欢迎指正~~本文结束啦~希望对你有所帮助。。学无止境,与诸君共勉~~

January 21, 2019 · 1 min · jiezi

从0到1实现Promise

前言Promise大家一定都不陌生了,JavaScript异步流程从最初的Callback,到Promise,到Generator,再到目前使用最多的Async/Await(如果对于这些不熟悉的可以参考我另一篇文章《JavaScript异步编程》),这不仅仅是技术实现的发展,更是思想上对于如何控制异步的递进。Promise作为后续方案的基础,是重中之重,也是面试时候最常被问到的。今天我们就一起从0到1实现一个基于A+规范的Promise,过程中也会对Promise的异常处理,以及是否可手动终止做一些讨论,最后会对我们实现的Promise做单元测试。完整的代码已经上传到github,想直接看代码的可以点这里。虽然已经有很多带你实现Promise类的文章了,但每个人理解的程度不一样,也许不同的文章可以带给你不同的思考呢,那我们就开始吧。正文1. 基础框架new Promise()时接收一个executor函数作为参数,该函数会立即执行,函数中有两个参数,它们也是函数,分别是resolve和reject,函数同步执行一定要放在try…catch中,否则无法进行错误捕获。MyPromise.jsfunction MyPromise(executor) { function resolve(value) { } function reject(reason) { } try { executor(resolve, reject); } catch (reason) { reject(reason); }}module.exports = MyPromise;resolve()接收Promise成功值value,reject接收Promise失败原因reason。test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { resolve(123);})2. 添加状态机目前实现存在的问题:Promise是一个状态机的机制,初始状态为 pending,成功状态为 fulfilled,失败状态为 rejected。只能从 pending -> fulfilled,或者从 pending -> rejected,并且状态一旦转变,就永远不会再变了。所以,我们需要为Promise添加一个状态流转的机制。MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}module.exports = MyPromise;test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { resolve(123);});promise.then(function(value) { console.log(‘value’, value);}, function(reason) { console.log(‘reason’, reason);})3. 添加then方法Promise拥有一个then方法,接收两个函数 onFulfilled 和 onRejected,分别作为Promise成功和失败的回调。所以,在then方法中我们需要对状态state进行判断,如果是fulfilled,则执行onFulfilled(value)方法,如果是rejected,则执行onRejected(reason)方法。由于成功值value和失败原因reason是由用户在executor中通过resolve(value) 和 reject(reason)传入的,所以我们需要有一个全局的value和reason供后续方法获取。MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; self.value = null; self.reason = null; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; self.value = value; } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; self.reason = reason; } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}MyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; if (self.state === FULFILLED) { onFuifilled(self.value); } if (self.state === REJECTED) { onRejected(self.reason); }};module.exports = MyPromise;4. 实现异步调用resolve目前实现存在的问题:同步调用resolve()没有问题,但如果是异步调用,比如放到setTimeout中,因为目前的代码在调用then()方法时,state仍是pending状态,当timer到时候调用resolve()把state修改为fulfilled状态,但是onFulfilled()函数已经没有时机调用了。针对上述问题,进行如下修改:MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; self.value = null; self.reason = null; self.onFulfilledCallbacks = []; self.onRejectedCallbacks = []; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; self.value = value; self.onFulfilledCallbacks.forEach(function(fulfilledCallback) { fulfilledCallback(); }); } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; self.reason = reason; self.onRejectedCallbacks.forEach(function(rejectedCallback) { rejectedCallback(); }); } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}MyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { onFuifilled(self.value); }); self.onRejectedCallbacks.push(() => { onRejected(self.reason); }); } if (self.state === FULFILLED) { onFuifilled(self.value); } if (self.state === REJECTED) { onRejected(self.reason); }};module.exports = MyPromise;我们添加了两个回调函数数组onFulfilledCallbacks和onRejectedCallbacks,用来存储then()方法中传入的成功和失败回调。然后,当用户调用resolve()或reject()的时候,修改state状态,并从相应的回调数组中依次取出回调函数执行。同时,通过这种方式我们也实现了可以注册多个then()函数,并且在成功或者失败时按照注册顺序依次执行。test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { setTimeout(function() { resolve(123); }, 1000);});promise.then(function(value) { console.log(‘value1’, value);}, function(reason) { console.log(‘reason1’, reason);});promise.then(function(value) { console.log(‘value2’, value);}, function(reason) { console.log(‘reason2’, reason);});5. then返回的仍是Promise读过PromiseA+规范的同学肯定知道,then()方法返回的仍是一个Promise,并且返回Promise的resolve的值是上一个Promise的onFulfilled()函数或onRejected()函数的返回值。如果在上一个Promise的then()方法回调函数的执行过程中发生了错误,那么会将其捕获到,并作为返回的Promise的onRejected函数的参数传入。比如:let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value1’, value); return 456;}).then((value) => { console.log(‘value2’, value);});let promise = new Promise((resolve, reject) => { resolve(123);});打印结果为:value1 123 value2 456let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value1’, value); a.b = 2; // 这里存在语法错误 return 456;}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果为:value1 123 reason2 ReferenceError: a is not defined可以看到,then()方法回调函数如果发生错误,会被捕获到,那么then()返回的Promise会自动变为onRejected,执行onRejected()回调函数。let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { console.log(‘value1’, value); return 456;}, (reason) => { console.log(‘reason1’, reason); return 456;}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果为:reason1 123 value2 456好啦,接下来我们就去实现then()方法依然返回一个Promise。MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; let promise2 = null; promise2 = new MyPromise((resolve, reject) => { if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch(reason) { reject(reason); } }); self.onRejectedCallbacks.push(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch(reason) { reject(reason); } }); } if (self.state === FULFILLED) { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } } if (self.state === REJECTED) { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } } }); return promise2;};可以看到,我们新增了一个promise2作为then()方法的返回值。通过let x = onFuifilled(self.value) 或者 let x = onRejected(self.reason)拿到then()方法回调函数的返回值,然后调用self.resolvePromise(promise2, x, resolve, reject),将新增的promise2、x、promise2的resolve和reject传入到resolvePromise()中。所以,下面我们重点看一下resolvePromise()方法。MyPromise.jsMyPromise.prototype.resolvePromise = function(promise2, x, resolve, reject) { let self = this; let called = false; // called 防止多次调用 if (promise2 === x) { return reject(new TypeError(‘循环引用’)); } if (x !== null && (Object.prototype.toString.call(x) === ‘[object Object]’ || Object.prototype.toString.call(x) === ‘[object Function]’)) { // x是对象或者函数 try { let then = x.then; if (typeof then === ‘function’) { then.call(x, (y) => { // 别人的Promise的then方法可能设置了getter等,使用called防止多次调用then方法 if (called) return ; called = true; // 成功值y有可能还是promise或者是具有then方法等,再次resolvePromise,直到成功值为基本类型或者非thenable self.resolvePromise(promise2, y, resolve, reject); }, (reason) => { if (called) return ; called = true; reject(reason); }); } else { if (called) return ; called = true; resolve(x); } } catch (reason) { if (called) return ; called = true; reject(reason); } } else { // x是普通值,直接resolve resolve(x); }};resolvePromise()是用来解析then()回调函数中返回的仍是一个Promise,这个Promise有可能是我们自己的,有可能是别的库实现的,也有可能是一个具有then()方法的对象,所以这里靠resolvePromise()来实现统一处理。下面是翻译自PromiseA+规范关于resolvePromise()的要求:Promise 解决过程Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise 。这种 thenable 的特性使得 Promise 的实现更具有通用性:只要其暴露出一个遵循 Promise/A+ 协议的 then 方法即可;这同时也使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。运行 [[Resolve]](promise, x) 需遵循以下步骤:x 与 promise 相等如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promisex 为 Promise如果 x 为 Promise ,则使 promise 接受 x 的状态:- 如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝- 如果 x 处于执行态,用相同的值执行 promise- 如果 x 处于拒绝态,用相同的据因拒绝 promisex 为对象或函数如果 x 为对象或者函数:- 把 x.then 赋值给 then- 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise- 如果 then 是函数,将 x 作为函数的作用域 this 调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise: - 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y) - 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise - 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用 - 如果调用 then 方法抛出了异常 e: - 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之 - 否则以 e 为据因拒绝 promise - 如果 then 不是函数,以 x 为参数执行 promise- 如果 x 不为对象或者函数,以 x 为参数执行 promise如果一个 promise 被一个循环的 thenable 链中的对象解决,而 [[Resolve]](promise, thenable) 的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的 TypeError 为据因来拒绝 promise。参考上述规范,结合代码中的注释,相信大家可以理解resolvePromise()的作用了。测试:test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { setTimeout(function() { resolve(123); }, 1000);});promise.then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(456); }).then((value) => { return new MyPromise((resolve, reject) => { resolve(789); }) });}, (reason) => { console.log(‘reason1’, reason);}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果:value1 123 value2 7896. 让then()方法的回调函数总是异步调用官方Promise实现的回调函数总是异步调用的:console.log(‘start’);let promise = new Promise((resolve, reject) => { console.log(‘step-’); resolve(123);});promise.then((value) => { console.log(‘step–’); console.log(‘value’, value);});console.log(’end’);打印结果:start step- end step– value1 123Promise属于微任务,这里我们为了方便用宏任务setTiemout来代替实现异步,具体关于宏任务、微任务以及Event Loop可以参考我的另一篇文章带你彻底弄懂Event Loop。MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; let promise2 = null; promise2 = new MyPromise((resolve, reject) => { if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { setTimeout(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); }); self.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); }); } if (self.state === FULFILLED) { setTimeout(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); } if (self.state === REJECTED) { setTimeout(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); } }); return promise2;};测试:test.jslet MyPromise = require(’./MyPromise.js’);console.log(‘start’);let promise = new MyPromise((resolve, reject) => { console.log(‘step-’); setTimeout(() => { resolve(123); }, 1000);});promise.then((value) => { console.log(‘step–’); console.log(‘value’, value);});console.log(’end’);打印结果:start step- end step– value1 123经过以上步骤,一个最基本的Promise就已经实现完了,下面我们会实现一些不在PromiseA+规范的扩展方法。7. 实现catch()方法then()方法的onFulfilled和onRejected回调函数都不是必传项,如果不传,那么我们就无法接收reject(reason)中的错误,这时我们可以通过链式调用catch()方法用来接收错误。举例:let promise = new Promise((resolve, reject) => { reject(‘has error’);});promise.then((value) => { console.log(‘value’, value);}).catch((reason) => { console.log(‘reason’, reason);});打印结果:reason has error不仅如此,catch()可以作为Promise链式调用的最后一步,前面Promise发生的错误会冒泡到最后一个catch()中,从而捕获异常。举例:let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}).catch((reason) => { console.log(‘reason’, reason);});打印结果:reason has error reason has error1那么catch()方法到底是如何实现的呢?答案就是在Promise的实现中,onFulfilled和onRejected函数是有默认值的:MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { onFuifilled = typeof onFuifilled === ‘function’ ? onFuifilled : value => {return value;}; onRejected = typeof onRejected === ‘function’ ? onRejected : reason => {throw reason};};MyPromise.prototype.catch = function(onRejected) { return this.then(null, onRejected);};可以看到,onRejected的默认值是把错误reason通过throw抛出去。由于我们对于同步代码的执行都是在try…catch中的,所以如果Promise发生了错误,如果没传onRejected,默认的函数会把错误reason抛出,然后会被promise2捕捉到,作为reject(reason)决议。catch()实现就是调用this.then(null, onRejected),由于promise2被reject,所以会执行onRejected回调,于是就捕捉到了第一个promise的错误。总结来说,then()方法中不传onRejected回调,Promise内部会默认帮你写一个函数作为回调,作用就是throw抛出reject或者try…catch到的错误,然后错误reason会被promise2作为reject(reason)进行决议,于是会被下一个then()方法的onRejected回调函数调用,而catch只是写了一个特殊的then(null, onRejected)而已。所以,我们在写Promise的链式调用的时候,在then()中可以不传onRejected回调,只需要在链式调用的最末尾加一个catch()就可以了,这样在该链条中的Promise发生的错误都会被最后的catch捕获到。举例1:let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { // 注意,不会走这里,因为第一个promise是被reject的 console.log(‘value1’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value2’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}, (reason) => { // 注意,这个then有onRejected回调 console.log(‘reason2’, reason);}).catch((reason) => { // 错误在上一个then就被捕获了,所以不会走到这里 console.log(‘reason3’, reason);});打印结果:reason2 123举例2:let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { console.log(‘value1’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value2’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}).catch((reason) => { // 由于链条中的then都没有onRejected回调,所以会一直被冒泡到最后的catch这里 console.log(‘reason3’, reason);});catch和then一样都是返回一个新的Promise。有的同学可能会有疑问,如果catch中的回调执行也发生错误该怎么办呢,这个我们后续在Promise异常处理中再做讨论。打印结果:reason3 1238. 实现finally方法finally是某些库对Promise实现的一个扩展方法,无论是resolve还是reject,都会走finally方法。MyPromise.jsMyPromise.prototype.finally = function(fn) { return this.then(value => { fn(); return value; }, reason => { fn(); throw reason; });};9. 实现done方法done方法作为Promise链式调用的最后一步,用来向全局抛出没有被Promise内部捕获的错误,并且不再返回一个Promise。一般用来结束一个Promise链。MyPromise.jsMyPromise.prototype.done = function() { this.catch(reason => { console.log(‘done’, reason); throw reason; });};10. 实现Promise.all方法Promise.all()接收一个包含多个Promise的数组,当所有Promise均为fulfilled状态时,返回一个结果数组,数组中结果的顺序和传入的Promise顺序一一对应。如果有一个Promise为rejected状态,则整个Promise.all为rejected。MyPromise.jsMyPromise.all = function(promiseArr) { return new MyPromise((resolve, reject) => { let result = []; promiseArr.forEach((promise, index) => { promise.then((value) => { result[index] = value; if (result.length === promiseArr.length) { resolve(result); } }, reject); }); });};test.jslet MyPromise = require(’./MyPromise.js’);let promise1 = new MyPromise((resolve, reject) => { console.log(‘aaaa’); setTimeout(() => { resolve(1111); console.log(1111); }, 1000);});let promise2 = new MyPromise((resolve, reject) => { console.log(‘bbbb’); setTimeout(() => { reject(2222); console.log(2222); }, 2000);});let promise3 = new MyPromise((resolve, reject) => { console.log(‘cccc’); setTimeout(() => { resolve(3333); console.log(3333); }, 3000);});Promise.all([promise1, promise2, promise3]).then((value) => { console.log(‘all value’, value);}, (reason) => { console.log(‘all reason’, reason);})打印结果:aaaa bbbb cccc 1111 2222 all reason 2222 333311. 实现Promise.reace方法Promise.race()接收一个包含多个Promise的数组,当有一个Promise为fulfilled状态时,整个大的Promise为onfulfilled,并执行onFulfilled回调函数。如果有一个Promise为rejected状态,则整个Promise.race为rejected。MyPromise.jsMyPromise.race = function(promiseArr) { return new MyPromise((resolve, reject) => { promiseArr.forEach(promise => { promise.then((value) => { resolve(value); }, reject); }); });};test.jslet MyPromise = require(’./MyPromise.js’);let promise1 = new MyPromise((resolve, reject) => { console.log(‘aaaa’); setTimeout(() => { resolve(1111); console.log(1111); }, 1000);});let promise2 = new MyPromise((resolve, reject) => { console.log(‘bbbb’); setTimeout(() => { reject(2222); console.log(2222); }, 2000);});let promise3 = new MyPromise((resolve, reject) => { console.log(‘cccc’); setTimeout(() => { resolve(3333); console.log(3333); }, 3000);});Promise.all([promise1, promise2, promise3]).then((value) => { console.log(‘all value’, value);}, (reason) => { console.log(‘all reason’, reason);})打印结果:aaaa bbbb cccc 1111 all reason 1111 2222 333312. 实现Promise.resolve方法Promise.resolve用来生成一个fulfilled完成态的Promise,一般放在整个Promise链的开头,用来开始一个Promise链。MyPromise.jsMyPromise.resolve = function(value) { let promise; promise = new MyPromise((resolve, reject) => { this.prototype.resolvePromise(promise, value, resolve, reject); }); return promise;};test.jslet MyPromise = require(’./MyPromise.js’);MyPromise.resolve(1111).then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(2222); })}).then((value) => { console.log(‘value2’, value);})打印结果:value1 1111 value2 2222由于传入的value有可能是普通值,有可能是thenable,也有可能是另一个Promise,所以调用resolvePromise进行解析。12. 实现Promise.reject方法Promise.reject用来生成一个rejected失败态的Promise。MyPromise.jsMyPromise.reject = function(reason) { return new MyPromise((resolve, reject) => { reject(reason); });};test.jslet MyPromise = require(’./MyPromise.js’);MyPromise.reject(1111).then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(2222); })}).then((value) => { console.log(‘value2’, value);}).catch(reason => { console.log(‘reason’, reason);});打印结果:reason 111113. 实现Promise.deferred方法Promise.deferred可以用来延迟执行resolve和reject。MyPromise.jsMyPromise.deferred = function() { let dfd = {}; dfd.promies = new MyPromise((resolve, reject) => { dfd.resolve = resolve; dfd.rfeject = reject; }); return dfd;};这样,你就可以在外部通过调用dfd.resolve()和dfd.reject()来决议该Promise。13. 如何停止一个Promise链假设这样一个场景,我们有一个很长的Promise链式调用,这些Promise是依次依赖的关系,如果链条中的某个Promise出错了,就不需要再向下执行了,默认情况下,我们是无法实现这个需求的,因为Promise无论是then还是catch都会返回一个Promise,都会继续向下执行then或catch。举例:new Promise(function(resolve, reject) { resolve(1111)}).then(function(value) { // “ERROR!!!”}).catch() .then() .then() .catch() .then()有没有办法让这个链式调用在ERROR!!!的后面就停掉,完全不去执行链式调用后面所有回调函数呢?我们自己封装一个Promise.stop方法。MyPromise.jsMyPromise.stop = function() { return new Promise(function() {});};stop中返回一个永远不执行resolve或者reject的Promise,那么这个Promise永远处于pending状态,所以永远也不会向下执行then或catch了。这样我们就停止了一个Promise链。new MyPromise(function(resolve, reject) { resolve(1111)}).then(function(value) { // “ERROR!!!” MyPromise.stop();}).catch() .then() .then() .catch() .then()但是这样会有一个缺点,就是链式调用后面的所有回调函数都无法被垃圾回收器回收。14. 如何解决Promise链上返回的最后一个Promise出现错误看如下例子:new Promise(function(resolve) { resolve(42)}).then(function(value) { a.b = 2;});这里a不存在,所以给a.b赋值是一个语法错误,onFulfilled回调函数是包在try…catch中执行的,错误会被catch到,但是由于后面没有then或catch了,这个错误无法被处理,就会被Promise吃掉,没有任何异常,这就是常说的Promise有可能会吃掉错误。那么我们怎么处理这种情况呢?方法一就是我们前面已经实现过的done()。new Promise(function(resolve) { resolve(42)}).then(function(value) { a.b = 2;}).done();done()方法相当于一个catch,但是却不再返回Promise了,注意done()方法中不能出现语法错误,否则又无法捕获了。方法二普通错误监听window的error事件可以实现捕获window.addEventListener(’error’, error => { console.log(error); // 不会触发});Promise没有被onRejected()处理的错误需要监听unhandledrejection事件window.addEventListener(‘unhandledrejection’, error => { console.log(‘unhandledrejection’, error); // 可以触发,而且还可以直接拿到 promise 对象});14. 单元测试结束相关单元测试以及完整代码可以到我的github查看,如果对你有帮助的话,就来个star吧~参考文档PromiseA+规范 ...

September 29, 2018 · 8 min · jiezi

使用 TypeScript 改造构建工具及测试用例

最近的一段时间一直在搞TypeScript,一个巨硬出品、赋予JavaScript语言静态类型和编译的语言。 第一个完全使用TypeScript重构的纯Node.js项目已经上线并稳定运行了。 第二个前后端的项目目前也在重构中,关于前端基于webpack的TypeScript套路之前也有提到过:TypeScript在react项目中的实践。 但是这些做完以后也总感觉缺了点儿什么 (没有尽兴):是的,依然有五分之一的JavaScript代码存在于项目中,作为一个TypeScript的示例项目,表现的很不纯粹。 所以有没有可能将这些JavaScript代码也换成TypeScript呢? 答案肯定是有的,首先需要分析这些代码都是什么:Webpack打包时的配置文件一些简单的测试用例(使用的mocha和chai)知道了是哪些地方还在使用JavaScript,这件事儿就变得很好解决了,从构建工具(Webpack)开始,逐个击破,将这些全部替换为TypeScript。Webpack 的 TypeScript 实现版本在这8102年,很幸福,Webpack官方已经支持了TypeScript编写配置文件,文档地址。 除了TypeScript以外还支持JSX和CoffeeScript的解释器,在这就忽略它们的存在了依赖的安装首先是要安装TypeScript相关的一套各种依赖,包括解释器及该语言的核心模块:npm install -D typescript ts-nodetypescript为这个语言的核心模块,ts-node用于直接执行.ts文件,而不需要像tsc那样会编译输出.js文件。ts-node helloworld.ts因为要在TypeScript环境下使用Webpack相关的东东,所以要安装对应的types。 也就是Webpack所对应的那些*.d.ts,用来告诉TypeScript这是个什么对象,提供什么方法。npm i -D @types/webpack一些常用的pLugin都会有对应的@types文件,可以简单的通过npm info @types/XXX来检查是否存在 如果是一些小众的plugin,则可能需要自己创建对应的d.ts文件,例如我们一直在用的qiniu-webpack-plugin,这个就没有对应的@types包的,所以就自己创建一个空文件来告诉TypeScript这是个啥:declare module ‘qiniu-webpack-plugin’ // 就一个简单的定义即可// 如果还有其他的包,直接放到同一个文件就行了// 文件名也没有要求,保证是 d.ts 结尾即可放置的位置没有什么限制,随便丢,一般建议放到types文件夹下 最后就是.ts文件在执行时的一些配置文件设置。 用来执行Webpack的.ts文件对tsconfig.json有一些小小的要求。 compilerOptions下的target选项必须是es5,这个代表着输出的格式。 以及module要求选择commonjs。{ “compilerOptions”: { “module”: “commonjs”, “target”: “es5”, “esModuleInterop”: true }}但一般来讲,执行Webpack的同级目录都已经存在了tsconfig.json,用于实际的前端代码编译,很可能两个配置文件的参数并不一样。 如果因为要使用Webpack去修改真正的代码配置参数肯定是不可取的。 所以我们就会用到这么一个包,用来改变ts-node执行时所依赖的配置文件:tsconfig-paths 在Readme中发现了这样的说法:If process.env.TS_NODE_PROJECT is set it will be used to resolved tsconfig.json。 在Webpack的文档中同样也提到了这句,所以这是一个兼容的方法,在命令运行时指定一个路径,在不影响原有配置的情况下创建一个供Webpack打包时使用的配置。将上述的配置文件改名为其它名称,Webpack文档示例中为tsconfig-for-webpack-config.json,这里就直接沿用了然后添加npm script如下{ “scripts”: { “build”: “TS_NODE_PROJECT=tsconfig-for-webpack-config.json webpack –config configs.ts” }}文件的编写关于配置文件,从JavaScript切换到TypeScript实际上并不会有太大的改动,因为Webpack的配置文件大多都是写死的文本/常量。 很多类型都是自动生成的,基本可以不用手动指定,一个简单的示例:import { Configuration } from ‘webpack’const config: Configuration = { mode: process.env.NODE_ENV === ‘production’ ? ‘production’ : ‘development’,}export default configConfiguration是一个Webpack定义的接口(interface),用来规范一个对象的行为。 在VS Code下按住Command + 单击可以直接跳转到具体的webpack.d.ts定义文件那里,可以看到详细的定义信息。 各种常用的规则都写在了这里,使用TypeScript的一个好处就是,当要实现一个功能时你不再需要去网站上查询应该要配置什么,可以直接翻看d.ts的定义。 如果注释写得足够完善,基本可以当成文档来用了,而且在VS Code编辑器中还有动态的提示,以及一些错误的纠正,比如上述的NODE_ENV的获取,如果直接写process.env.NODE_ENV || ‘development’是会抛出一个异常的,因为从d.ts中可以看到,关于mode只有三个有效值production、developemnt和none,而process.env.NODE_ENV显然只是一个字符串类型的变量。 所以我们需要使用三元运算符保证传入的参数一定是我们想要的。 以及在编写的过程中,如果有一些自定义的plugin之类的,可能在使用的过程中会抛异常提示说某个对象不是有效的Plugin对象,一个很简单的方法,在对应的plugin后边添加一个as webpack.Plugin即可。 在这里TypeScript所做的只是静态的检查,并不会对实际的代码执行造成任何影响,就算类型因为强行as而改变,也只是编译期的修改,在实际执行的JavaScript代码中还是弱类型的 在完成了上述的操作后,再执行npm run XXX就可以直接运行TypeScript版本的Webpack配置咯。探索期间的一件趣事因为我的项目根目录已经安装了ts-node,而前端项目是作为其中的一个文件夹存在的,所以就没有再次进行安装。 这就带来了一个令人吐血的问题。 首先全部流程走完以后,我直接在命令行中输入TS_NODE_PROJECT=XXX.json NODE_ENV=dev webpack –config ./webpack/dev.ts 完美运行,然后将这行命令放到了npm scripts中:{ “scripts”: { “start”: “TS_NODE_PROJECT=XXX.json NODE_ENV=dev webpack –config ./webpack/dev.ts” }}再次运行npm start,发现竟然出错了-.-,提示我说import语法不能被识别,这个很显然就是没有应用我们在ts_NODE_PROJECT中指定的config文件。 刚开始并不知道问题出在哪,因为这个在命令行中直接执行并没有任何问题。 期间曾经怀疑是否是环境变量没有被正确设置,还使用了cross-env这个插件,甚至将命令写到了一个sh文件中进行执行。 然而问题依然存在,后来在一个群中跟小伙伴们聊起了这个问题,有人提出,你是不是全局安装了ts-node。 检查以后发现,果然是的,在命令行执行时使用的是全局的ts-node,但是在npm scripts中使用的是本地的ts-node。 在命令行环境执行时还以为是会自动寻找父文件夹node_modules下边的依赖,其实是使用的全局包。 乖乖的在client-src文件夹下也安装了ts-node就解决了这个问题。 全局依赖害人。。测试用例的改造前边的Webpack改为TypeScript大多数原因是因为强迫症所致。 但是测试用例的TypeScript改造则是一个能极大提高效率的操作。为什么要在测试用例中使用 TypeScript测试用例使用chai来编写,(之前的Postman也是用的chai的语法) chai提供了一系列的语义化链式调用来实现断言。 在之前的分享中也提到过,这么多的命令你并不需要完全记住,只知道一个expect(XXX).to.equal(true)就够了。 但是这样的通篇to.equal(true)是巨丑无比的,而如果使用那些语义化的链式调用,在不熟练的情况下很容易就会得到:Error: XXX.XXX is not a function因为这确实有一个门槛问题,必须要写很多才能记住调用规则,各种not、includes的操作。 但是接入了TypeScript以后,这些问题都迎刃而解了。 也是前边提到的,所有的TypeScript模块都有其对应的.d.ts文件,用来告诉我们这个模块是做什么的,提供了什么可以使用。 也就是说在测试用例编写时,我们可以通过动态提示来快速的书写断言,而不需要结合着文档去进行“翻译”。 使用方式如果是之前有写过mocha和chai的童鞋,基本上修改文件后缀+安装对应的@types即可。 可以直接跳到这里来:开始编写测试脚本 但是如果对测试用例感兴趣,但是并没有使用过的童鞋,可以看下边的一个基本步骤。安装依赖TypeScript相关的安装,npm i -D typescript ts-nodeMocha、chai相关的安装,npm i -D mocha chai @types/mocha @types/chai如果需要涉及到一些API的请求,可以额外安装chai-http,npm i -D chai-http @types/chai-http环境的依赖就已经完成了,如果额外的使用一些其他的插件,记得安装对应的@types文件即可。 如果有使用ESLint之类的插件,可能会提示modules必须存在于dependencies而非devDependencies 这是ESLint的import/no-extraneous-dependencies规则导致的,针对这个,我们目前的方案是添加一些例外:import/no-extraneous-dependencies: - 2 - devDependencies: - “/*.test.js” - “/.spec.js” - “**/webpack” - “/webpack/*“针对这些目录下的文件/文件夹不进行校验。_是的,webpack的使用也会遇到这个问题_开始编写测试脚本如果是对原有的测试脚本进行修改,无外乎修改后缀、添加一些必要的类型声明,不会对逻辑造成任何修改。一个简单的示例// number-comma.tsexport default (num: number | string) => String(num).replace(/B(?=(d{3})+$)/g, ‘,’)// number-comma.spec.tsimport chai from ‘chai’import numberComma from ‘./number-comma’const { expect } = chai// 测试项describe(’number-comma’, () => { // 子项目1 it(’1234567 should transform to 1,234,567’, done => { expect(numberComma(1234567)).to.equal(‘1,234,567’) done() }) // 子项目2 it(’123 should never transform’, done => { const num = 123 expect(numberComma(num)).to.equal(String(num)) done() })})如果全局没有安装mocha,记得将命令写到npm script中,或者通过下述方式执行./node_modules/mocha/bin/mocha -r ts-node/register test/number-comma.spec.ts# 如果直接这样写,会抛出异常提示 mocha 不是命令mocha -r ts-node/register test/number-comma.spec.tsmocha有一点儿比较好的是提供了-r命令来让你手动指定执行测试用例脚本所使用的解释器,这里直接设置为ts-node的路径ts-node/register,然后就可以在后边直接跟一个文件名(或者是一些通配符)。 目前我们在项目中批量执行测试用例的命令如下:{ “scripts”: { “test”: “mocha -r ts-node/register test//*.spec.ts” }}npm test可以直接调用,而不需要添加run命令符,类似的还有start、build等等 一键执行以后就可以得到我们想要的结果了,再也不用担心一些代码的改动会影响到其他模块的逻辑了 (前提是认真写测试用例) 小结做完上边两步的操作以后,我们的项目就实现了100%的TypeScript化,在任何地方享受静态编译语法所带来的好处。 附上更新后的代码含量截图:最近针对TypeScript做了很多事情,从Node.js、React以及这次的Webpack与Mocha+Chai。 TypeScript因为其存在一个编译的过程,极大的降低了代码出bug的可能性,提高程序的稳定度。 全面切换到TypeScript更是能够降低在两种语法之间互相切换时所带来的不必要的消耗,祝大家搬砖愉快。之前关于 TypeScript 的笔记TypeScript在node项目中的实践TypeScript在react项目中的实践一个完整的 TypeScript 示例typescript-example欢迎各位来讨论关于TypeScript使用上的一些问题,针对稳重的感觉不足之处也欢迎指出。参考资料ts-nodeconfiguration-languages | webpackmochajschaijs ...

September 3, 2018 · 2 min · jiezi