Yulfy Yulfy - 5 months ago 116
Javascript Question

Testing React Native apps with Jest

I've been trying to get Jest working with my react native project without much luck. Seems most threads are hacked solutions to get things up and running and I can't seem to get over the last hurdle I'm facing.

Problem



I'm getting the following error when trying to run the following piece of code. If I mock react-native inside of the jestSupport/env.js file I can get past the error but obviously I cannot use any of the structures such as AsyncStorage to actually test my code (as I'll only have the mocked functionality).

Question



Does anyone have any suggestions on how to solve this problem?

At this stage I'm willing to try anything up to and including scrapping everything test related that I have and trying again. If that were the case, I'd need some set of guidelines to follow as the React Native docs are horribly out of date regarding Jest and I'm relatively new to the React scene.

Error



Runtime Error
Error: Cannot find module 'ReactNative' from 'react-native.js'
at Runtime._resolveNodeModule (/Users/Yulfy/Downloads/COMPANY-Mobile/node_modules/jest-cli/src/Runtime/Runtime.js:451:11)
at Object.<anonymous> (/Users/Yulfy/Downloads/COMPANY-Mobile/node_modules/react-native/Libraries/react-native/react-native.js:181:25)
at Object.<anonymous> (/Users/Yulfy/Downloads/COMPANY-Mobile/network/connections.js:8:18)


Test Code



jest.unmock('../network/connections');

import Authorisation from '../network/connections';

describe('connections', () => {
it('Should store and retrieve a mocked user object', () => {
Auth = new Authorisation();
const TEST_STRING = "CONNECTION TEST PASS";
var userObj = {
test_string: TEST_STRING
};
Auth._localStore(userObj, (storeRes) => {
Auth._localRetrieve((retRes) => {
expect(retRes.test_string).toEqual(TEST_STRING);
});
});
});
});


connection.js



/*
* All returns should give the following structure:
* {isSuccess: boolean, data: object}
*/


import React, { Component } from 'react';
import { AsyncStorage } from 'react-native';

const Firebase = require('firebase');
const FIREBASE_URL = 'https://COMPANY-test.firebaseio.com';
const STORAGE_KEY = 'USER_DATA';
class Authorisation{
_ref = null;
user = null;

constructor(){

}

getOne(){return 1;}
_setSystemUser(userObj, authObj, callback){
var ref = this.connect();
if(ref === null){
callback({isSuccess:false, data:{message:"Could not connect to the server"}});
}
ref = ref.child('users').child(authObj.uid);
ref.once("value", function(snapshot){
if(snapshot.exists()){
callback({isSuccess:false, data:{message:"Email is currently in use"}});
return;
}
ref.set(userObj, function(error){
if(error){
callback({isSuccess:false, data:error});
}else{
callback({isSuccess:true, data:authObj});
}
});
});
}

_localStore(userObj, callback){
AsyncStorage.setItem(STORAGE_KEY, userObj, (error) => {
console.log("_localStore::setItem -> ", error);
if(error){
callback({
isSuccess:false,
data:'Failed to store user object in storage.'
});
}else{
callback({
isSuccess:true,
data: userObj
});
}
});
}
_localRetrieve(callback){
AsyncStorage.getItem(STORAGE_KEY, (error, res) => {
console.log("_localStore::getItem:error -> ", error);
console.log("_localStore::getItem:result -> ", res);
if(error){
callback({
isSuccess:false,
data:error
});
}else{
callback({
isSuccess: true,
data: res
});
}
});
}

connect(){
if(this._ref === null){
_ref = new Firebase(FIREBASE_URL);
}
return _ref;
}

getUser(){

}

isLoggedIn(){

}

registerUser(userObj, callback){
var ref = this.connect();
if(ref === null){
callback({isSuccess:false, data:{message:"Could not connect to the server"}});
}
var that = this;
ref.createUser({
email: userObj.username,
password: userObj.password
}, function(error, userData){
if(error){
callback({isSuccess:false, data:error});
return;
}
var parseObj = {
email: userObj.username,
fullName: userObj.fullName
};
that.loginUser(parseObj, function(res){
if(res.isSuccess){
that._setSystemUser(parseObj, res.data, callback);
}else{
callback(res);//Fail
}
});
});
}

loginUser(userObj, callback){
var ref = this.connect();
if(ref === null){
callback({isSuccess:false, data:{message:"Could not connect to the server"}});
}
ref.authWithPassword({
email: userObj.email,
password: userObj.password
}, function(error, authData){
if(error){
callback({isSuccess:false, data:error.message});
}else{
callback({isSuccess:true, data:authData});
}
});

}
}

