【问题标题】:Using .then(fn1,fn2) acts differently than .then(fn1).catch(fn2) when handling rejected promise in Jest test [duplicate]在 Jest 测试中处理被拒绝的承诺时,使用 .then(fn1,fn2) 的行为不同于 .then(fn1).catch(fn2) [重复]
【发布时间】:2022-01-15 08:09:53
【问题描述】:

在尝试处理更新失败并且我想将按钮重置为其原始状态的情况时,我发现 then(fn1,fn2)then(fn1).catch(fn2) 在被拒绝的承诺后给出了不同的结果,尽管事实上 @987654321 @。

test('Reproduce error', () => {
    //setup jsdom
    document.body = document.createElement('body');
    let button = document.createElement('button');
    button.id = 'id';
    document.body.appendChild(button);

    //setup functions used
    let promiseReject;

    const updaterMock = function () {
        return new Promise((resolve,reject)=>{promiseReject = reject});
    };

    const markButtonOn = () => {};

    const markButtonOff = function () {
        console.log("I'm marking off");
        button.textContent = 'off';

        button.onclick = () => {
            button.textContent = 'updating';
            console.log("I'm updating");
            updaterMock()
                //using only then(): test passes
                .then(markButtonOn,markButtonOff);
                //using both then() and catch(): test fails on last expect()
                //.then(markButtonOn).catch(markButtonOff);
        };
    }

    //initial state
    markButtonOff();

    //always passes
    expect(button.textContent).toBe('off');

    //try to turn on
    button.click();

    //always passes
    expect(button.textContent).toBe('updating');

    //trigger the update to fail
    promiseReject();

    //force test to wait for the mock's returned promise to resolve
    return Promise.resolve().then(() => {
        //passes with .then(markButtonOn,markButtonOff)
        //fails with .then(markButtonOn).catch(markButtonOff), received 'updating'
        expect(button.textContent).toBe('off');
    });
});

我尽可能地减少了这种情况,去掉了所有不必要的东西。 (至少,据我所知。我对 Javascript/Typescript 还是很陌生。旁注:我从 here 获得了 promiseReject 技术。)

如果在中间button.onclick 部分的updaterMock() 调用之后更改注释掉的行,则测试结果会更改。

如果我只使用then(fn1,fn2),则测试通过。

如果我使用 then(fn1).catch(fn2) 测试失败,说明 textContent 仍处于“更新”状态。

在这两种情况下,Jest 报告对 console.log() 的所有 3 次调用都失败了:

我要结束了

我正在更新

我要结束了

这告诉我对 markButtonOff 的第二次调用正在执行,但 Jest 没有看到元素的变化。

  1. 为什么then(fn1,fn2)then(fn1).catch(fn2) 的行为不同?

  2. 如何更改测试中的内容以解决此问题(理想情况下,只是对测试代码做一些事情,而不是对 markButtonOff 生产代码,我直接将其写入测试以方便其他人重现。 )

【问题讨论】:

  • 不,promise.then(fn1, fn2) 不一定与 promise.then(fn1).catch(fn2) 相同 - 如果 fn1 在第一次调用时抛出错误,fn2 不会被调用来对其做出反应 - 它只有在promise 拒绝时才会调用。在第二种情况下,如果promisefn1 拒绝链,则将调用fn2。仅仅因为catch(f) 等同于.then(undefined, f) 并不意味着它与将两者结合起来是一样的。
  • 话虽如此,我怀疑问题出在//force test to wait for the mock's returned promise to resolve - 你不应该期望两个不相关的承诺链以任何特定的顺序得到解决。不同运行之间的顺序可能会一致,但通常无法预测。因此,您不能保证按钮承诺链在测试结束时的承诺链之前完成。
  • @VLAZ fn1 甚至没有被调用,所以我不认为这种情况与这种情况有关,因为这里只发生了拒绝路径。至于结尾部分,这是我根据 Jest 文档中的示例可以得出的最佳解决方案。有什么更好的处理方法?
  • 保存来自onclick 的promise - 执行p = updaterMock().then(markButtonOn).catch(markButtonOff) 然后在测试结束时执行p.then() 而不是Promise.resolve().then() - 这将保证断言在promise 链解析后运行.希望正确的时机足以让测试通过。如果不是,至少现在测试是完全可以预测的。
  • @VLAZ 不幸的是,这将涉及尝试从生产代码内部保存结果以用于测试。即使在这个简化的示例中,我也没有看到它像 p 那样工作在一个块内,因此在测试结束时无法在该块外访问。

