34

I'm building an app with React Native. I want to minimize how often I communicate to the database, so I make heavy use of AsyncStorage. There's a lot of room for bugs in the translation between DB and AsyncStorage though. Therefore, I want to make sure that AsyncStorage has the data I believe it does by running automated tests against it. Surprisingly, I haven't found any information on how to do that online. My attempts to do it on my own haven't worked out.

Using Jest:

it("can read asyncstorage", () => {
return AsyncStorage.getItem('foo').then(foo => {
  expect(foo).not.toBe("");
});  });

This method failed with an error:

TypeError: RCTAsyncStorage.multiGet is not a function

Removing the return will cause it to run instantly without waiting for the value and improperly pass the test.

I got hit with the exact same error when I tried to test it using the await keyword:

it('can read asyncstorage', async () => {
this.foo = "";
await AsyncStorage.getItem('foo').then(foo => {
    this.foo = foo;
});
expect(foo).not.toBe(""); });

Any suggestions on how to successfully run assertions against the values in AsyncStorage? I'd prefer to continue using Jest but if it can only be done with some alternate testing library I'm open to that.

David Schumann
  • 13,380
  • 9
  • 75
  • 96
Brian Case
  • 373
  • 1
  • 3
  • 7

7 Answers7

54

For everyone who sees this question in > 2019:

Since Nov 2020, AsyncStorage was renamed back to @react-native-async-storage/async-storage", which causes this warning to appear if you're importing it from react-native:

Warning: Async Storage has been extracted from react-native core and will be removed in a future release.

The new module includes its own mock, so you don't have to worry about writing your own anymore.

Per the project's documentation, you can set it up in 2 different ways:

With mocks directory

  • In your project root directory, create a __mocks__/@react-native-community directory.
  • Inside that folder, create async-storage.js file.
  • Inside that file, export Async Storage mock.
    export default from '@react-native-async-storage/async-storage/jest/async-storage-mock'
    

Jest should then mock AsyncStorage by default in all your tests. If it doesn't, try calling jest.mock(@react-native-async-storage/async-storage) at the top of your test file.

With Jest setup file

  • In your Jest config (probably in package.json or jest.config.js) add the setup file's location:
    "jest": {
      "setupFiles": ["./path/to/jestSetupFile.js"]
    }
    
  • Inside your setup file, set up the AsyncStorage mock:
    import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
    
    jest.mock('@react-native-community/async-storage', () => mockAsyncStorage);
    

If you're using TypeScript, using the 2nd option (Jest setup file) is way easier, since with the 1st one (mocks directory) it won't associate @types/react-native-community__async-storage with the mock automatically.

David Castillo
  • 4,266
  • 4
  • 23
  • 25
  • 1
    I don't see any `/jest` folder inside the `async-storage` folder :( jest is throwing an error that it can find the path you specified – betoharres Apr 21 '19 at 05:47
  • 1
    @betoharres, are you using `@react-native-community/async-storage` or React Native's built-in `AsyncStorage`? – David Castillo Apr 22 '19 at 14:44
  • 1
    @david-catillo Yes! I was like 1 version behind from them to add the mock to the library. Ref to my solution here: https://github.com/react-native-community/react-native-async-storage/issues/39#issuecomment-481971075 – betoharres Apr 23 '19 at 02:12
  • I also don't see a jest folder in my `@react-native-community/async-storage` folder. Just a `node_modules` folder. And I'm on v1.3.4 which appears to be current. – Jonathan Tuzman May 07 '19 at 19:31
  • 1
    Note: the package mentioned above does not work with an expo project yet: https://github.com/react-native-community/async-storage/issues/172 – Alon Dahari Sep 01 '19 at 19:47
  • 1
    For Expo: Here is a simple way to stub out AsyncStorage either in your test or in your jest setup file: jest.mock("@react-native-community/async-storage", () => ({ AsyncStorage: { clear: jest.fn().mockName("clear"), getAllKeys: jest.fn().mockName("getAllKeys"), getItem: jest.fn().mockName("getItem"), removeItem: jest.fn().mockName("removeItem"), setItem: jest.fn().mockName("setItem") } })); – sturdynut Jan 12 '20 at 19:10
26

May be you can try something like this:

mockStorage.js

export default class MockStorage {
  constructor(cache = {}) {
    this.storageCache = cache;
  }

  setItem = jest.fn((key, value) => {
    return new Promise((resolve, reject) => {
      return (typeof key !== 'string' || typeof value !== 'string')
        ? reject(new Error('key and value must be string'))
        : resolve(this.storageCache[key] = value);
    });
  });

