RealOrangeOne / react-native-mock

A fully mocked and test-friendly version of react native (maintainers wanted)
MIT License
570 stars 153 forks source link

Understanding react-native-mock and react-native testing #13

Open maraujop opened 8 years ago

maraujop commented 8 years ago

Hi Leland,

We've crossed some tweets beforehand, but as usual Twitter character limits don't help. Thanks for offering your help, really appreciated.

The app we are building is using mostly RN + redux. Redux reducers and actions were easy to test using a combination of Mocha + Proxyquire + Sinon + Chai

However, some logic is still in the components as you know, so I wanted to test them. At the beginning of this week I read this article and somebody mentioned react-native-mock in the comments, it made sense to me.

First, As I mentioned to you in Twitter, when I install enzyme or react-native-mock with RN 0.20.0-rc (this is what I'm using currently) I get name collisions when I do react-native start for launching the packager. I understand I don't need the packager for the tests at all, but when I stop testing and start coding I launch the packager. In order to get it working I need to remove react and react-native-mock folders.

captura de pantalla 2016-02-17 a las 10 51 35

Second, my component testing isn't going as I was expecting. I've been able to test a very simple test component I created, however our components are composed of several nested components and have many which rely on native parts. With proxyquire, which sometimes is a little rough, I manage to mock this native parts, however it looks like shallow enzyme has trouble with them.

My test skeleton looks like this:

import React from "react";
const proxyquire = require('proxyquire').noCallThru()
const sinon = require('sinon')
import { shallow } from "enzyme";
require('react-native-mock/mock')

describe.only("<DrawerOption />", () => {

  let DrawerOption

  beforeEach(() => {
    DrawerOption = proxyquire('geminis/src/components/drawer/DrawerOption', {
      'several mocks go here': function() {
        return sinon.spy();
      },
    })
  })

  it("Render basic DrawerOption without props", () => {
    console.log(DrawerOption)
    let wrapper = shallow(<DrawerOption />);
    expect(wrapper.find('View')).to.have.length(1);
  });
});

My console.log throws:

{ [Function] displayName: 'DrawerOption' }

However the test breaks with:

TypeError: Cannot read property 'propTypes' of undefined

I'm aware there are probably better ways than proxyquire, but I'm quite new to all this. Any hints about the first and second problem would be great.

Thanks for your open source work with this project and enzyme :+1:

Cheers, Miguel

lelandrichardson commented 8 years ago

@maraujop Hi Miguel,

Thanks for the detailed issue.

On the face of it, I suspect that proxyquire is going to be causing some problems with the way react-native-mock works.

Regardless, though... you need to be calling react-native-mock/mock before any calls to require('react-native') get made. react-native-mock/mock fills the require cache for require('react-native') with the mock, so that future calls to require('react-native') use the mock instead.

If you are using mocha, the best way to do this is to actually just pass --require react-native-mock/mock as a command line arg to your mocha call. This will ensure it gets called first.

Additionally, for your usage with proxyquire... I believe you will need to explicitly pass in the mock RN module every time you use it, or else it will grab the real RN since proxyquire uses a fresh require cache. I would really suggest just not using proxyquire at all, but I understand if that is not an option for you.

If you need to use proxyquire, pass in react-native-mock in this way:

DrawerOption = proxyquire('geminis/src/components/drawer/DrawerOption', {
  'react-native': require('react-native-mock'),
})

Lastly, this project is not complete.... not all of the React Native components are fully mocked, so if you are using some properties like ListView.propType.foo or something, they might not be there. I am going to be working towards getting all of these mocks finished very soon, though.

lelandrichardson commented 8 years ago

Also, as an update... i'm not sure why the packager would be complaining to you the way you say it is. Just as an FYI, I use this setup in my RN projects and leave the packager running pretty much 24/7 without this problem... I can help you debug it further, but let's see if the suggestions made above are enough to get this working....

maraujop commented 8 years ago

Thanks a lot @lelandrichardson for taking the time to answer,

I've added --require react-native-mock/mock. To be honest I forgot to mention I had a --require test/setup.js that contained the import of react-native-mock, so I'm guessing this part was already ok, sorry.

The reason why I'm using proxyquire, is for example the fact that we use libraries like react-native-vector-icons which has a native part or react-native-i18n. I didn't know proxyquire used a fresh require cache, but makes sense, to be honest I didn't know all these inners until some days ago, I'm very out of my comfort zone here.

I've tried passing react-native mocking to proxyquire as you've suggested, but It hasn't changed much, I still get the same error. However I think posting the whole traceback might be useful here:

  1) <DrawerOption /> Render basic DrawerOption without props:
     TypeError: Cannot read property 'propTypes' of undefined
      at [object Object].ReactCompositeComponentMixin._processProps (node_modules/react/lib/ReactCompositeComponent.js:352:20)
      at [object Object].ReactCompositeComponentMixin.mountComponent (node_modules/react/lib/ReactCompositeComponent.js:129:28)
      at [object Object].wrapper [as mountComponent] (node_modules/react/lib/ReactPerf.js:66:21)
      at [object Object].ReactShallowRenderer._render (node_modules/react/lib/ReactTestUtils.js:366:14)
      at _batchedRender (node_modules/react/lib/ReactTestUtils.js:348:12)
      at ReactDefaultBatchingStrategyTransaction.Mixin.perform (node_modules/react/lib/Transaction.js:136:20)
      at Object.ReactDefaultBatchingStrategy.batchedUpdates (node_modules/react/lib/ReactDefaultBatchingStrategy.js:62:19)
      at Object.batchedUpdates (node_modules/react/lib/ReactUpdates.js:94:20)
      at [object Object].ReactShallowRenderer.render (node_modules/react/lib/ReactTestUtils.js:343:16)
      at Object.render (node_modules/enzyme/build/react-compat.js:115:26)
      at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:71:21)
      at shallow (node_modules/enzyme/build/index.js:44:10)
      at Context.<anonymous> (test/src/components/DrawerOption.js:60:39)

