[英]How can I mock a service within a React component to isolate unit tests in jest?
I'm trying to refactor a unit test to isolate a service that calls an API using axios from the component calling the service.我正在尝试重构一个单元测试,以从调用该服务的组件中隔离一个使用 axios 调用 API 的服务。
The service is for the moment really simple:该服务目前非常简单:
import axios from 'axios'
export default class SomeService {
getObjects() {
return axios.get('/api/objects/').then(response => response.data);
}
}
Here's a snippet of the component that calls the service:下面是调用该服务的组件片段:
const someService = new SomeService();
class ObjectList extends Component {
state = {
data: [],
}
componentDidMount() {
someService.getObjects().then((result) => {
this.setState({
data: result,
});
});
}
render() {
// render table rows with object data
}
}
export default ObjectList
I can test that ObjectList renders data as I'd expect by mocking axios:我可以测试 ObjectList 是否按照 mocking axios 的预期呈现数据:
// ...
jest.mock('axios')
const object_data = {
data: require('./test_json/object_list_response.json'),
};
describe('ObjectList', () => {
test('generates table rows from object api data', async () => {
axios.get.mockImplementationOnce(() => Promise.resolve(object_data));
const { getAllByRole } = render(
<MemoryRouter>
<table><tbody><ObjectList /></tbody></table>
</MemoryRouter>
);
await wait();
// test table contents
});
});
Everything passes without issue.一切顺利通过。 As a mostly academic exercise, I was trying to figure out how to mock SomeService instead of axios, which is where things went awry because I think I don't understand enough about the internals of what's getting passed around.
作为一项主要的学术练习,我试图弄清楚如何模拟 SomeService 而不是 axios,这就是事情出错的地方,因为我认为我对传递的内部结构了解不够。
For example, I figured since SomeService just returns the axios response, I could similarly mock SomeService, sort of like this:例如,我认为由于 SomeService 只返回 axios 响应,我可以类似地模拟 SomeService,有点像这样:
// ...
const someService = new SomeService();
jest.mock('./SomeService')
const object_data = {
data: require('./test_json/object_list_response.json'),
};
describe('ObjectList', () => {
test('generates table rows from object api data', async () => {
someService.getObjects.mockImplementationOnce(() => Promise.resolve(object_data))
// etc.
This fails with an error: Error: Uncaught [TypeError: Cannot read property 'then' of undefined]
, and the error traces back to this line from ObjectList
:这失败并出现错误:
Error: Uncaught [TypeError: Cannot read property 'then' of undefined]
,并且错误从ObjectList
追溯到这一行:
someService.getObjects().then((result) => {
What specifically do I need to mock so that the ObjectList
component can get what it needs to from SomeService
to set its state?我具体需要模拟什么,以便
ObjectList
组件可以从SomeService
获得它需要的东西来设置它的 state?
After some trial and error playing around with different approaches suggested in the jest documentation , the only thing that seemed to work was calling jest.mock()
with the module factory parameter, like so:在使用 jest 文档中建议的不同方法进行了一些试验和错误之后,似乎唯一可行的方法是使用模块工厂参数调用
jest.mock()
,如下所示:
// rename data to start with 'mock' so that the factory can use it
const mock_data = {
data: require('./test_json/object_list_response.json'),
};
jest.mock('./SomeService', () => {
return jest.fn().mockImplementation(() => {
return {
getObjects: () => {
return Promise.resolve(mock_data).then(response => response.data)
}
};
});
});
// write tests
Using mockResolvedValue()
didn't work because I couldn't chain .then()
off of it.使用
mockResolvedValue()
不起作用,因为我无法将 .then( .then()
链接出来。
If this leads anyone to a more elegant or idiomatic solution, I'd welcome other answers.如果这会导致任何人找到更优雅或惯用的解决方案,我会欢迎其他答案。
The problem with mocking class instances is that it may be difficult to reach class instance and its methods without having a reference. mocking class 实例的问题在于,在没有参考的情况下可能难以到达 class 实例及其方法。 Since
someService
is local to component module, it can't be accessed directly.由于
someService
是组件模块的本地服务,因此无法直接访问。
Without specific mock, jest.mock('./SomeService')
relies on class automatic mock that works in unspecified ways.如果没有特定的模拟,
jest.mock('./SomeService')
依赖于以未指定方式工作的class 自动模拟。 The question shows that different instances of mocked class have different getObjects
mocked methods that don't affect each other, despite getObjects
is prototype method and conforms to new SomeService().getObjects === new SomeService().getObjects
in unmocked class.该问题表明,模拟 class 的不同实例具有不同的
getObjects
模拟方法,这些方法不会相互影响,尽管getObjects
是原型方法并且符合new SomeService().getObjects === new SomeService().getObjects
in unmocked class。
The solution is to not rely on automatic mocking but make it work the way it's expected.解决方案是不要依赖自动 mocking,而是让它按照预期的方式工作。 A practical way to make mocked method accessible outside class instance is to carry it alongside mocked module.
使模拟方法可以在 class 实例之外访问的一种实用方法是将其与模拟模块一起携带。 This way
mockGetObjects.mockImplementationOnce
will affect existing someService
.这样
mockGetObjects.mockImplementationOnce
会影响现有的someService
。 mockImplementationOnce
implies that the method can change the implementation later per test: mockImplementationOnce
暗示该方法可以在以后每次测试更改实现:
import { mockGetObjects }, SomeService from './SomeService';
jest.mock('./SomeService', () => {
let mockGetObjects = jest.fn();
return {
__esModule: true,
mockGetObjects,
default: jest.fn(() => ({ getObjects: mockGetObjects }))
};
});
...
mockGetObjects.mockImplementationOnce(...);
// instantiate the component
If the method should have constant mocked implementation, this simplifies the task because the implementation can be specified in jest.mock
.如果该方法应该具有恒定的模拟实现,这会简化任务,因为可以在
jest.mock
中指定实现。 It may still be beneficial to expose mockGetObjects
for assertions.为断言公开
mockGetObjects
可能仍然是有益的。
For posterity, another solution is creating a manual mock within a __mocks__
folder (inspired by Estus Flask's comment and this documentation ).对于后代,另一种解决方案是在
__mocks__
文件夹中创建手动模拟(受 Estus Flask 的评论和本文档的启发)。
./__mocks__/SomeService.js
export const mockGetObjects = jest.fn()
const mock = jest.fn(() => {
return {getObjects: mockGetObjects}
})
export default mock
Then the plain jest.mock('./SomeService')
call works with the implementation later being defined in the test:然后简单的
jest.mock('./SomeService')
调用与稍后在测试中定义的实现一起工作:
mockGetObjects.mockImplementationOnce(() => {
return Promise.resolve(object_data).then(response => response.data)
})
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.