Jest - 在函数fs.writefile中Mock回调

4

我在这个主题上卡了3小时。 我找不到如何测试此代码中的if(err)分支的解决方案:

    function createFile(data){

return new Promise(function(resolve, reject) {

    try {

        if(data === null || data === undefined){
            throw new Error(errorMessages.noDataDefined);
        }

        let internalJobId = uuid.v4();
        let fileName = 'project_name' + internalJobId + '.xml';

        fs.writeFile(config.tmpPath + fileName, data, function (err) {
            if (err){
                throw new Error(err.toString());
            } else {
                resolve(fileName);
            }
            
        });

    } catch (error) {
        return reject(error);
    }
});

这个测试通过了,但它并没有调用if (err) { throw new Error(err.toString())}。

我必须找到一个解决方案,让回调返回一个错误,但是我还没有得到正确的解决方案。

test('Error', () => {

    jest.mock('fs', () => ({
        writeFile: jest.fn((path, data, callback) => callback(Error('some error')))
      }));

    return expect(createFile('Does not matter')).rejects.toThrow('some error');


});

但是在这个测试中,甚至没有出现拒绝的情况,因此从未抛出错误。如果有人能帮我解决问题,我将不胜感激。


1
请提供 https://stackoverflow.com/help/mcve,以便重现问题。您忽略了相关代码片段。没有createFile。如果使用'new Promise'封装fs.writeFile,则存在错误,因为在fs.writeFile回调中抛出错误不会导致拒绝的Promise。 - Estus Flask
你好,我已经更新了问题。我正在检查一个错误,抛出这个错误,在catch块中拒绝这个错误。 - Buzz
3个回答

3
这里有两个问题。一个是fs.writeFile没有被正确地模拟。另一个是createFile不能正确处理错误,无法满足期望。 jest.mock作用于还未被导入的模块,并且会被提升到块的顶部(或在顶级使用时会被提升到import之上)。如果已经导入了使用fs函数的模块,它就无法影响fs。由于fs函数通常与其命名空间一起使用,因此它们也可以作为方法进行模拟。
应该是这样的:
// at top level
import fs from 'fs';
jest.mock('fs', ...);
...

或者:
// inside test
jest.spyOn(fs, 'writeFile').mockImplementation(...);
...

可以肯定地说,这样做能使测试更具有特异性:

expect(fs.writeFile).toBeCalledTimes(1);
expect(fs.writeFile).toBeCalledWith(...);
return expect(createFile('Does not matter'))...

Promise构造函数不需要try..catch,因为它已经在内部捕获了所有同步错误,并且不能捕获来自回调函数的异步错误。对于需要拒绝承诺的地方,可以更喜欢使用reject以保持一致性。

fs.writeFile回调内部抛出错误是一个错误,会导致承诺处于挂起状态。它没有机会拒绝承诺,也没有机会在回调外部使用try..catch捕获,从而导致未捕获的错误。

修正如下:

function createFile(data){
    return new Promise(function(resolve, reject) {
        if(data === null || data === undefined){
            reject(new Error(errorMessages.noDataDefined));
        }

        let internalJobId = uuid.v4();
        let fileName = 'project_name' + internalJobId + '.xml';

        fs.writeFile(config.tmpPath + fileName, data, function (err) {
            if (err){
                reject(new Error(err.toString()); // reject(err) ?
            } else {
                resolve(fileName);
            }            
        });
    });
}

为了将嵌套最小化,不需要使用Promise的部分可以在函数中使用async移出构造函数:
async function createFile(data){
    if(data === null || data === undefined){
        throw new Error(errorMessages.noDataDefined);
    }

    return new Promise(function(resolve, reject) {    
        let internalJobId = uuid.v4();
        ...

还有一个fs.promises API可能不需要被转换成 Promise。

同时注意,new Error(err.toString())可能是不必要的,会导致意外的错误信息并使断言失败。Promise 可以使用原样的err来拒绝。如果目的是删除不必要的错误信息或更改错误堆栈,则应该使用new Error(err.message)


1
感谢您的帮助回复。我按照您的建议修改了代码,删除了try / catch块,并将第一个if语句放在了promise之上并放入了async函数中。但是所有的更改都使代码更加清晰,但似乎仍然没有解决我的第一个问题。我仍然无法模拟一个函数,使得这一行"reject(new Error(err.toString()); // reject(err) ?"被覆盖。我不知道是否只有我这样,但我对Jest的模拟部分感到比大多数开发部分更复杂。 - Buzz
这是问题的一部分,你可能还没有遇到。我已经更新了。虽然不难,但可能需要对它如何干扰Node模块以及底层发生了什么有一些了解,才能舒适地使用它。 - Estus Flask
不幸的是,这个代码 "jest.spyOn('fs', 'writeFile').mockImplementation(...);" 的结果是 "Cannot spy on a primitive value; string given"。 - Buzz
那是一个打字错误,已经修复。 - Estus Flask

1
答案是:

解决方案为:

 jest.spyOn(fs, 'writeFile').mockImplementation((f, d, callback) => {
                callback('some error');
 });
    

感谢 Estus 烧瓶!

不用谢。请注意,这是一个不合规范的模拟,无法检测到错误处理中所描述的问题,因为它会同步触发错误,而writeFile是异步的。应该是 => setTimeout(() => { callback('some error') }) - Estus Flask
好的,谢谢。我已经修改了代码。在两种情况下覆盖率都达到了100%,但这更符合要求,我明白了。所以现在我很想尝试使用s3 Api,我认为这是一件容易的事情。虽然我还需要在jest中学习很多,但希望有一天我能够完全理解一切。;) - Buzz

0
以下三行代码对我来说完成了工作:
import * as fs from 'fs/promises';

jest.mock('fs/promises');

jest.spyOn(fs, 'writeFile').mockImplementation( your implementation here );

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接