Async-storage: [Jest] Mocked async storage

Created on 4 Jun 2020  路  20Comments  路  Source: react-native-async-storage/async-storage

Current behavior

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 !

Expected behavior

Test runs without any errors

Repro steps

  • npm test

Environment

  • Async Storage version: 1.11.0
  • React-Native version: 0.62.2
  • Platform tested: Jest(terminal)/iOS / Android
  • Logs/Error that are relevant:
    error: TypeError: Cannot read property 'getItem' of undefined

Most helpful comment

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.

All 20 comments

Hey,

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:

  1. Couldn't reproduce on a new repo
  2. I have 4 components, 2 of them use AsyncStorage, all tests pass but the last one with 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:

  1. In your project root directory, create __mocks__/@react-native-community directory.
  2. 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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dominiczaq picture dominiczaq  路  22Comments

sumanthyedoti picture sumanthyedoti  路  25Comments

Waqas-Jani picture Waqas-Jani  路  28Comments

hms111111 picture hms111111  路  24Comments

rogueturnip picture rogueturnip  路  27Comments