Can't mock function multiple times react, testing - javascript

I want to test my component:
const Courses: React.FC = () => {
const { data, error } = useSWR(
'some url...',
fetcher
);
console.log(data, error);
if (error) {
return (
<CoursesContainer>
<Error>Something went wrong.</Error>
</CoursesContainer>
);
}
if (!data) return <Loader title="loader" />;
return (
<CoursesContainer>
<CollapsibleTable courses={data} />
</CoursesContainer>
);
};
export default Courses;
but I don't know why I can't mock it to return different value for each test. I've tried that:
jest.mock('../../utils/fetcher', () => ({
fetcher: jest
.fn()
.mockReturnValue('default')
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call'),
readData: jest.fn(),
}));
test('Basic render. fetch pending', async () => {
const component = render(<Courses />);
await waitFor(() => component.findByTitle('loader'));
expect(component.baseElement).toMatchSnapshot();
});
test('Basic render, fetch success', async () => {
const component = render(<Courses />);
await waitFor(() => component.findByText('CollapsibleTable'));
expect(component.baseElement).toMatchSnapshot();
});
test('Basic render, fetch error', async () => {
const component = render(<Courses />);
await waitFor(() => component.findByText('Something went wrong.'));
expect(component.baseElement).toMatchSnapshot();
});
and that doesn't work well. For each of tests there is only first call console.log() - The console.log(data, error); from Courses.tsx.
The feedback from jest:
console.log
undefined undefined
at Courses (src/components/Courses.tsx:14:11)
console.log
first call undefined
at Courses (src/components/Courses.tsx:14:11)
console.log
first call undefined
at Courses (src/components/Courses.tsx:14:11)
console.log
first call undefined
at Courses (src/components/Courses.tsx:14:11)
And of course the third test (Basic render, fetch error) is failed cos of that.
I can't use spyOn() instead, cos of my fetcher is separate function whithout object.
## UPDATE ##
There are my fetcher and readData functions:
const fetcher = (url: string) => {
return fetch(url)
.then((response) => response.json())
.then((data: Array<IFetchData>) => readData(data));
};
const readData = (data: Array<IFetchData>) => {
let myData: Array<ICourse> = [];
[ there are some simple operations which create new myData array with
properties which I need (there is not any async operations)]
return myData;
};

You have to give mock implementation for readData as well.
According to jest specification,
We can create a mock function with jest.fn(). If no implementation is given, the mock function will return undefined when invoked.
This will make more sense about your test.
await waitForElementToBeRemoved(() => component.getByTitle('loader'));
We're waiting for the loader title to be removed which ensures that the title shows up in the first place and now it is removed when loader is completed.
jest.mock('../../utils/fetcher', () => ({
fetcher: jest
.fn()
.mockResolvedValue('default')
.mockResolvedValueOnce('first call')
.mockResolvedValueOnce('second call'),
readData: jest.fn().mockResolvedValue('Read call'), //provide reseolve value
//jest.fn() returns undefined when we dont't provide implementation
}));
test('Basic render. fetch pending', async () => {
const component = render(<Courses />);
await waitForElementToBeRemoved(() => component.getByTitle('loader'));
expect(component.baseElement).toMatchSnapshot();
});
test('Basic render, fetch success', async () => {
const component = render(<Courses />);
await waitForElementToBeRemoved(() => component.getByText('CollapsibleTable'));
expect(component.baseElement).toMatchSnapshot();
});
test('Basic render, fetch error', async () => {
const component = render(<Courses />);
await waitForElementToBeRemoved(() => component.getByText('Something went wrong.'));
expect(component.baseElement).toMatchSnapshot();
});
#Updated answer
Sorry to say that you can't achieve what you want. The reason is the render function is called only once in your test case so it means that the fetcher and readData API will call only once.
const mockFn = jest.fn();
jest.mock('../../utils/fetcher', () => ({
fetcher: mockFn.mockResolvedValueOnce('first call'),
readData: mockFn.mockResolvedValue(['Read call']), // returns array
}));
test('Basic render. fetch pending', async () => {
const component = render(<Courses />);
await waitForElementToBeRemoved(() => component.getByTitle('loader'));
expect(mockFn).toHaveBeenCalledTimes(1); // test passed
expect(component.baseElement).toMatchSnapshot();
});
Even your provide mockResolvedValueOnce again it will give undefined as render function doesn't get a chance to call the second time to mock version of fetcher and readData.