  getItem = jest.fn((key) => {
    return new Promise((resolve) => {
      return this.storageCache.hasOwnProperty(key)
        ? resolve(this.storageCache[key])
        : resolve(null);
    });
  });

  removeItem = jest.fn((key) => {
    return new Promise((resolve, reject) => {
      return this.storageCache.hasOwnProperty(key)
        ? resolve(delete this.storageCache[key])
        : reject('No such key!');
    });
  });

  clear = jest.fn((key) => {
    return new Promise((resolve, reject) =>  resolve(this.storageCache = {}));
  });

  getAllKeys = jest.fn((key) => {
    return new Promise((resolve, reject) => resolve(Object.keys(this.storageCache)));
  });
}

and inside your test file:

import MockStorage from './MockStorage';

const storageCache = {};
const AsyncStorage = new MockStorage(storageCache);

jest.setMock('AsyncStorage', AsyncStorage)

// ... do things
David Schumann
  • 13,380
  • 9
  • 75
  • 96
yadhu
  • 15,423
  • 7
  • 32
  • 49
  • 4
    Getting 'Cannot find module 'AsyncStorage' from 'myFileName.js' ' , in spite of trying to import it from 'react-native' - any ideas? :) – jhm Jul 10 '17 at 12:47
  • Should be accepted answer since it most clearly answers the origin question – mcabe Feb 27 '18 at 14:53
  • How do you clear the `storageCache` ? I tried `beforeEach` with `await AsyncStorage.clear()` but it does not affect the `storageCache` of the mock – Orelus Jun 05 '18 at 07:34
  • 2
    I am using this answer as solution, but I'm having the same issue as @jhm. Any progress? Thanks. – RCohen Jan 08 '20 at 16:19
21

My original answer just pointed at how the author of react-native-simple-store had dealt with the mocking. I've updated my answer with my own mocking that removes Jason's hard-coded mock responses.

Jason Merino has a nice simple approach to this in https://github.com/jasonmerino/react-native-simple-store/blob/master/tests/index-test.js#L31-L64

jest.mock('react-native', () => ({
AsyncStorage: {
    setItem: jest.fn(() => {
        return new Promise((resolve, reject) => {
            resolve(null);
        });
    }),
    multiSet:  jest.fn(() => {
        return new Promise((resolve, reject) => {
            resolve(null);
        });
    }),
    getItem: jest.fn(() => {
        return new Promise((resolve, reject) => {
            resolve(JSON.stringify(getTestData()));
        });
    }),
    multiGet: jest.fn(() => {
        return new Promise((resolve, reject) => {
            resolve(multiGetTestData());
        });
    }),
    removeItem: jest.fn(() => {
        return new Promise((resolve, reject) => {
            resolve(null);
        });
    }),
    getAllKeys: jest.fn(() => {
        return new Promise((resolve) => {
            resolve(['one', 'two', 'three']);
        });
    })
  }
}));

My own mock:

const items = {};

jest.mock('react-native', () => ({

AsyncStorage: {        

    setItem: jest.fn((item, value) => {
        return new Promise((resolve, reject) => {        
    items[item] = value;
            resolve(value);
        });
    }),
    multiSet:  jest.fn((item, value) => {
        return new Promise((resolve, reject) => {
    items[item] = value;
            resolve(value);
        });
    }),
    getItem: jest.fn((item, value) => {
        return new Promise((resolve, reject) => {
            resolve(items[item]);
        });
    }),
    multiGet: jest.fn((item) => {
        return new Promise((resolve, reject) => {
            resolve(items[item]);
        });
    }),
    removeItem: jest.fn((item) => {
        return new Promise((resolve, reject) => {
            resolve(delete items[item]);
        });
    }),
    getAllKeys: jest.fn((items) => {
        return new Promise((resolve) => {
            resolve(items.keys());
        });
    })
  }
}));
David Schumann
  • 13,380
  • 9
  • 75
  • 96
