前言
本博客旨在入门一些单元测试的内容同时手写promise
垫片作为练手,代码非(mei)常(shen)简(me)单(yong)。本次用到的技术为
- Typescript
- sinon 提供工具测试函数
- chai 提供断言
- mocha 提供好看的打印和描述函数
上述工具只要上手三分钟就可以入门,本次手写代码过程舒适,阅读无忧,居家旅行必备。
安装环境
yarn global add ts-node mocha //全局安装
mkdir promise-demo //创建测试文件夹
cd promise-demo
yarn init -y
yarn add chai mocha --dev //局部安装chai和mocha
yarn add @types/chai --dev //typescript声明文件
yarn add @types/mocha --dev
touch test.ts
mocha -r ts-node/register test.ts //运行测试
到package.json
里面加入
"scripts": {
"test":"mocha -r ts-node/register test.ts"
},
就可以使用yarn test
命令代替上面那句运行测试的命令
安装sinon
yarn add sinon sinon-chai --dev
yarn add @types/sinon @types/sinon-chai --dev
yarn add ts-node typescript --dev //全局装了也要再装否则报错
引入
//test.ts
import * as chai from "chai";
import * as sinon from "sinon";
import * as sinonChai from "sinon-Chai";
chai.use(sinonChai);
const assert = chai.assert;
写一段代码试试
//test.ts
describe("我来看看有没有用", () => {
it("我想看是否相等", () => {
//@ts-ignore 这个注释是让ts太智能提示报错
assert.isFalse(1 === 2);
});
it("我想看看函数有没有运行", () => {
let fn = sinon.fake();//提供一个假函数对象
const fn2 = () => {
fn();
};
//断言函数没有运行
assert.isFalse(fn.called);//fn.called意思是执行过
fn2();
//断言函数运行了
assert.isTrue(fn.called);
});
});
yarn test
到目前为止,已经成功执行了。
上面的环境搭建可能会随着未来的改变而改变,不过在我写这篇博客的时候,能够正常工作。
为什么要学单元测试
1、单元测试提供了被测单元的使用场景,起到了使用文档的作用。
2、单元测试提供快速反馈,把bug消灭在开发阶段,减少问题流到集成测试、验收测试和用户,降低了软件质量控制的成本。
3、测试代码可作为可执行文档存在,且永不过时,不用想着改个代码跑去同步各种文档了
4、如果你不想在代码review时被人找出一些莫名其妙的错误,必须写单元测试
5、用单元测试有利于装B(重要,要考)
Start
接收一个函数并调用
在手写的时候,我们需要考虑一下promise的使用场景,首先我们写一个基本的代码看看
const promise=new Promise((resolve,reject)=>{
resolve()
})
promise.then(()=>{},()=>{})
上面我们可以看出以下几点
- Promise是一个类
- Promise需要一个new调用
- Promise接收一个函数
那么我们可以按照上面的描述先写一段代码
class promise{
constructor(fn) {
if (!new.target) {
throw new Error("需要new调用");
}
if (typeof fn !== "function") {
throw new Error("fn不是函数");
}
}
接着我们写测试用例
describe("测试promise的功能", () => {
it("不加new不能调用", () => {
assert.throw(() => {
//@ts-ignore ts有时候太智能,会自动报错,所以加这句
promise();
});
});
it("不传函数不能调用", () => {
assert.throw(() => {
//@ts-ignore
new promise(1);
});
assert(new promise(() => {}));
});
});
assert.throw
方法接收一个函数,当这个函数内部抛出错误的时候,就会捕获到
接着我们再看new promise(fn)
中的fn
必须立即执行,并且接收resolve
和reject
两个函数。
完善代码
class promise {
private resolve() {}
private reject() {}
constructor(fn) {
if (!new.target) {
throw new Error("需要new调用");
}
if (typeof fn !== "function") {
throw new Error("fn不是函数");
}
fn(this.resolve, this.reject);
}
}
resolve
和reject
是定义在类中的方法,上面代码用了provate
定义成只有在类中能调用的私有函数.
可能这也是比较难想到的点,在定义时直接传入的函数在类的定义时是直接调用的,并且传入的resolve
、reject
也是在类里面定义好的方法。
测试用例
it("函数传了之后立即执行", () => {
const fn = sinon.fake();//给一个假函数
new promise(fn);//传给new promise
assert(fn.called);//看看有没有调用过
});
it("传入的函数有一个reject和resolve都是函数", () => {
const p = new promise((resolve, reject) => {
assert.isFunction(resolve);
assert.isFunction(reject);
});
});
#Promise的状态
promise实例一共有三种不同的状态,分别是初始状态pending
、当调用resolve
后转化为fulfilled
、当调用rejected
后转化为rejected
,
完善代码
class promise {
status = "pending";//给一个初始状态
private resolve() {
if (this.status === "pending") {
this.status = "fulfilled"; //修改status
}
}
private reject() {
if (this.status === "pending") {
this.status = "rejected";
}
}
constructor(fn) {
if (!new.target) {
throw new Error("需要new调用");
}
if (typeof fn !== "function") {
throw new Error("fn不是函数");
}
fn(this.resolve.bind(this), this.reject.bind(this);//注意这的绑定
}
}
上面的代码中,由于this.resolve
里面有this
需要指向实例,如果不绑定this
的话指向的是全局对象。
不过如果写成箭头函数也可以不绑定this
,就像下面的代码一样,箭头函数会根据词法作用域帮我们绑上环境中的this
。
private reject = () => {
if (this.status === "pending") {
this.status = "rejected";
}
};
写测试用例
it("new promise后返回一个对象,它有一个status是pending", () => {
const p = new promise((resolve, reject) => {});
assert.isObject(p);
assert(p.status === "pending");
});
it("new promise后如果调用resolve,那么status会变成fulfilled", () => {
const a = new promise((resolve, reject) => {
resolve();
});
assert(a.status === "fulfilled");
});
it("new promise后如果调用reject,那么status会变成rejected", () => {
const a = new promise((resolve, reject) => {
reject();
});
assert(a.status === "rejected");
});
it("重复调用resolve或者reject,status不会改变", () => {
const a = new promise((resolve, reject) => {
resolve();
reject();
});
assert(a.status === "fulfilled");
});
查看结果
小结
现在来回顾一下我们上面的代码做了什么
- 给类做了
Polyfill
判断 - 添加了
status
- 内部定义了
resolve
、reject
函数 - 在类定义后马上执行传入的函数
- 通过函数的参数
resolve
或者reject
来修改状态
我们可以通过单元测试很清楚地知道每一步我们需要做的内容以及代码执行正确性。(虽然第一次写单元测试时非常麻烦)
then函数
下面我们来做一下then
函数,每个promise
实例都有一个then
方法,这个then
方法可以接收两个函数参数(可以不是函数).
class promise {
status = "pending";
success = null;//这里保存
fail = null;//这里保存
...//省略重复的代码
then(success?, fail?) {
if (typeof success === "function") {
this.success = success;
}
if (typeof fail === "function") {
this.fail = fail;
}
}
}
测试用例
it("new promise实例有一个then方法", () => {
assert.isFunction(new promise((resolve, reject) => {}).then);
});
then
的异步调用
如果resolve
了会自动调用then
函数的第一个函数,如果reject
会自动调用then
函数的第二个函数。这里是比较难的点,因为then
方法是实例定义完之后才调用的,所以如果要resolve
后调用只能设置一个异步函数,这里用定时器设置这个函数。
success = null;//保存success函数用
fail = null; //保存fail函数用
private resolve = () => {
if (this.status === "pending") {
this.status = "fulfilled";
}
setTimeout(() => {
if (this.success) {//注意 这段必须要写,否则success依然为空
this.success();
}
}, 0);
};
private reject = () => {
if (this.status === "pending") {
this.status = "rejected";
}
setTimeout(() => {
if (this.fail) {
this.fail();
}
}, 0);
};
上面的定时器必须设置一个条件,否则虽然是异步,但是没调用then
之前success
是null
,所以这里需要设置判断条件,这样就会把异步任务推给队列,当Event Loop
轮询发现调用then
方法后才执行这个异步任务。
测试用例
it("resolve**之后**会执行then的第一个参数函数", (done) => {
const fn = sinon.fake();
const p = new promise((resolve, reject) => {
resolve();
});
p.then(fn, () => {});//执行这里的时候还是同步代码,但是触发了异步任务
//then之前没执行
assert.isFalse(fn.called);
setTimeout(() => {
// then之后执行了
assert.isTrue(fn.called);
done();
}, 0);
});
it("reject**之后**会执行then的第二个参数函数", (done) => {
const fn = sinon.fake();
const p = new promise((resolve, reject) => {
reject();
});
p.then(null, fn);//执行这里的时候还是同步代码,但是触发了异步任务
//then之前没执行
assert.isFalse(fn.called);//同步代码
setTimeout(() => {
// then之后执行了
assert.isTrue(fn.called);
done();
}, 0);
});
上面的测试用例有个done
参数,这个参数的意思是当我下面的测试用例用到异步的话,需要调用done
来告知测试函数在执行完异步函数后再执行。
多次调用then
函数
这里可以采用event hub
的形式,把所有要执行的函数都存到一个数组中,在用的时候一次性调用就行,我在下面贴一下有修改的代码
class promise {
status = "pending";
callbacks = [];
private resolve = (result) => {
if (this.status === "pending") {
this.status = "fulfilled";
setTimeout(() => {
this.callbacks.forEach((handle) => {
if (typeof handle[0] === "function") {
handle[0].call(undefined,result);
}
});
}, 0);
}
};
private reject = (reason) => {
if (this.status === "pending") {
this.status = "rejected";
setTimeout(() => {
this.callbacks.forEach((handle) => {
if (typeof handle[1] === "function") {
handle[1].call(undefined,reason);
}
});
}, 0);
}
};
constructor(fn) {
if (!new.target) {
throw new Error("需要new调用");
}
if (typeof fn !== "function") {
throw new Error("fn不是函数");
}
fn(this.resolve, this.reject);
}
then(success?, fail?) {
const handle = [];
if (typeof success === "function") {
handle[0] = success;
}
if (typeof fail === "function") {
handle[1] = fail;
}
this.callbacks.push(handle);
}
}
测试用例
it("then可以调用多次fn--成功情况", (done) => {
const fn1 = sinon.fake();
const fn2 = sinon.fake();
const p = new promise((resolve, reject) => {
resolve();
});
p.then(fn1, null);
p.then(fn2, null);
setTimeout(() => {
assert(fn1.calledOnce);//证明只执行一次
assert(fn1.calledBefore(fn2));//证明fn1在fn2后执行
done();
}, 0);
});
it("then可以调用多次fn--失败情况", (done) => {
const fn1 = sinon.fake();
const fn2 = sinon.fake();
const p = new promise((resolve, reject) => {
reject();
});
p.then(null, fn1);
p.then(null, fn2);
setTimeout(() => {
assert(fn1.calledOnce);//证明只执行一次
assert(fn1.calledBefore(fn2));//证明fn1在fn2之后执行
done();
}, 0);
});
it("resolve、reject都调用,then参数都传,看看有没有执行正确", (done) => {
const fn1 = sinon.fake();
const fn2 = sinon.fake();
const p = new promise((resolve, reject) => {
resolve();
reject();
});
p.then(fn1, fn2);
setTimeout(() => {
assert(fn1.called);
assert.isFalse(fn2.called);
done();
}, 0);
});
传递参数
当resolve或者reject时,我可以传递将参数传递给then
作为then方法中两个函数的参数(这里的代码很简单,我在上面已经把参数传递进去了,所以下面贴一下测试用例)
it("resolve或者reject后可以传递参数", () => {
const p = new promise((resolve, reject) => {
resolve(2);
});
p.then((r) => {
assert(r === 2);
});
new promise((resolve, reject) => reject("错误")).then(null, (reason) => {
assert(reason === "错误");
});
});
最后
上面的垫片并非完全按照promise A+来的,如果说要真正实现符合规范的手写promise规范,请参照这里
promisesaplus.com/
后面半段then的链式调用、promise的解决程序等等都是基于规范来写的,由于这部分过于抽象,我会在未来单独写一个手写promise的博客。
祝大家2021年新年快乐,心想事成~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!