Looks like your MockReturnValue chain is out of order. The default should be last, like this:
jest.mock('../../utils/fetcher', () => ({
fetcher: jest
.fn()
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call')
.mockReturnValue('default'),
readData: jest.fn(),
}));
See the approved answer from here:
Jest mock the same function twice with different arguments

Related

`.pipe()` not executing `debounceTime`

I'm trying to debounce() an Observable with pipe() and chaining .subscribe() but for some reason the function in the subscribe is still being called over a dozen times in one go.
What I'm trying to do is pipe the withChangesForTables and debounce the sync call because I want it to be called only when a whole batch of changes have been made. So I created a provider for the sync and wrapped it around my RootNavigator
withChangesForTables on WatermelonDB source code
const SyncContext = createContext();
function useSync() {
return useContext(SyncContext);
}
function SyncProvider({children}) {
const [isSyncing, setIsSyncing] = useState(false);
const [hasUnsynced, setHasUnsynced] = useState(false);
async function checkUnsyncedChanges() {
const hasChanges = await hasUnsyncedChanges({
database
});
setHasUnsynced(hasChanges);
}
async function sync() {
await checkUnsyncedChanges();
if (!isSyncing && hasUnsynced) {
setIsSyncing(true);
await synchronizeWithServer();
setIsSyncing(false);
}
}
database.withChangesForTables([
'table_name',
'table_name2'
]).pipe(
skip(1),
// ignore records simply becoming `synced`
filter(changes => !changes.every(change => change.record.syncStatus === 'synced')),
// debounce to avoid syncing in the middle of related actions - I put 100000 to test only
debounceTime(100000),
).subscribe({
//calls API endpoint to sync local DB with server
next: () => sync(),
error: e => console.log(e)
});
const value = {
isSyncing,
hasUnsynced,
checkUnsyncedChanges,
sync
};
return (
<SyncContext.Provider value={value}>
{children}
</SyncContext.Provider>
);
}
I had to move withChangesForTables into a useEffect and retrun it in order to unsubcribe which seems to have resolved the issue. The code now looks something like this:
useEffect(() => {
return database.withChangesForTables([
'table_name',
'table_name2'
]).pipe(
skip(1),
filter(changes => !changes.every(change => change.record.syncStatus === 'synced')),
debounceTime(500),
).subscribe({
next: () => sync(),
error: e => console.log(e)
});
}, [])

How do I have two tests in the same file with PollyJS and Jest?