标签: javascript promise jestjs es6-promise


【解决方案1】:

VLAZ 正确识别了我的核心问题,即两个承诺链不会以相同的顺序解决。

在被拒绝的承诺后,我在then(fn1,fn2)then(fn1).catch(fn2) 之间得到了不同的结果,这纯粹是运气/随机性/非确定性垃圾。

经过讨论,我更好地理解了这个问题,并重新设计了测试,使其从头到尾都有一个连续的承诺链。

test('Reproduce error', () => {
    //setup jsdom
    document.body = document.createElement('body');
    let button = document.createElement('button');
    button.id = 'id';
    document.body.appendChild(button);

    //setup functions used
    let startPromiseChain;

    let p = new Promise((resolve, reject) => {
        console.log('A');
        startPromiseChain = resolve;
    }).then(() => {
        //verify the initial state after the click() is to be 'updating'
        //and then reject() to trigger the catch() handler in markButtonOff
        console.log('L');
        expect(button.textContent).toBe('updating');
        return Promise.reject();
    });

    //"injected" mock
    console.log('B');
    const updaterMock = function () {
        return p;
    };

    //** begin production code
    console.log('C');
    const markButtonOn = () => {};

    //added a finally() call to the promise chain to be able to inform the
    //test that the promise has finished resolving inside the production code
    const markButtonOff = function (finallyCallback) {
        console.log('F/M');
        button.textContent = 'off';

        button.onclick = () => {
            console.log('I');
            button.textContent = 'updating';
            updaterMock()
                //using only then(): test passes
                //.then(markButtonOn,markButtonOff).finally(finallyCallback);
                //using both then() and catch(): test passes as well
                .then(markButtonOn).catch(markButtonOff).finally(finallyCallback);
        };
    }

    //** end production code

    //function to start the 2nd promise chain in the test (below), once the
    //promise chain inside markButtonOff finishes
    let callOnFinally;

    //create a promise that resolves after markButtonOff finishes updating
    let q = new Promise((resolve,reject) => {
        console.log('D');
        callOnFinally = resolve;
    }).then(() => {
        //make sure the button says it is 'off' again after it finishes updating
        console.log('N');
        expect(button.textContent).toBe('off');
    });

    //setup the initial button state
    console.log('E');
    markButtonOff(callOnFinally);

    //assert button is initially off
    console.log('G');
    expect(button.textContent).toBe('off');

    //try to turn on
    console.log('H');
    button.click();

    //trigger the update to fail, simulating an API request failure
    //this starts the first promise in the test to resolve
    console.log('J');
    startPromiseChain();

    console.log('K');
    //return the promise created above so Jest waits for it to resolve
    return q;
});

当我运行测试时,所有console.log 调用都按正确的字母顺序排列。

这里是过程的总结:

  1. 创建一个我们可以手动激活的 Promise 以启动整个过程,并包含一个 .then(),它在 Promise 链启动后检查初始状态,并实际触发我们想要测试的拒绝路径。

  2. 设置生产代码。这里我添加了一个回调函数,在then(fn1,fn2)then(fn1).catch(fn2) 之后的finally() 中调用。这是必要的(据我所知),并且是一个可以接受的折衷方案,因为该回调也可以用于其他想要等待按钮完成更新的事情。

  3. 设置另一个可以通过 finally() 回调触发的承诺,并让它执行测试的最终断言,以确保在生产代码完成后一切都应该是这样。

  4. 在第 1 步启动 Promise 链,并从测试中返回第 3 步的 Promise,以便 Jest 在报告通过或失败之前等待整个链完成。

由于需要以正确的顺序定义和分配变量,它看起来有点复杂,但它完美地工作并且合乎逻辑。 (除非我犯了一个错误,它只是运气好,但我想我现在明白了。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-01-21
    • 2017-09-04
    • 2018-07-04
    • 2017-10-25
    • 2021-10-17
    • 1970-01-01
    • 2017-08-19
    • 1970-01-01
    相关资源
    最近更新 更多