PromiseA+测试从一个报错解读 promises-aplus-tests 源码

2,681 阅读4分钟

前言

  • 在手写实现 Promise 并使用 promises-aplus-tests 测试是否满足 PromiseA+ 规范;之前在使用时,都是能正常工作的,由于修改了一些代码,想重新测试一下,结果执行 npm run test 时,报错了。

报错信息

  • TypeError: adapter.deferred is not a function
var d = adapter.deferred();
TypeError: adapter.deferred is not a function

image.png

查找错误原因

查找 promises-aplus-tests 官方文档

  • 文档中有这样一段描述:

In order to test your promise library, you must expose a very minimal adapter interface. These are written as Node.js modules with a few well-known exports:

  • resolved(value): creates a promise that is resolved with value.
  • rejected(reason): creates a promise that is already rejected with reason.
  • deferred(): creates an object consisting of { promise, resolve, reject }:
    • promise is a promise that is currently in the pending state.
    • resolve(value) resolves the promise with value.
    • reject(reason) moves the promise from the pending state to the rejected state, with rejection reason reason.
  • 意思是:
    • 为了测试 promise 库,需要提供一个 adapter 的接口;
    • 剩下的是说明 adapter 接口导出三个方法:
      • resolved(value):创建一个 resolved(value)promise
      • rejected(reason):创建一个 resolved(value)promise
      • deferred()deferred 函数需要返回一个对象,包括(promiseresolvereject)。
  • 查看 MyPromise.js 文件是否有 deferred 方法,在文件最后,找到了是存在 deferred 方法定义的。
// defered
MyPromise.deferred = function () {
    const dfd = {};
    dfd.promise = new MyPromise( (resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject;
    })
    return dfd;
}
  • 那为什么还是不行呢? image.png

查看 promises-aplus-tests 源码

找到报错文件

  • 报错文件位于 /node_modules/promises-aplus-tests/lib/programmaticRunner.js,报错行数为该文件第 13 行。
function normalizeAdapter(adapter) {
    if (!adapter.resolved) { // adapter 是否有 resolved 方法
        // 添加 adapter.deferred() 返回的 resolve 方法
        adapter.resolved = function (value) {
            var d = adapter.deferred(); // 文件第 13 行
            d.resolve(value);
            return d.promise;
        };
    }

    if (!adapter.rejected) {  // adapter 是否有 rejected 方法
        // 添加 adapter.deferred() 返回的 reject 方法
        adapter.rejected = function (reason) { 
            var d = adapter.deferred();
            d.reject(reason);
            return d.promise;
        };
    }
}
  • 这行代码位于一个叫 normalizeAdapter 的函数中,接收一个 adapter,目的是把 adapter 标准化,添加必须的三个方法。那我们在函数的开头添加一行打印,看一下接收的 adapter 是什么。 image.png
  • 打印出来的 adapter 是一个空对象?那我们继续看 normalizeAdapter 函数在那些地方使用过,通过搜索函数名,发现在两个地方使用过:
    1. module.exports
    module.exports.mocha = function (adapter, mochaOpts, cb) {
        ...
        normalizeAdapter(adapter);
        ...
    }
    
    1. module.exports.mocha
    module.exports.mocha = function (adapter) {
        normalizeAdapter(adapter);
        ...
    }
    

查找引入 programmaticRunner.js 的文件

  • 想要查找引入 programmaticRunner.js 的文件,我们可以通过在左侧 promises-aplus-tests 文件夹下直接搜索文件名;由于 promises-aplus-tests 下的文件不多,也可以一个个文件打开去文件开头找。
  • 找到文件在同级的 cli.js 文件中引入并使用。
// promises-aplus-tests/lib/cli.js
#!/usr/bin/env node
"use strict";

var path = require("path");
var getMochaOpts = require("./getMochaOpts");
var programmaticRunner = require("./programmaticRunner");

var filePath = getAdapterFilePath();
var adapter = adapterObjectFromFilePath(filePath);
var mochaOpts = getMochaOpts(process.argv.slice(3));
programmaticRunner(adapter, mochaOpts, function (err) {
    if (err) {
        process.exit(err.failures || -1);
    }
});

// 获取需要测试文件的绝对路径
function getAdapterFilePath() {
    if (process.argv[2]) {
        return path.join(process.cwd(), process.argv[2]);
    } else {
        throw new Error("Specify your adapter file as an argument.");
    }
}

// 通过文件的绝对路径使用 require 引入文件
function adapterObjectFromFilePath(filePath) {
    try {
        return require(filePath);
    } catch (e) {
        var error = new Error("Error `require`ing adapter file " + filePath + "\n\n" + e);
        error.cause = e;

        throw error;
    }
}
  • cli.js 文件中可以找到 adapter 是通过 adapterObjectFromFilePath 函数返回的,adapterObjectFromFilePath 函数又接收了一个 filePath 的参数,根据参数名称我们大概能猜出这是一个文件的路径;filePath 是通过 getAdapterFilePath 函数返回的,那我们就先查看 getAdapterFilePath 函数。
function getAdapterFilePath() {
    if (process.argv[2]) {
        return path.join(process.cwd(), process.argv[2]);
    } else {
        throw new Error("Specify your adapter file as an argument.");
    }
}
  • getAdapterFilePath 通过判断获取命令参数判断是否有第二个参数(需要测试的文件名称),存在就返回当前项目目录的绝对地址拼接需要测试的文件名称的完整路径,不存在就抛出一个错误。
// package.json
 "scripts": {
        "test": "promises-aplus-tests MyPromise"
    },

image.png

  • adapterObjectFromFilePath 通过 getAdapterFilePath 返回的文件路径使用 require 方式引入需要测试的文件;不存在就抛出异常。
function adapterObjectFromFilePath(filePath) {
    try {
        return require(filePath); // 使用的 CommonJS 规范
    } catch (e) {
        var error = new Error("Error `require`ing adapter file " + filePath + "\n\n" + e);
        error.cause = e;
        throw error;
    }
}
  • 到这儿,我们就知道为什么我们拿到的是一个空对象了,因为 MyPromise.js 文件中没有使用 CommonJS 规范去暴露我们的 MyPromise 对象,所以我们只需要添加一下代码在文件最后即可。
module.exports = MyPromise
  • 再次执行 npm run test,就可以正常运行了。 image.png
  • 解决方法是不是很简单,就这?就这。 image.png

总结

  • 当遇到某些问题,他人没有遇到过时,我们可以通过梳理问题,一步步去查找问题的根源,对比是否与相应要求与自己的是否一致,追根溯源。
  • 不要害怕看源码,梳理代码的执行过程,查看每一步的执行结果,看执行结果是否满足下一步执行条件,不满足再往上查找是哪一步结果出了问题。

往期精彩

「点赞、收藏和评论」

❤️关注+点赞+收藏+评论+转发❤️,创作不易,鼓励笔者创作更好的文章,谢谢🙏大家。