export default Authorisation;


If you've read this far, thanks for your time!

-Yulfy

Answer

TL;DR

I have a working example of Jest running with the latest version React Native (v0.28.0) in this GitHub Repo.

-

After investigating this problem for quite some time, I finally found the solution.

There are a few online examples of React Native applications that are integrated with Jest, but you can't simply copy and paste the code into your codebase and expect it to work, unfortunately. This is because of RN version differences.

Versions of React Native prior to v0.20.0 contained a .babelrc file in the packager (node_modules/react-native/packager/react-packager/.babelrc) that some online examples directly include it in their package.json. However, versions v0.20.0 and up changed to no longer include this file which means that you can no longer attempt to include it. Because of this, I recommend using your own .babelrc file and definte your own presets and plugins.

I don't know what your package.json file looks like, but it's an incredibly important piece to solving this problem.

{
    "name": "ReactNativeJest",
    "version": "0.0.1",
    "jest": {
        "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
        "unmockedModulePathPatterns": [
            "node_modules"
        ],
        "verbose": true,
        "collectCoverage": true
    },
    "scripts": {
        "test": "jest"
    },
    "dependencies": {
        "react": "^15.1.0",
        "react-native": "^0.27.2"
    },
    "devDependencies": {
        "babel-core": "^6.4.5",
        "babel-jest": "^12.1.0",
        "babel-plugin-transform-regenerator": "^6.0.18",
        "babel-polyfill": "^6.0.16",
        "babel-preset-react-native": "^1.9.0",
        "babel-types": "^6.1.2",
        "chai": "^3.5.0",
        "enzyme": "^2.3.0",
        "jest-cli": "^12.1.1",
        "react-addons-test-utils": "^15.1.0",
        "react-dom": "^15.1.0"
    }
}

The other important piece is mocking React Native. I created a __mocks__/react-native.js file that looks like this:

'use strict';

var React = require('react');
var ReactNative = React;

ReactNative.StyleSheet = {
    create: function(styles) {
        return styles;
    }
};

class View extends React.Component {}
class Text extends React.Component {}
class TouchableHighlight extends React.Component {}

// Continue to patch other components as you need them
ReactNative.View = View;
ReactNative.Text = Text;
ReactNative.TouchableHighlight = TouchableHighlight;

module.exports = ReactNative;

By monkey patching the React Native functions like this, you can successfully avoid the weird Jest errors you get when trying to run your tests.

Finally, make sure you create a .babelrc file in your project's root directory that has, at the very least, these following lines:

{
    "presets": ["react-native"],
    "plugins": [
        "transform-regenerator"
    ]
}

This file will be used to tell babel how to correctly transform your ES6 code.

After following this setup, you should have no problems running Jest with React Native. I am confident that a future version of React Native will make it easier to integrate the two frameworks together, but this technique will work perfectly for the current version :)

EDIT

Instead of manually mocking the ReactNative elements in your __mocks__/react-native.js file, you can use the react-native-mock library to do the mocking for you (make sure to add the library to your package.json file):

// __mocks__/react-native.js

module.exports = require('react-native-mock');

I updated my GitHub Repo example to demonstrate this method.