最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 「新年快乐」用单元测试的方式写一个promise垫片

    正文概述 掘金(SwordQiu)   2021-01-01   521

    前言

    本博客旨在入门一些单元测试的内容同时手写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
    

    「新年快乐」用单元测试的方式写一个promise垫片 到目前为止,已经成功执行了。

    上面的环境搭建可能会随着未来的改变而改变,不过在我写这篇博客的时候,能够正常工作。

    为什么要学单元测试

    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(() => {}));
      });
    });
    

    「新年快乐」用单元测试的方式写一个promise垫片 assert.throw方法接收一个函数,当这个函数内部抛出错误的时候,就会捕获到

    接着我们再看new promise(fn)中的fn必须立即执行,并且接收resolvereject两个函数。

    完善代码

    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);
      }
    }
    

    resolvereject是定义在类中的方法,上面代码用了provate定义成只有在类中能调用的私有函数.

    可能这也是比较难想到的点,在定义时直接传入的函数在类的定义时是直接调用的,并且传入的resolvereject也是在类里面定义好的方法。

    测试用例

      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");
      });
    

    查看结果 「新年快乐」用单元测试的方式写一个promise垫片

    小结

    现在来回顾一下我们上面的代码做了什么

    • 给类做了Polyfill判断
    • 添加了status
    • 内部定义了resolvereject函数
    • 在类定义后马上执行传入的函数
    • 通过函数的参数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之前successnull,所以这里需要设置判断条件,这样就会把异步任务推给队列,当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年新年快乐,心想事成~


    起源地下载网 » 「新年快乐」用单元测试的方式写一个promise垫片

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元