问题
I'm trying to refactor a unit test to isolate a service that calls an API using axios from the component calling the service.
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:
// ...
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.
For example, I figured since SomeService just returns the axios response, I could similarly mock SomeService, sort of like this:
// ...
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
:
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?
回答1:
The problem with mocking class instances is that it may be difficult to reach class instance and its methods without having a reference. Since someService
is local to component module, it can't be accessed directly.
Without specific mock, jest.mock('./SomeService')
relies on
class automatic mock that works in unspecified ways. 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.
The solution is to not rely on automatic mocking but make it work the way it's expected. A practical way to make mocked method accessible outside class instance is to carry it alongside mocked module. This way mockGetObjects.mockImplementationOnce
will affect existing someService
. mockImplementationOnce
implies that the method can change the implementation later per test:
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
. It may still be beneficial to expose mockGetObjects
for assertions.
回答2:
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:
// 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.
If this leads anyone to a more elegant or idiomatic solution, I'd welcome other answers.
回答3:
For posterity, another solution is creating a manual mock within a __mocks__
folder (inspired by Estus Flask's comment and this documentation).
./__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:
mockGetObjects.mockImplementationOnce(() => {
return Promise.resolve(object_data).then(response => response.data)
})
来源:https://stackoverflow.com/questions/63286972/how-can-i-mock-a-service-within-a-react-component-to-isolate-unit-tests-in-jest