Here as you can see, the traceback is traversing react instead of react-native, so my guess is that the mocking is in place and working. Trying all this, I realized something weird is happening. If you recall my previous skeleton. If I change the DrawerOption initialization to:

describe.only("<DrawerOption />", () => {

  let DrawerOption = 'testing'

  beforeEach(() => {
    DrawerOption = proxyquire('geminis/src/components/drawer/DrawerOption', {
      'several mocks go here': function() {
        return sinon.spy();
      },
    })
  })

  it("Render basic DrawerOption without props", () => {
    console.log(DrawerOption)
    let wrapper = shallow(<DrawerOption />);
    expect(wrapper.find('View')).to.have.length(1);
  });
});

I get this:

AssertionError: expected { Object (root, unrendered, ...) } to have a length of 1 but got 0

And If I log the wrapper variable I get:

ShallowWrapper {
  root: [Circular],
  unrendered:
   { '$$typeof': Symbol(react.element),
     type: 'testing',
     key: null,
     ref: null,
     props: {},
     _owner: null,
     _store: {} },
  renderer:
   { _instance: null,
     render: [Function: render],
     getRenderOutput: [Function: getRenderOutput] },
  node:
   { '$$typeof': Symbol(react.element),
     type: 'testing',
     key: null,
     ref: null,
     props: {},
     _owner: null,
     _store: {} },
  nodes:
   [ { '$$typeof': Symbol(react.element),
       type: 'testing',
       key: null,
       ref: null,
       props: {},
       _owner: null,
       _store: {} } ],
  length: 1,
  options: {} }

This looks like if the shallow was using the initial variable value (undefined before, 'testing' after), instead of getting the mock proxyquire returns. The DrawerOption here is one of the most basic components I have in my app, it uses some Texts and Views, and FontMapper (a component from react-native-vector-icons).

Last, I'm using RN 0.20, the packager issue might be something of this rc version ?

Thanks a lot, cheers, Miguel

lelandrichardson commented 8 years ago

Hey Miguel,

Thanks for the update. It looks to me like your setup is working, but there just seems to be a component missing.

Do you know which component it is calling propTypes on that is undefined? It seems to me like this could just be a component that is missing in react-native-mock

maraujop commented 8 years ago

Hi Leland,

Thanks to your comment, I tried to reduce it to absurdity. I started changing DrawerOption into a much simpler component testing it after every change to find out what broke it. The result was I that ended up having a single View in my render and the test failed the same way. Apparently, proxyquire doesn't get on well with enzyme somehow, or maybe it's with react-native-mock, I'm not sure.

Using this simplified view, when I do:

import DrawerOption from 'geminis/src/components/drawer/DrawerOption'

or

var DrawerOption = require('geminis/src/components/drawer/DrawerOption')

I don't get TypeError: Cannot read property 'propTypes' of undefined. However If I proxyquire this module it fails. When react-native-mock ends up being feature complete, I will still have the problem of mocking RN custom native components, how are you mocking this?

Thanks a lot, cheers Miguel

lnhrdt commented 8 years ago

When react-native-mock ends up being feature complete, I will still have the problem of mocking RN custom native components, how are you mocking this?

Any luck? Today I'm finding myself down this rabbit hole as well. I've been struggling to figure out how to mock a custom component and then make expectations on it under test.

RealOrangeOne commented 8 years ago

I've been working with unit testing a load of custom components, and I've found the best thing to do is render them with shallow. Then you can test prop calls to their child components. react-native-mock takes care of mocking all the react-native stuff in a way enzyme can work with.