Skip to content

Instantly share code, notes, and snippets.

@danReynolds
Created October 24, 2017 19:01
Show Gist options
  • Save danReynolds/ffab8050c6432c63afbdcdccf8afd31b to your computer and use it in GitHub Desktop.
Save danReynolds/ffab8050c6432c63afbdcdccf8afd31b to your computer and use it in GitHub Desktop.
ava-to-jest
# Moving from Ava to Jest
---
## Pros and Cons
Benefits:
- Locally it has sped up tests from about 40-50 seconds for Ava, to 8-10 for jest
- On Travis it has cut test runs down about 9-10 minutes
- My system doesn't freeze up from Ava memory spiking and making my fans run like crazy :)
- Allows for nested tests
- Includes own mocking library based on Jasmine
- Allows for interactive test debugging
- Snapshot testing
- Made by Facebook and has a strong React/React-native community + better docs
Negatives:
You need to know a couple different ways to mock things and when to use each, hopefully this can address that though.
---
## New Testing Scripts
The Ava test scripts in the `package.json` have been replaced with the following:
```json
// basic testing
"test": "jest",
// debugging tests
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
// do not show anything output to console, hides warnings
"test:silent": "jest --silent",
// enter Jest's interactive test watching
"test:watch": "jest --watch",
// only run tests related to files staged files
"test:change": "jest --onlyChanged",
// stop running the tests once an error is encountered
"test:failfast": "jest --bail",
// Run the tests and output coverage using at most 4 workers since travis has limited
// concurrency
"coverage": "npm run test:silent -- --coverage --maxWorkers=4",
// Jest will output coverage data to `lcov.info`, send that to coveralls
"coveralls": "npm run coverage && cat ./coverage/lcov.info | coveralls",
```
All the commands support sending a path as an argument for running specific tests. My preference
is for doing `npm run test:silent` because it makes seeing the tests that passed easier,
we could consider using it as the default going forward.
---
## Debugging Tests
What the heck is `node --inspect-brk node_modules/.bin/jest --runInBand`?
You can visit the node docs [here](https://nodejs.org/en/docs/inspector/).
`inspect-brk` basically enables the inspctor and then we run jest with the `runInBand` flag,
which makes tests run synchronously since the debugging doesn't work well with multiple processes.
It is described in more detail [here](http://facebook.github.io/jest/docs/en/troubleshooting.html).
Steps to debugging:
1. To use it drop a debugger in the file you want to go to
2. Run `npm test:debug` optionally with a path to the specific file
3. Open `chrome://inspect` and click the node session listed there
4. Now with this tab open it will take you to the inspector every time you run `test:debug`
5. Debug like normally!
Note that this requires node > 8 so to get this feature and general performance improvements bump to
node `v8.7.0` from https://nodejs.org/en/
---
## How to Test with Jest
Jest has [good documentation](http://facebook.github.io/jest/docs/en/api.html). The core mocking features are:
* `jest.fn`
* `jest.spyOn`
* `jest.mock`
* `jest.mock`
* `jest.mockClear`
* `jest.mockRestore`
* `jest.doMock`
* `jest.resetModules`
as well as lifecycle hooks like `beforeEach`, `afterEach`, `beforeAll`, `afterAll` and test groupings
using `describe`.
---
## Using jest.mock
`jest.mock` is used to mock out the implementation of a module.
It takes a second argument that is the replacement implementation, which is what we're now using in our `Test/Setup.js` like we were with mockery to replace implementations:
```javascript
jest.mock('@nerdwallet/nwa', () => ({
NWA: () => ({
track: (eventName, eventProps, callback) => {
if (callback) process.nextTick(callback);
},
trackPageView: () => jest.fn(),
generatePageViewId: () => jest.fn(),
setGlobalProp: () => jest.fn(),
enableLogger: () => jest.fn(),
getGlobalProps: () => jest.fn(),
getUserProps: () => jest.fn(),
}),
}));
```
---
## Using jest.fn
`jest.fn` returns a mock function that is used in the `Tests/Setup.js` a lot as the mock out
the implementation of modules. You can pass it a return value like shown here:
```javascript
const mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalled();
// With a mock implementation:
const returnsTrue = jest.fn(() => true);
console.log(returnsTrue()); // true;
```
I don't use it much outside of the test setup file though because it doesn't provide a way to restore
the original implementation of functions, so you'd need to keep a reference to the original.
---
## Using jest.spyOn
`jest.spyOn` is what I recommend using most, it creates a mock function similar to `jest.fn` but also
tracks calls to the function and supports `mockClear` and `mockRestore` for reseting calls to the mock and restoring the original implementation of what you mocked.
In the `test-apptentive` file I used it before all tests to mock out `NWApptentive`:
```javascript
jest.spyOn(NWApptentive, 'engage').mockReturnValue(Promise.resolve(true));
jest.spyOn(NWApptentive, 'setUserInfo').mockReturnValue(Promise.resolve(true));
jest.spyOn(Utilities, 'logError');
```
First specify the module, then the method to mock off of it as a string and use mock methods like `mockReturnValue` or `mockImplementation` or any others from [here](http://facebook.github.io/jest/docs/en/mock-function-api.html#mockfnmockreturnvaluevalue).
---
## Example from test-apptentive
```javascript
test('apptentive middleware should clear user\'s info on signout', () => {
const action = signout.success();
store.dispatch(action);
expect(NWApptentive.setUserInfo).toHaveBeenCalledWith(null, null);
});
```
```javascript
afterEach(() => {
NWApptentive.engage.mockClear();
NWApptentive.setUserInfo.mockClear();
Utilities.logError.mockClear();
});
```
In between tests here I use `mockClear` to reset calls to them or sometimes `mockRestore` if you need the original functionality in a different test in that file. You don't need to restore it if you're done with it for the file.
---
## jest.doMock
> When using babel-jest, calls to mock will automatically be hoisted to the top of the code block. Use this method if you want to explicitly avoid this behavior.
There are only a couple cases in our code where we need different module implementations *between* tests in the same file and if you do then use `doMock` which is similar to mock but does not get hoisted and interfere with other tests.
One example is the `Platform` module:
---
## Example from test-is-auth-session-ready
```javascript
describe('Android < 5.0', () => {
beforeAll(() => {
const mockVersion = AndroidSdkVersions['5.0'] - 1;
jest.doMock('Platform', () => ({ OS: 'android', Version: mockVersion }));
});
afterAll(() => {
jest.resetModules();
});
test('Should return false if access token not present', () => {
state.auth.hasAccessToken = false;
expect(isAuthSessionReady(state)).toBe(false);
});
test('Should return true if access token present', () => {
expect(isAuthSessionReady(state)).toBe(true);
});
});
```
Here I want to set the Platform just for the Android section in the test file. I can use
the lifecycle hooks of beforeAll and afterAll for this test group.
---
## jest.resetModules
After the test group we **need** to do call `jest.resetModules` to clear the module registry cache and restore the implementation so that iOS can similarly use `doMock`.
Do not just try to do another `doMock` on top of the old one.
---
## Example of Accessing Spies from test-push-middleware
Here we setup some event listeners in the middleware that we want to call in our tests.
```javascript
beforeAll(() => {
addEventListenerSpy = jest.spyOn(PushNotificationIOS, 'addEventListener');
deviceInfoSpy = jest.spyOn(DeviceInfo, 'getModel');
});
test('Should not set pending permission due to simulator notifications error', () => {
deviceInfoSpy.mockReturnValue('Simulator');
const registerHandler = addEventListenerSpy.mock.calls[1][1];
registerHandler();
expect(store.getActions()).toEqual([pushDeviceRegistered()]);
});
```
Just access the mock object off of the spy to examine or execute the calls.
---
## Snapshots
Jest supports snapshot testing:
> Instead of rendering the graphical UI, which would require building the entire app, you can use a test renderer to quickly generate a serializable value for your React tree.
The first time the snapshot test is added it saves the output which you can inspect. Any time you run the tests it will diff the new output against what it has saved and if it has changed you have the choice to update it if you intended for that change or reflect on what went wrong.
---
## Component Snapshot: GoogleSignInButton
Here is a snapshot for a component I did as an example:
```javascript
beforeAll(() => {
jest.spyOn(Selectors, 'getCurrentRoute').mockReturnValue('route');
});
afterAll(() => {
Selectors.getCurrentRoute.mockRestore('Selectors');
});
it('should render correctly', () => {
const tree = renderer.create(
<GoogleSignInButton store={store} />
).toJSON();
expect(tree).toMatchSnapshot();
});
```
---
## Reducer Snapshot: Debt
And it can easily be done for reducers too:
```javascript
test('START getDebtGoalDetails', () => {
expect(debtReducer(INITIAL_STATE, getDebtGoalDetails.start())).toMatchSnapshot();
});
test('SUCCESS getDebtGoalDetails', () => {
const payloadData = {
payload: {
test_key: 3,
},
};
expect(debtReducer(INITIAL_STATE, getDebtGoalDetails.success(payloadData))).toMatchSnapshot();
});
test('FAIL getDebtGoalDetails', () => {
expect(debtReducer(INITIAL_STATE, getDebtGoalDetails.fail())).toMatchSnapshot();
});
```
---
## Useful References
* [Jest Docs](http://facebook.github.io/jest/docs/en/api.html)
* [Jest Debugging](http://facebook.github.io/jest/docs/en/troubleshooting.html)
* [Using Jest with React Native](http://facebook.github.io/jest/docs/en/tutorial-react-native.html#content)
* [The Node Inspector](https://nodejs.org/en/docs/inspector/)
* [Jest CLI](http://facebook.github.io/jest/docs/en/cli.html#content)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment