Skip to content

[feat] Jest mocks, AsyncStorage testing guidelines #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 8, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,7 @@ jobs:
- run:
name: Wake device
command: |
adb shell input keyevent
adb shell input keyevent 82 &
adb shell input keyevent 82

- run:
name: Run e2e tests
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ getData = async () => {

See docs for [api and more examples.](docs/API.md)

## Writing tests

Using [Jest](https://jestjs.io/) for testing? Make sure to check out [docs on how to integrate it with this module.](./docs/Jest-integration.md)

## Contribution

See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out.
Expand Down
77 changes: 77 additions & 0 deletions docs/Jest-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Jest integration

Async Storage module is tighly coupled with a `Native Module`, meaning it needs a running React Native application to work properly. In order to use it in tests, you have to provide its separate implementation. Follow those steps to add mocked `Async Storage` to your test cases.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Native Module -> NativeModule


## Using Async Storage mock

Select a method that suits your needs:

### Mock `node_modules`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to With __mocks__ directory


1. In your project root directory, create `__mocks__/@react-native-community` directory.
2. Inside that folder, create `async-storage.js` file.
3. Inside that file, export `Async Storage` mock.

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

### Use Jest setup files

1. In your Jest config (probably in `package.json`) add setup files location:

```json
"jest": {
"setupFiles": ["./path/to/jestSetupFile.js"]
}
```

2. Inside your setup file, set up Async Storage mocking:

```javascript
import mockAsyncStorage from '@react-native-community/async-storage/jest/async-storage-mock';

jest.mock('@react-native-community/async-storage', () => mockAsyncStorage);
```
## Testing with mock

Each public method available from `Async Storage` is [a mock function](https://jestjs.io/docs/en/mock-functions), that you can test for certain condition, for example, check if it has been called with a specific arguments:

```javascript
it('checks if Async Storage is used', async () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder why the indentation? Are you using Prettier to format that? If not, I definitely recommend

await asyncOperationOnAsyncStorage();

expect(AsyncStorage.getItem).toBeCalledWith('myKey');
})
```

## Overriding Mock logic

You can override Async Storage mock implementation, by replacing its inner functions:

```javascript
// somewhere in your configuration files
import AsyncStorageMock from '@react-native-community/async-storage/jest/async-storage-mock';

AsyncStorageMock.multiGet = jest.fn(([keys], callback) => {
// do something here to retrieve files
callback([]);
})

export default AsyncStorageMock;
```

You can [check mock implementation](../jest/async-storage-mock.js) to get more insight into its signatures.

## Troubleshooting

### **`SyntaxError: Unexpected token export` in async-storage/lib/index.js**

This is likely because `Jest` is not transforming Async Storage. You can point it to do so, by adding `transformIgnorePatterns` setting in Jest's configuration.


```json
"jest": {
"transformIgnorePatterns": ["/node_modules/@react-native-community/async-storage/(?!(lib))"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Make it valid JSON
  2. Use a catch-all regex for all @react-native-community packages to help others :D :
"transformIgnorePatterns": ["node_modules/(?!react-native|@react-native-community)"]
  1. Make a note that this should be included by default in RN 0.60

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this addressed, mind fixing?

}
```
156 changes: 149 additions & 7 deletions example/__tests__/App.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,157 @@
/**
* @format
* @lint-ignore-every XPLATJSCOPYRIGHT1
*/
/* eslint-disable no-shadow */

import 'react-native';
import React from 'react';
import App from '../App';

// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
import AsyncStorage from '@react-native-community/async-storage';

it('renders correctly', () => {
renderer.create(<App />);
describe('Async Storage mock functionality', () => {
describe('Promise based', () => {
it('can read/write data to/from storage', async () => {
const newData = Math.floor(Math.random() * 1000);

await AsyncStorage.setItem('key', newData);

const data = await AsyncStorage.getItem('key');

expect(data).toBe(newData);
});

it('can clear storage', async () => {
await AsyncStorage.setItem('temp_key', Math.random() * 1000);

let currentValue = await AsyncStorage.getItem('temp_key');

expect(currentValue).not.toBeNull();

await AsyncStorage.clear();

currentValue = await AsyncStorage.getItem('temp_key');

expect(currentValue).toBeNull();
});

it('can clear entries in storage', async () => {
await AsyncStorage.setItem('random1', Math.random() * 1000);
await AsyncStorage.setItem('random2', Math.random() * 1000);

let data1 = await AsyncStorage.getItem('random1');
let data2 = await AsyncStorage.getItem('random2');

expect(data1).not.toBeNull();
expect(data2).not.toBeNull();

await AsyncStorage.removeItem('random1');
await AsyncStorage.removeItem('random2');
data1 = await AsyncStorage.getItem('random1');
data2 = await AsyncStorage.getItem('random2');
expect(data2).toBeNull();
expect(data1).toBeNull();
});

it('can use merge with current data in storage', async () => {
let originalPerson = {
name: 'Jerry',
age: 21,
characteristics: {
hair: 'black',
eyes: 'green',
},
};

await AsyncStorage.setItem('person', JSON.stringify(originalPerson));

originalPerson.name = 'Harry';
originalPerson.characteristics.hair = 'red';
originalPerson.characteristics.shoeSize = 40;

await AsyncStorage.mergeItem('person', JSON.stringify(originalPerson));

const currentPerson = await AsyncStorage.getItem('person');
const person = JSON.parse(currentPerson);

expect(person).toHaveProperty('name', 'Harry');
expect(person.characteristics).toHaveProperty('hair', 'red');
expect(person.characteristics).toHaveProperty('shoeSize', 40);
});
});

describe('Callback based', () => {
it('can read/write data to/from storage', done => {
const newData = Math.floor(Math.random() * 1000);

AsyncStorage.setItem('key', newData, function() {
AsyncStorage.getItem('key', function(_, value) {
expect(value).toBe(newData);
done();
}).catch(e => done.fail(e));
});
});
it('can clear storage', done => {
AsyncStorage.setItem('temp_key', Math.random() * 1000, () => {
AsyncStorage.getItem('temp_key', (_, currentValue) => {
expect(currentValue).not.toBeNull();
AsyncStorage.clear(() => {
AsyncStorage.getItem('temp_key', (_, value) => {
expect(value).toBeNull();
done();
}).catch(e => done.fail(e));
});
}).catch(e => done.fail(e));
});
});

it('can clear entries in storage', done => {
AsyncStorage.setItem('random1', Math.random() * 1000, () => {
AsyncStorage.setItem('random2', Math.random() * 1000, () => {
AsyncStorage.getItem('random1', (_, data1) => {
AsyncStorage.getItem('random2', (_, data2) => {
expect(data1).not.toBeNull();
expect(data2).not.toBeNull();

AsyncStorage.removeItem('random1', () => {
AsyncStorage.removeItem('random2', () => {
AsyncStorage.getItem('random1', (_, value1) => {
AsyncStorage.getItem('random2', (_, value2) => {
expect(value1).toBeNull();
expect(value2).toBeNull();
done();
}).catch(e => done.fail(e));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're using callback approach, but here taking advantage of getItem returning a promise. You should use the first argument (named _ instead of err or error) and check if it's not null, then call done.fail

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why I'm catching some of the block is that error thrown in callback methods are swallowed by Jest - meaning it'll get timeout singal, without specific reason why caused that.

Note that I only wrap blocks with assertions inside - I couldn't find nicer way to do callbacks and still get errors when assertion fail

});
});
});
}).catch(e => done.fail(e));
});
});
});
});

it('can use merge with current data in storage', done => {
let originalPerson = {
name: 'Jerry',
age: 21,
characteristics: {
hair: 'black',
eyes: 'green',
},
};

AsyncStorage.setItem('person', JSON.stringify(originalPerson), () => {
originalPerson.name = 'Harry';
originalPerson.characteristics.hair = 'red';
originalPerson.characteristics.shoeSize = 40;
AsyncStorage.mergeItem('person', JSON.stringify(originalPerson), () => {
AsyncStorage.getItem('person', (_, currentPerson) => {
const person = JSON.parse(currentPerson);
expect(person).toHaveProperty('name', 'Harry');
expect(person.characteristics).toHaveProperty('hair', 'red');
expect(person.characteristics).toHaveProperty('shoeSize', 40);
done();
}).catch(e => done.fail(e));
});
});
});
});
});
File renamed without changes.
3 changes: 2 additions & 1 deletion example/e2e/config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"setupFilesAfterEnv": ["./init.js"],
"testEnvironment": "node"
"testEnvironment": "node",
"testMatch": [ "**/?(*.)+(e2e).[jt]s?(x)" ]
}
7 changes: 7 additions & 0 deletions example/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @format
*/

import mockAsyncStorage from '../jest/async-storage-mock';

jest.mock('@react-native-community/async-storage', () => mockAsyncStorage);
Loading