用新的模拟覆盖现有/共享的 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 模拟,仅用于一个测试的主要内容,如果未能解决你的问题,请参考以下文章