用新的模拟覆盖现有/共享的 Jest 模拟,仅用于一个测试

Posted

技术标签:

【中文标题】用新的模拟覆盖现有/共享的 Jest 模拟,仅用于一个测试【英文标题】:Overriding an existing/shared Jest mock with a new mock for only one test 【发布时间】:2021-08-22 15:40:27 【问题描述】:

我有一个集成测试的 Jest 测试套件,使用 suite-global 模拟进行数据库访问,根据 SQL,我返回不同的模拟响应:

jest.mock('@my-org/our-mysql-wrapper', () => 
    const query = jest.fn(async (sql, params) => 
        if (sql === 'select foo from bar') 
            return [];
         else if (sql === 'select baz') 
            return [ messageId: 3 ,  messageId: 4 ,  messageId: 5 ];
         else if (...) 
            return ...;
         else 
            console.log('UNEXPECTED QUERY SENT TO MOCK: ', sql, params);
            return [];
        
    );
    const end = jest.fn(async () => true);

    return jest.fn(() => ( query, end ));
);

describe('suite', () => 
    //tests here
);

这对于阳性测试非常有效,但我对此感到沮丧的是阴性测试。例如,在某些情况下,如果数据库不返回任何结果,我们可能希望抛出错误。为了测试我需要让我的模拟对相同的输入表现不同。典型的正面测试在运行前不会覆盖数据库模拟,而负面测试需要:

it('should throw and handle an error if the db returns no results for Widget lookup', async () => 
    const mockDB = require('@my-org/our-mysql-wrapper')();
    mockDB.query.mockImplementation(jest.fn(asnyc (sql, params) => 
        if ( sql === 'select * from Widgets' )
            //this is the use-case that I want to override for this test
            return [];
        else
            //...
        
    ));
    
    const someValue = await tool.doThing();
    expect(buglogger).toHaveBeenCalled(); //actual test will be more specific...

    //I tried plugging in mockRestore/mockClear/mockReset here
);

正如上面所写的,这个测试实际上通过了,但是它破坏了在它之后运行的测试,因为它不会在它自己之后进行清理。据我所知,这就是mockClear()mockReset()mockRestore() 在不同变体中应该做的事情;但在测试结束时,我无法找到一种方法将我的模拟恢复到原始的覆盖前模拟的实现。

我在其他一些情况下也使用过jest.spyOn(),但这也不是我想要的。在这种情况下,我的测试失败了,并且对于其他测试,模拟仍然被破坏。

it('should throw and handle an error if the db returns no results for Widget lookup', async () => 
    const mockDB = require('@my-org/our-mysql-wrapper')();
    jest.spyOn(mockDb, 'query');
    mockDB.query.mockImplementation(jest.fn(asnyc (sql, params) => 
        if ( sql === 'select * from Widgets' )
            //this is the use-case that I want to override for this test
            return [];
        else
            //...
        
    ));
    
    const someValue = await tool.doThing();
    expect(buglogger).toHaveBeenCalled(); //actual test will be more specific...
    mockDb.query.mockRestore();
);

我也尝试过mockImplementationOnce(),但这对我不起作用,因为有问题的查询不是将运行的第一个查询。使用此方法会自动清理自身,但不会(据我所知,不能)使我的测试通过,因为它会在调用相关查询之前第一次使用后清理自身。

但是由于mockImplementationOnce 可以以恢复原始模拟的方式进行自我清理,难道不应该有一些手动方法来覆盖现有模拟仅用于一次测试吗?这就是mockImplementationOnce 正在做的事情,不是吗? (在第一次调用后而不是在 1 次测试后进行清理;但它似乎正在恢复原始模拟......)

我做错了什么,在这里?

【问题讨论】:

我不是 100% 确定,但我认为您对 mockClear 等人的理解是错误的......它会将模拟重置为原始功能,例如original 返回 42,mock 返回 52,mockclear 将使其在调用后开始返回 42。我认为您需要查看 beforeEach 并在那里设置模拟函数,以便在每次测试(它)之前,它会模拟该函数,然后在您失败时覆盖模拟,然后下一个测试将重置到默认的模拟设置 @Jarede 我想我确实理解这些方法在做什么,但我希望它们会有点堆栈推送/弹出而不是完全重置。 beforeEach 方法确实让我想到了。我正在用 fetch 做类似的事情。从长远来看,我希望将测试重构为不必重新定义整个功能的东西,只有测试想要覆盖的情况。不过,我可能无法绕过它。 【参考方案1】:

我认为问题出在这两种情况下,您试图将模拟分配给您导入的实际包。

您是否尝试如下更改您的模拟设置:

const mockDB = require('@my-org/our-mysql-wrapper')();
const dbSpy = jest.spyOn(mockDB, 'query');

dbSpy.mockImplementation(jest.fn(asnyc (sql, params) => 
  ...
));

dbSpy.mockClear();
dbSpy.mockRestore();

【讨论】:

这似乎是一个有效的做法,但它仍然会影响其他测试。 实际上,我并没有完全按照您的建议进行尝试。我仍然 - 并且想要保留 - 我的 jest.mock() 调用测试之间的默认/共享模拟(因此在您的示例中,它将位于第 1 行和第 2 行之间)。我已经尝试过您将间谍创建为全局和内部测试。无论哪种方式,它都会影响其他测试。 您能分享一下您是如何导出@my-org/our-mysql-wrapper 模块的吗? 包装器模块是一个工厂函数,它接受客户和环境作为输入(例如 FooCompany、QA)并在连接到数据库后返回一个 mysql 连接池。这被简化以适应,但显示了基础知识:module.exports = (cust, env) => settings = getCustomerSettings(cust,env); return mysql.createPool( ...settings ); @AdamTuttle 你解决了吗?

以上是关于用新的模拟覆盖现有/共享的 Jest 模拟,仅用于一个测试的主要内容,如果未能解决你的问题,请参考以下文章

使用 Jest 进行测试的共享 utils 函数

jest.mock():如何使用工厂参数模拟 ES6 类默认导入

PowerMock:模拟仅影响一个测试的静态方法

用 jest 模拟 vue 的 i18n

如何用 jest 解开单个实例方法

在 NestJS Jest 测试中覆盖提供程序