Search code examples
react-nativejestjsdetox

Mocking RNCamera with Detox does not work, non-mock impl is called


I'm trying to mock out RNCamera in a Detox test. However, the mock camera does not override the real implementation.

I followed these instructions and incorporated advice from these posts.

The result is:

  • The mock camera class file itself is executed (top-level logging is printed)
  • Mock camera's takePictureAsync and render functions are not called
  • Emulator's default camera is used

I have also tried:

  • RN_SRC_EXT=e2e react-native run-android, then detox test
  • adding "test-runner": "jest" and callingjest.mock` in the best body
  • replacing RN_SRC_EXT=e2e with RN_SRC_EXT=e2e.js
  • cleanup: rm -rf node_modules; yarn install etc.

My app layout is like (non-test-relevant content excluded):

package.json:

{
  ...
  "dependencies": {
    ...
    "@types/jest": "^24.0.21",
    "react": "16.9.0",
    "react-dom": "latest",
    "react-native": "0.61.1",
    "react-native-camera": "^3.6.0",
  },
  "devDependencies": {
    "@babel/core": "latest",
    "@babel/preset-env": "latest",
    "@babel/register": "latest",
    "@babel/preset-react": "latest",
    "@react-native-community/eslint-config": "^0.0.5",
    "babel-jest": "^24.9.0",
    "detox": "^14.5.1",
    "jest": "^24.9.0",
    "jest-fetch-mock": "^2.1.2",
    "metro-react-native-babel-preset": "^0.56.0",
    "mocha": "^6.2.2",
    "react-test-renderer": "16.9.0"
  },
  "detox": {
    "configurations": {
      "android.emu.debug": {
        "binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk",
        "build": "RN_SRC_EXT=e2e cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..",
        "type": "android.emulator",
        "device": {
          "avdName": "Pixel_2_API_29"
        }
      },
      "android.emu.release": {
        "binaryPath": "android/app/build/outputs/apk/release/app-release.apk",
        "build": "RN_SRC_EXT=e2e cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..",
        "type": "android.emulator",
        "device": {
          "avdName": "Pixel_2_API_29"
        }
      }
    }
  }
}

e2e/config.js:

require('@babel/register')({
    //cache: true,
    presets: [require('metro-react-native-babel-preset')],
    plugins: [require('@babel/plugin-transform-runtime').default],
    only: ['./e2e', './js'],
    ignore: ['node_modules']
  });

e2e/config.json:

{
    "setupFilesAfterEnv": ["./init.js"],
    "testEnvironment": "node",
    "reporters": ["detox/runners/jest/streamlineReporter"],
    "testMatch": ["**/__tests__/**/*.js?(x)", "**/?(*.)(e2e).js?(x)"],
    "verbose": true
}

e2e/mocha.opts:

--recursive
--timeout 300000
--bail
--file e2e/init.js
--require e2e/config.js
--require e2e/config.json
--file e2e/react-native-camera.e2e.js

metro.config.js:

const defaultSourceExts = require('metro-config/src/defaults/defaults').sourceExts
module.exports = {
    transformer: {
        getTransformOptions: async () => ({
            transform: {
                experimentalImportSupport: false,
                inlineRequires: false,
            },
        }),
    },
    resolver: { 
        sourceExts: process.env.RN_SRC_EXT
                    ? process.env.RN_SRC_EXT.split(',').concat(defaultSourceExts)
                    : defaultSourceExts
      }
};
// printed as: module.exports.resolver from e2e { sourceExts: [ 'e2e', 'js', 'json', 'ts', 'tsx' ] }
console.log("module.exports from e2e", module.exports);

e2e/config.json:

{
    "setupFilesAfterEnv": ["./init.js"],
    "testEnvironment": "node",
    "reporters": ["detox/runners/jest/streamlineReporter"],
    // tried with and without this line
    "testMatch": ["**/__tests__/**/*.js?(x)", "**/?(*.)(e2e).js?(x)"],
    "verbose": true
}

test.spec.js

describe('Example', () => {
  beforeEach(async () => {
      await device.reloadReactNative();
  });

  it('should blah blah balh', async () => {
      // test implementation
  });
 });

init.js:

const detox = require('detox');
const config = require('../package.json').detox;
const adapter = require('detox/runners/mocha/adapter');

before(async () => {
    await detox.init(config);
});

beforeEach(async function() {
    await adapter.beforeEach(this);
});

afterEach(async function() {
    await adapter.afterEach(this);
});

after(async () => {
    await detox.cleanup();
});

e2e/react-native-camera.e2e.js: (from here)

import React from 'react';

const timeout = ms => new Promise(resolve => setTimeout(resolve, ms));

// This IS printed on detox test -c android.emu.debug.
console.log("executing react-native-camera-e2e.js");
export class RNCamera extends React.Component {
  static Constants = {
    Aspect: {},
    BarCodeType: {},
    Type: { back: 'back', front: 'front' },
    CaptureMode: {},
    CaptureTarget: {},
    CaptureQuality: {},
    Orientation: {},
    FlashMode: {},
    TorchMode: {},
  };

  takePictureAsync = async () => {
    console.log("mock RNCamera takePictureAsync"); // Never printed
    await timeout(2000);
    return {  uri: './static-image.jpg'  };
  };

  render() {
   // This is never printed.
    console.log("mock RNCamera render()");
    return null;
  }
}

export const takePictureAsync = async () => {
  console.log("mock RNCamera takePictureAsync"); // never printed
  await timeout(2000);
  return { uri: './static-image.jpg' };
};


export default RNCamera;

Solution

  • Mocking with detox and mocking with Jest are 2 different things.

    In order to be able to mock third party module with detox, you can create a proxy component provider that will decide what component to use. So that you will be able to load the mocked version for detox testing.

    In your case you can create a CameraProvider.js that will just export the real RNCamera:

    export { RNCamera } from 'react-native-camera';
    

    Then you will need to create the mocked version CameraProvider.e2e.js alongside the previous file for end-to-end testing with detox:

    import React from 'react';
    
    const timeout = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    export class RNCamera extends React.Component {
        static Constants = {
            Aspect: {},
            BarCodeType: {},
            Type: { back: 'back', front: 'front' },
            CaptureMode: {},
            CaptureTarget: {},
            CaptureQuality: {},
            Orientation: {},
            FlashMode: {},
            TorchMode: {},
            AutoFocus: { on: {} },
            WhiteBalance: { auto: {} },
        };
    
        takePictureAsync = async () => {
            console.log('mock RNCamera takePictureAsync');
            await timeout(2000);
            return { uri: './static-image.jpg' };
        };
    
        render() {
            console.log('mock RNCamera render()');
            return null;
        }
    }
    

    Finally, if you have correctly configured your metro bundler in the root folder of your react-native project, you should be able to either build your app or start the bundler (for debug targets) with the RN_SRC_EXT=e2e.js env variable to let the metro bundler know which file extensions it should replace.