Testing React Native components in Node with react-test-renderer
Background
The react-test-renderer
package makes it convenient to test components outside of their native environment (e.g. on an iOS/Android device for React Native components). Instead of rendering “real” components, react-test-renderer
renders JavaScript objects so that the tests can be run in Node.
Below, we will cover rendering elements, testing props, finding nested elements, updating props, and calling event handlers. This is by no means a definitive guide, but hopefully points you in the right direction.
Setup
npm install react-test-renderer --save-dev
If you are using Jest to run your tests, you will want to make sure to use the React Native preset. If your project was created using react-native init
, this should already be configured (check your package.json
or jest.config.js
)
// package.json
{
"jest": {
"preset": "react-native"
}
}// jest.config.js
module.exports = {
preset: "react-native",
// ...
};
Note: Snapshot testing is popular use case with the react-test-renderer
. This article does not cover snapshots, but you can read about them in the Jest documentation.
Testing
The Component
Tests will be run for this <Testable>
component.
// components/Testable.js
import React from 'react';
import { View, Text } from 'react-native';
import { Consumer } from './Context';const Testable = props => (
<Consumer>
{value => (
<View>
<Text>{value} & {props.pair}</Text>
</View>
)}
</Consumer>
);export default Testable;
<Testable>
is a pretty boring component, but the big thing to note is the <Consumer>
component. This comes from React’s new context API and is paired with a <Provider>
component. These components are created by calling React.createContext()
.
// components/Context.js
import React from 'react';const { Provider, Consumer } = React.createContext('test');
export { Provider, Consumer };
<Testable>
renders a <Consumer>
, which means that it needs to be rendered inside of a <Provider>
.
// New context API reference
const App = () => (
<Provider value="red">
<Testable pair="blue" />
</Provider>
);/* App will render:
* <View>
* <Text>Red & Blue</Text>
* </View>
*/
Rendering with react-test-renderer
The default export from react-test-renderer
is an object with a create
method. We can pass create
a React element and it will return a renderer instance.
import React from 'react';
import 'react-native';
import renderer from 'react-test-renderer';import { Provider } from '../src/components/Context';
import Testable from '../src/components/Testable';describe('<Testable>', () => {
it('renders the correct text', () => {
const value = 'greetings';
const pair = 'salutations'; const inst = renderer.create(
<Provider value={value}>
<Testable pair={pair} />
</Provider>
);
});
});
The renderer instance has a few methods that we will cover later, but for now we will focus on the root
property it exposes. root
is an element instance for the root component that was rendered (<Provider>
here). Element instances expose a few properties that are useful in testing: type
, props
, and the instance’s parent
and children
instances.
Finding Components
In addition to the above properties, element instances also provide methods that can be used to find descendants.
instance.findByType
is used to find an element instance with the provided component type.instance.findByProps
is useful for finding an element instance with the provided props.- There are also “all” versions of these functions (
findAllByType
andfindAllByProps
) that you can use to find multiple instances.
In the above test, we want to verify that the the correct text is rendered, so we should look for a <Text>
component. We can then check that element instance’s props.children
(not the children
property of the instance, which is an element instance), to verify the output.
import { Text } from 'react-native';describe('<Testable>', () => {
it('renders the correct text', () => {
const value = 'greetings';
const pair = 'salutations';
const inst = renderer.create(
<Provider value={value}>
<Testable pair={pair} />
</Provider>
);
const textInst = inst.root.findByType(Text);
expect(
textInst.props.children.join()
).toBe(`${value} & ${pair}`);
});
});
We need to join textInst.props.children
because the JSX:
<Text>{value} & {props.pair}</Text>
is equivalent to:
React.createElement(Text, null, value, " & ", props.pair)
so the children
is an array, not a string.
Updating Props
If you want to re-render your elements, you can call the update
method of the renderer instance. This should be passed the new React element tree to be rendered. As long as the type of the root element passed to update
is the same as the one passed to create
, the element instances will be updated instead of unmounted and replaced.
describe('<Testable>', () => {
it('updates with the correct text', () => {
const value = 'greetings';
const value2 = 'farewell';
const pair = 'salutations'; const inst = renderer.create(
<Provider value={value}>
<Testable pair={pair} />
</Provider>
);
const textInst = inst.root.findByType(Text);
expect(
textInst.props.children.join()
).toBe(`${value} & ${pair}`); inst.update(
<Provider value={value2}>
<Testable pair={pair} />
</Provider>
); expect(
textInst.props.children.join()
).toBe(`${value2} & ${pair}`);
});
});
Events
If you want to test any event props of a component, you will have to manually call them. You can do this by accessing the props from the element instance’s props
object.
// components/Clickable.js
import React from 'react';
import { View, Text, TouchableHighlight } from 'react-native';class Clickable extends React.Component { state = { number: 1 }; pressHandler = () => {
this.setState(prevState => (
{ number: prevState.number + 1 }
));
}render() {
return (
<View>
<TouchableHighlight onPress={this.pressHandler}>
<Text>{this.state.number}</Text>
</TouchableHighlight>
</View>
);
}
}export default Clickable;
The <Clickable>
renders a <TouchableHighlight>
, which has an onPress
prop. Once we render the <Clickable>
, we can use findByType
to find the TouchableHighlight
and call its onPress
function.
We can either check that the state was updated by accessing it directly through the root instance’s instance.state
or by checking the rendered text value.
describe('<Clickable>', () => {
it('updates number when clicked', () => {
const inst = renderer.create(<Clickable />);
const button = inst.root.findByType(TouchableHighlight);
const text = inst.root.findByType(Text); // default state
expect(text.props.children).toBe(1);
// or
expect(inst.root.instance.state.number).toBe(1); button.props.onPress(); // state was updated by the button press
expect(text.props.children).toBe(2);
});
});