Testing lifecycle methods with React Native, enzyme’s mount and JSDOM
[Note: this was posted on 2nd Sept 2018. The code samples work for React Native 0.55.2, sinon 6.1.5, jest 23.5.0, enzyme 3.5.0, jsdom 12.0.0]
The problem:
Testing lifecycle methods with React is a well-solved problem, but I couldn’t find a concrete all-in-one solution to get a similar approach working with React Native. After some poking around GitHub, StackOverflow and Google I finally managed to put it together.
The solution(s):
The first key piece of the puzzle was react-native-mock-render, a fork of react-native-mock by Dan Manges and team. Thank you for this Dan! You can learn more their work here: https://www.joinroot.com/blog/mounting-react-native-components-with-enzyme-and-jsdom/
The second piece of the puzzle was a response to a React Native feature request to Kent C Dodd’s react-testing-library. In it, Alex wrote some sample code showing how to coax JSDOM into doing what we’d need. Thoughtfully, he also included the code to stop the (many!) errors which would otherwise flood your terminal such as:
Warning: <View /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.Finally, I found I was getting a localStorage error:
SecurityError: localStorage is not available for opaque originsAlthough this isn’t related to the specific problem at hand, the solution is to declare the url (anything will do) when configuring JSDOM
new JSDOM(``, {
url: "https://bighealth.com", // <=============
}).window;Putting it all together
Here’s the (complete) code I now use when I want to test the lifecycle methods for a React Native project:
yarn add react-native-mock-render enzyme sinon jsdom --devIn your jest setup file (e.g. for me that’ll be in tests/setup.js):
import "react-native-mock-render/mock";const { JSDOM } = require("jsdom");
const { document } = new JSDOM(``, {
url: "https://example.com", // or whatever
}).window;global.document = document;
global.window = document.defaultView;
Object.keys(document.defaultView).forEach(property => {
if (typeof global[property] === "undefined") {
global[property] = document.defaultView[property];
}
});function suppressDomErrors() {
const suppressedErrors = /(React does not recognize the.*prop on a DOM element|Unknown event handler property|is using uppercase HTML|Received `true` for a non-boolean attribute `accessible`|The tag.*is unrecognized in this browser|PascalCase)/;
// eslint-disable-next-line no-console
const realConsoleError = console.error;
// eslint-disable-next-line no-console
console.error = message => {
if (message.match(suppressedErrors)) {
return;
}
realConsoleError(message);
};
}suppressDomErrors()
In your package.json:
"jest": {
"preset": "react-native",
"testURL": "https://example.com",
...
},And so now you can test, for instance, componentDidMount by doing:
import { mount } from "enzyme";
import spy from "sinon"
import Foo from "./Foo"it("mounts", () => {
Foo.prototype.componentDidMount = jest.fn(e => e);
mount(<Foo />)
const spy = sinon.spy(Foo.prototype, "componentDidMount");
expect(spy).toHaveBeenCalledTimes(1);
});;
Where Foo is your React component. Note that you have to set componentDidMount to be a jest mock function if it’s not explicitly defined by you in your component’s class methods. Otherwise you’ll get:
TypeError: Attempted to wrap undefined property componentDidMount as functionSo there we go! I hope this post helps someone get off to a quick start testing lifecycle methods with React Native. Any questions/corrections, do feel free to leave them in the comments and I’ll try to get back to you as soon as I can.
