Hey guys !
I tried to mock async storage by applying what is written in the "jest integration" section.
i'm getting Cannot read property 'getItem' of undefined when running tests. The app works without any problem when launched, the issue only appears when running tests.
I am not testing directly async storage in my tests, I am testing a component that uses AsyncStorage.
The test:
it('LoginDeleteAccount renders correctly', () => {
const { toJSON } = render(<MockedNavigator component={LoginDeleteAccount} />)
expect(toJSON()).toMatchSnapshot()
})
Inside the component LoginDeleteAccount:
async retrieveEmails() {
try {
const emails_array = await AsyncStorage.getItem('emails')
...
} catch (error) {
console.log(error)
}
}
As for the mocking, I have a index.js file in __mocks__/@react-native-community/async-storage.
In it:
export default from '@react-native-community/async-storage/jest/async-storage-mock'
Any ideas ? thanks in advance !
Test runs without any errors
TypeError: Cannot read property 'getItem' of undefinedHey,
Have you tried mocking it using setup file method?
Yes, same result. I tried both methods.
Can you provide a repo that can reproduce the issue? That'd be super useful for fixing the issue
Hmm the one i'm working on is for a company so it's private. I'll have to create another one. I don't have enough time for that right now but I will as soon as i'm less busy !
@Krizzu Hmm weird:
TypeError: Cannot read property 'getItem' of undefined. The 4 tests are in the same file:describe('Login', () => {
afterEach(cleanup)
it('LoginNewAccount renders correctly', () => {
const { toJSON } = render(<MockedNavigator component={LoginNewAccount} />)
expect(toJSON()).toMatchSnapshot()
})
it('LoginAddAccount renders correctly', () => {
const { toJSON } = render(<MockedNavigator component={LoginAddAccount} />)
expect(toJSON()).toMatchSnapshot()
})
it('LoginExistingAccount renders correctly', () => {
const { toJSON } = render(<MockedNavigator component={LoginExistingAccount} />)
expect(toJSON()).toMatchSnapshot()
})
it('LoginDeleteAccount renders correctly', () => {
const { toJSON } = render(<MockedNavigator component={LoginDeleteAccount} />)
expect(toJSON()).toMatchSnapshot()
})
})
Both LoginAddAccount and LoginDeleteAccount use AsyncStorage, but only the first one passes. Is it possible that after LoginAddAccount test passes, AsyncStorage mock is somehow consumed and deleted ?
I tried:
beforeEach(() => {
jest.mock('@react-native-community/async-storage', () => MockedAsyncStorage)
})
but didn't change anything. I also tried commenting out the cleanup function but still the same issue.
Here's a link to the repo where I tried to reproduce the issue:
https://github.com/Alaa-Ben/reeact-native-mock-asyn-storage
I couldn't reproduce it so not sure it's useful, but it may help.
Seems like your setup somehow removes the mock after the first usage. Do you have any jest setup files?
cc @thymikee - any idea what potentially might cause this behavior?
You need to call jest.mock in the module scope, not in beforeEach or test because it's hoisted to the top of the block.
@Krizzu Jest setup file is empty.
@thymikee I added jest.mock('@react-native-community/async-storage', () => MockedAsyncStorage) in the module scope.
Now I get: cannot read property 'default' of undefined.
In __mocks__/@react-native-community/async-storage:
export default from '@react-native-community/async-storage/jest/async-storage-mock'
In __tests__/login-test.js:
import MockedAsyncStorage from '../__mocks__/@react-native-community/async-storage'
jest.mock('@react-native-community/async-storage', () => MockedAsyncStorage)
I don't even understand why I should call jest.mock in my test file. For all other native modules, having a folder in __mocks__ with the same name as the package is enough to mock it.
Shouldn't your __mocks__ file export:
export {default} from '@react-native-community/async-storage/jest/async-storage-mock'
?
FYI, you should never import from __mocks__ directory, Jest will find this mock automatically when you import the module and use it instead of an original module.
Shouldn't your __mocks__ file export:
export {default} from '@react-native-community/async-storage/jest/async-storage-mock'
?
For the import, I just followed the docs: https://react-native-community.github.io/async-storage/docs/advanced/jest
I tried adding the {default} but still the same problem: Cannot read property 'getItem' of undefined
FYI, you should never import from __mocks__ directory, Jest will find this mock automatically when you import the module and use it instead of an original module.
Yeah I know, it works for all the other mocks I have, I juste added the jest.mock in an attempt to understand why it doesn't work, I took it out after that.
Based on the OP you seem to use different setup. Here's what docs say:
- In your project root directory, create __mocks__/@react-native-community directory.
- Inside that folder, create async-storage.js file.
While you created index.js file. I don't think this should make a difference (and if it does, it's a Jest bug, make sure you're on the latest version), but please make sure you follow the docs exactly as they are.
I tried both methods: with index.js and with async-storage.js. I also tried {default} on both files. Same issue. And I am using the latest version (26.0.1, FYI the index.js method works with the other mocks I have).
Please provide a minimal repro we can download then.
https://github.com/Alaa-Ben/reeact-native-mock-asyn-storage
npm install && npm test reproduces the issue
I think I am having the same issue when using AsyncStorage through redux-persist.
/Volumes/sites/clim8/mobile-app/node_modules/redux-persist/lib/createPersistoid.js:98
writePromise = storage.setItem(storageKey, serialize(stagedState)).catch(onWriteFail);
^
TypeError: Cannot read property 'catch' of undefined
@Alaa-Ben
The error you see (getItem of undefined) does not come from AsyncStorage mock. It's from FlatList component (check stack trace).
Dunno what causes that, but as workaround, you can mock FlatList with RN's ScrollView
jest.mock('react-native/Libraries/Lists/FlatList', () => {
const RN = jest.requireActual('react-native');
return RN.ScrollView;
});
Hmm turns out it was passing an empty array to Flatlist. FlatList actually works, no need to mock it manually. Thanks all of you for your help ! I got mislead by the "getItem" keyword :'(
@MartinCerny-awin make sure to use jest.useFakeTimers(); as redux-persist uses intervals internally.
I temporarily solved cannot read .catch of undefined when using redux-persist.
Instead of using the ready-made mocks, I manually added this to my jest.setup.ts
jest.mock('@react-native-community/async-storage', () => {
return {
getItem: async (...args) => args,
setItem: async (...args) => args,
removeItem: async (...args) => args,
};
});
Or, better. Copy and paste this to your project's __mocks__(create this directory if it does not exist)
__mocks__/@react-native-community/async-storage/index.js
const asMock = {
__INTERNAL_MOCK_STORAGE__: {},
setItem: async (key, value, callback) => {
const setResult = await asMock.multiSet([[key, value]], undefined);
callback && callback(setResult);
return setResult;
},
getItem: async (key, callback) => {
const getResult = await asMock.multiGet([key], undefined);
const result = getResult[0] ? getResult[0][1] : null;
callback && callback(null, result);
return result;
},
removeItem: (key, callback) => asMock.multiRemove([key], callback),
mergeItem: (key, value, callback) =>
asMock.multiMerge([[key, value]], callback),
clear: _clear,
getAllKeys: _getAllKeys,
flushGetRequests: () => {},
multiGet: _multiGet,
multiSet: _multiSet,
multiRemove: _multiRemove,
multiMerge: _multiMerge,
};
async function _multiSet(keyValuePairs, callback) {
keyValuePairs.forEach((keyValue) => {
const key = keyValue[0];
asMock.__INTERNAL_MOCK_STORAGE__[key] = keyValue[1];
});
callback && callback(null);
return null;
}
async function _multiGet(keys, callback) {
const values = keys.map((key) => [
key,
asMock.__INTERNAL_MOCK_STORAGE__[key] || null,
]);
callback && callback(null, values);
return values;
}
async function _multiRemove(keys, callback) {
keys.forEach((key) => {
if (asMock.__INTERNAL_MOCK_STORAGE__[key]) {
delete asMock.__INTERNAL_MOCK_STORAGE__[key];
}
});
callback && callback(null);
return null;
}
async function _clear(callback) {
asMock.__INTERNAL_MOCK_STORAGE__ = {};
callback && callback(null);
return null;
}
async function _getAllKeys() {
return Object.keys(asMock.__INTERNAL_MOCK_STORAGE__);
}
async function _multiMerge(keyValuePairs, callback) {
keyValuePairs.forEach((keyValue) => {
const key = keyValue[0];
const value = JSON.parse(keyValue[1]);
const oldValue = JSON.parse(asMock.__INTERNAL_MOCK_STORAGE__[key]);
asMock.__INTERNAL_MOCK_STORAGE__[key] = JSON.stringify(
_deepMergeInto(oldValue, value),
);
});
callback && callback(null);
return null;
}
const _isObject = (obj) => typeof obj === 'object' && !Array.isArray(obj);
const _deepMergeInto = (oldObject, newObject) => {
const newKeys = Object.keys(newObject);
const mergedObject = oldObject;
newKeys.forEach((key) => {
const oldValue = mergedObject[key];
const newValue = newObject[key];
if (_isObject(oldValue) && _isObject(newValue)) {
mergedObject[key] = _deepMergeInto(oldValue, newValue);
} else {
mergedObject[key] = newValue;
}
});
return mergedObject;
};
export default asMock;
I don't know why wrapping the async functions with jest.fn(async () => {}) does not work.
@karlmarxlopez thanks man, i've spent hours on this and your solution really worked for me.
Most helpful comment
I temporarily solved
cannot read .catch of undefinedwhen usingredux-persist.Instead of using the ready-made mocks, I manually added this to my
jest.setup.tsOr, better. Copy and paste this to your project's
__mocks__(create this directory if it does not exist)__mocks__/@react-native-community/async-storage/index.jsI don't know why wrapping the async functions with
jest.fn(async () => {})does not work.