jaygooby
  • 2,436
  • 24
  • 42
  • This solution will work only for your use case, the solution below should be the accepted one! – Ouadie Jan 23 '17 at 14:06
  • @Ouadie it needs editing so `multiGet` and `multiSet` are defined. @brian-case was asking specifically about `multiGet`. I'll dig out how I ended up mocking mine, avoiding the hard-coded responses to `resolve()` – jaygooby Jan 23 '17 at 14:55
  • Getting 'Cannot find module 'AsyncStorage' from 'react-native-implementation.js' ' in my test when using your mock - any idea why? :) @jaygooby – jhm Jul 10 '17 at 12:44
  • @jaygooby, instead of `items.keys()` should be `Object.keys(items)` – DTupalov Nov 23 '17 at 07:27
  • 2
    Because of the `const items = {}`: `The module factory of 'jest.mock()' is not allowed to reference any out-of-scope variables. Invalid variable access: items`. – JulienD Mar 29 '18 at 13:26
  • 1
    Thanks, FYI you might wanna update your answer since AsyncStorage has been moved out of 'react-native'. From the link you pasted: ``` import AsyncStorage from '@react-native-community/async-storage'; jest.mock('@react-native-community/async-storage', () => ({ ``` – MCH Sep 17 '19 at 05:51
3

Install the module using the command : Run this command from the root directory of the project.

npm install --save mock-async-storage

In the project root directory create __mocks__\@react-native-community folder. Inside that create a file async-storage.js. Code in async-storage.js

export default from '@react-native-community/async-storage/jest/async-storage-mock'

Inside package.json configure the jest as follows:

"jest": {
    "preset": "jest-expo",
    "transformIgnorePatterns" : ["/node_modules/@react-native-community/async-storage/(?!(lib))"]
  },

Here I am using jest-expo for testing. If you are using jest then the preset value will be jest not jest-expo.

In the project root directory create a file called jest.config.js Configuration inside the jest.config.js file:

module.exports = {
    setupFilesAfterEnv: [
      './setup-tests.js',
    ],
  };

In the project root directory create a file called setup-tests.js. Code in this file is :

import MockAsyncStorage from 'mock-async-storage';

const mockImpl = new MockAsyncStorage();
jest.mock('@react-native-community/async-storage', () => mockImpl);

In the project root directory create the test file. Here I am calling it Example.test.js.

import  AsyncStorage  from '@react-native-community/async-storage';

beforeEach(() => {
    AsyncStorage.clear();
    // console.log(`After the data is being reset :`)
    // console.log(AsyncStorage)
});

it('can read asyncstorage', async () => {

    await AsyncStorage.setItem('username', 'testUser')
    let usernameValue = await AsyncStorage.getItem('username')
    // console.log(`After the data is being set :`)
    // console.log(AsyncStorage)
    expect(usernameValue).toBe('testUser')
});

Here setting the value of username to testUser using AsyncStorage.setItem. Then fetching the value using getItem function. The test case is to compare whether the usernameValue is equal to testUser. If yes then the test case passes else the test case will fail.

Using beforeEach so that every time a test case is being run Asyncstorage is being cleared and is empty. If needed one can check what is present in Asyncstorage using console.log

Run the command yarn test to run the tests. The output is:

enter image description here

Teja Goud Kandula
  • 1,462
  • 13
  • 26
1

I think jest.setMock could be in this case better than jest.mock so we can use react-native with no problem just mocking the AsyncStorage like that:

jest.setMock('AsyncStorage', {
  getItem: jest.fn(
    item =>
      new Promise((resolve, reject) => {
        resolve({ myMockObjectToReturn: 'myMockObjectToReturn' });
      })
  ),
});
robertovg
  • 1,018
  • 11
  • 16
1

1- add folder __mocks__ in root project

2- add folder @react-native-async-storage in __mocks__ folder

3- add file async-storage.js in @react-native-async-storage

4- add code:

let db = {};

export default {
    setItem: (item, value) => {
        return new Promise((resolve, reject) => {
            db[item] = value;
            resolve(value);
        });
    },
    multiSet: (item, fun) => {
        return new Promise((resolve, reject) => {
            for (let index = 0; index < item.length; index++) {
                db[item[index][0]] = item[index][1];
            }
            fun()
            resolve(value);
        });
    },
    getItem: (item, value= null) => {
        return new Promise((resolve, reject) => {
            resolve(db[item]);
        });
    },
    multiGet: (item) => {
        return new Promise((resolve, reject) => {
            resolve(db[item]);
        });
    },
    removeItem: (item) => {
        return new Promise((resolve, reject) => {
            resolve(delete db[item]);
        });
    },
    getAllKeys: (db) => {
        return new Promise((resolve) => {
            resolve(db.keys());
        });
    }
}
0

I just ran into this issue running jest with latest version of Expo and solved it following the 'jest setup file' option in the directions at https://react-native-async-storage.github.io/async-storage/docs/advanced/jest/.

Note that AsyncStorage was (for whatever reason) renamed\moved back to "@react-native-async-storage/async-storage" and no longer contains "community" in its package name.

cchapin
  • 180
  • 1
  • 11