I'm having a strange issue with PolyJS and Jest. I have 2 tests in a single file -- the first one passes, and the second returns an error from the fetch in MyComponent: TypeError: Cannot read property 'status' of undefined"
It doesn't matter which one is second. Standalone each test passes, but together the second fails so it looks like Polly is just not doing the fetch from it's recordings.
I've tried various things like context.polly.flush() in afterEach but it doesn't seem to work. Any ideas?
Polly config:
export const setupDefaultPolly = () => {
const context = setupPolly({
mode: process.env.POLLY_MODE,
adapters: [require('#pollyjs/adapter-node-http')],
persister: require('#pollyjs/persister-fs'),
persisterOptions: {
fs: {
recordingsDir: path.resolve(__dirname, '../__recordings__'),
},
},
})
return context
My test file:
describe('MyComponent', () => {
setupDefaultPolly()
it('should render correctly', async () => {
const fetchSpy = jest.spyOn(global, 'fetch')
let wrapper
await act(async () => {
wrapper = mount(<MyComponent thing="exists" />)
})
expectLoadingPage(wrapper).toBeTruthy()
await waitFor(() => {
wrapper.update()
expectLoadingPage(wrapper).toBeFalsy()
})
expect(wrapper.text()).toContain('exists')
expect(withoutMuiID(wrapper)).toMatchSnapshot()
expect(fetchSpy).toHaveBeenCalledTimes(3)
})
it('should display error when not found', async () => {
let wrapper
await act(async () => {
wrapper = mount(<MyComponent thing="not-exists" />)
})
expectLoadingPage(wrapper).toBeTruthy()
await waitFor(() => {
wrapper.update()
expectLoadingPage(wrapper).toBeFalsy()
})
expect(wrapper.text()).toContain('Not found: not-exists')
expect(withoutMuiID(wrapper)).toMatchSnapshot()
})
})

How to mock and test Axios rejected promise?

I created a class called API and it's a simple wrapper around Axios
export class API {
static get = async (route: string, version: string = API_VERSION) => {
try {
return await axios.get(`${BASE_URL + version}${route}`);
} catch (error) {
throw error;
}
};
}
I'm trying to test the catch branch of the get method:
I tried:
describe('API Throws Errors', () => {
beforeEach(() => {
// axios.get.mockImplementation(() => Promise.reject('rejected'));
jest.mock('axios', () => ({
get: jest.fn().mockReturnValue(Promise.reject('error'))
}));
});
it('get fails', async () => {
await expect(() => {
API.get(GROUPS.url());
}).rejects.toEqual('error');
});
afterEach(() => {
jest.clearAllMocks();
});
});
You can mock behaviour of axios.get by using jest.mock. Put the below code above the describe section:
jest.mock('axios', () => ({
get: jest.fn().mockReturnValue(Promise.reject('error'))
}));
And you test the error like below:
it('get fails', async () => {
await expect(API.get("bad_url")).rejects.toEqual('error');
});
Exact Code
jest.mock('axios', () => ({
get: jest.fn().mockReturnValue(Promise.reject('error')),
}));
describe('API Throws Errors', () => {
it('get fails', async () => {
await expect(API.get(GROUPS.url())).rejects.toEqual('error');
});
});
Note:
If you have another test case that shouldnt be failed, you can just mock it to return Promise.resolve(). Or you can just simple clear the mock.
describe('API Throws Errors', () => {
it('get fails', async () => {
await expect(API.get(GROUPS.url())).rejects.toEqual('error');
});
it('should success', async () => {
Axios.get.mockReturnValue(Promise.resolve(SOME_VALUE));
await expect(API.get(GROUPS.url())).resolves.toEqual(SOME_VALUE);
});
});
toThrowError() is supposed to be asserted against a function and not result because if an error happens on function call, expect doesn't have a chance to be evaluated. It's applicable only to regular functions where an error is thrown. It's not applicable to async functions because they don't throw an error but return a result, which rejected promise.
rejects.toThrowError() construction is a way how rejected promise can be asserted, an assertion needs to be provided with a promise instead of a function that returns it:
await expect(API.get("bad_url")).rejects.toThrowError();
You can mock the axios for promise rejection like this
jest.mock('axios', () => ({
post: jest.fn(() => Promise.reject(new Error(''))),
get: jest.fn(() => Promise.reject(new Error(''))),
put: jest.fn(() => Promise.reject(new Error(''))),
delete: jest.fn(() => Promise.reject(new Error(''))),
}));
This method worked perfectly for me.
Hope this works for you also.

Mock Axios to test .catch()

I have been trying to write tests to test a axios call but now need to test the catch part.
I have been able to do the then by mocking axios like so but can't seem to get a way to test catch. I have followed many different examples from stack overflow and the web.
jest.mock('axios', () => jest.fn(() => Promise.resolve({ data: mockData })));
but that will always return a good result so can't test the catch. The bit of code I want to test is: goToUrl() is just a window.location.assign(url) but imported.
fetchBundlesFromApi(params)
.then(({ data: { bundles } }) => {
updateBundles(bundles);
this.setState({ showUpdatingPrices: false });
})
.catch(() => goToUrl(bundlesUrl));
In my test for .then() part I do this:
const fetchedBundles = await fetchBundlesFromApi(
'?params',
);
expect(fetchedBundles.data).toEqual(mockData);
However if I follow examples like this one Mocking Axios with Jest in React - mock function not being called I can't manually mock get if I put a mock axios file in a folder __mocks__ then a lot of the test suit fails so I just want to mock it in this one test file.
here is one of the examples I tried doing:
jest.mock('axios', () => ({
get: () => jest.fn(() => Promise.resolve({ data: mockData })),
default: () => jest.fn(() => Promise.resolve({ data: mockData })),
}));
but the tests error with TypeError: (0 , _axios.default) is not a function
EDIT:
Here is my fetchBundlesApi function:
const fetchBundlesFromApi = params => axios(`${bundleRoute}/bundles${params}`);
EDIT: catch test
it('should redirect if api fails', async () => {
const networkError = new Error('Some network error');
axios.mockRejectedValueOnce(networkError);
const goToUrl = jest.fn();
let error;
try {
await fetchBundlesFromApi('?params');
} catch (err) {
error = err;
}
expect(error).toEqual(networkError);
expect(goToUrl).toHaveBeenCalled();
});
in my component I import goToUrl like so:
import { goToUrl } from 'Helpers';
You can make use of Jests ability to pop implementations off once they've run i.e. mockImplementationOnce and friends.
import axios from 'axios';
jest.mock('axios');
// default implementation
axios.get.mockResolvedValue(mockedData);
describe('#fetchBundlesFromApi', () => {
it('returns data from API', async () => {
const fetchedBundles = await fetchBundlesFromApi('?params');
expect(fetchedBundles.data).toEqual(mockData);
});
it('redirects on failure', () => {
// override behaviour for this one call
axios.get.mockRejectedValueOnce();
// verify your failure test
});
});

How to test async function with spyOn?

I am trying to test an async function in a react native app.
class myClass extends React.Component {
...
closeModal = async () => {
if (someCondition) {
await myFunction1();
} else {
await myFunction2();
}
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('Main');
};
...
}
This is my test:
const navigation = {
navigate: jest.fn(),
state: { params: { onGoBack: jest.fn() } },
};
const renderComponent = overrides => {
props = {
navigation,
...overrides,
};
return shallow(< myClass.wrappedComponent {...props} />);
};
describe('When the user presses the close icon', () => {
it('should close the modal', () => {
const wrapper = renderComponent();
const instance = wrapper.instance();
const spyCloseModal = jest.spyOn(instance, 'closeModal');
instance().forceUpdate();
component
.find({ testID: 'close-icon' })
.props()
.onPress();
expect(spyCloseModal).toHaveBeenCalled(); // this is passed
expect(navigation.navigate).toHaveBeenCalled(); // this is not passed
});
});
It looks like it gets stuck on the await calls. If I remove the await calls then it passes. Someone mentioned in another post to use .and.callThrough after spyOn but it gives me this error
Cannot read property 'callThrough' of undefined
one of solution is to make your test async and run await (anything) to split your test into several microtasks:
it('should close the modal', async () => {
const wrapper = renderComponent();
component
.find({ testID: 'close-icon' })
.props()
.onPress();
await Promise.resolve();
expect(navigation.state.params.onGoBack).toHaveBeenCalled();
expect(navigation.navigate).toHaveBeenCalledWith("Main");
});
I believe you don't need either .forceUpdate nor .spyOn on instance method. once navigation happens properly it does not matter by what internal method it has been called
more on microtask vs macrotask: https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f
alternative is to use macrotask(setTimeout(...., 0))
it('should close the modal', (done) => {
const wrapper = renderComponent();
component
.find({ testID: 'close-icon' })
.props()
.onPress();
setTimeout(() => {
expect(navigation.state.params.onGoBack).toHaveBeenCalled();
expect(navigation.navigate).toHaveBeenCalledWith("Main");
done();
});
}
Yes, you're on the right track...the issue is that closeModal is asynchronous.
The await hasn't finished by the time execution returns to the test so this.props.navigation.navigate hasn't been called yet.
The test needs to wait for closeModal to complete before asserting that navigate has been called.
closeModal is an async function so it will return a Promise...
...and you can use the spy to retrieve the Promise it returns...
...then you can call await on that Promise in your test to make sure closeModal has completed before asserting that navigate has been called.
Here is a simplified working example to get you started:
import * as React from 'react';
import { shallow } from 'enzyme';
class MyClass extends React.Component {
closeModal = async () => {
await Promise.resolve();
this.props.navigation.navigate('Main');
}
render() { return <div onClick={() => this.closeModal()}></div> }
}
test('MyClass', async () => { // <= async test function
const props = { navigation: { navigate: jest.fn() }};
const wrapper = shallow(<MyClass {...props} />);
const instance = wrapper.instance();
const spyCloseModal = jest.spyOn(instance, 'closeModal');
wrapper.find('div').simulate('click');
expect(spyCloseModal).toHaveBeenCalled(); // Success!
const promise = spyCloseModal.mock.results[0].value; // <= get the Promise returned by closeModal
await promise; // <= await the Promise
expect(props.navigation.navigate).toHaveBeenCalled(); // Success!
})
Note the use of mockFn.mock.results to get the Promise returned by closeModal.

Categories

Resources