react-tracking icon indicating copy to clipboard operation
react-tracking copied to clipboard

Provide testing/mocking instructions and export react-tracking/mock

Open tizmagik opened this issue 5 years ago • 7 comments

I've been meaning to provide this as importable within react-tracking via something like: import track, { mockTrackEvent, TrackingPropType } from 'react-tracking/mock'; but haven't had a chance yet. PRs welcome! 😁

/* __mocks__/react-tracking.js */

const PropTypes = require('prop-types');

const mockTrackEvent = jest.fn();

module.exports.TrackingPropType = PropTypes.shape();

module.exports = () => (...clsOrMethod) => {
  if (clsOrMethod.length === 1) {
    // decorating a class
    const [cls] = clsOrMethod;
    cls.defaultProps = {
      ...cls.defaultProps,
      tracking: {
        trackEvent: mockTrackEvent,
      },
    };
    return cls;
  }

  // decorating a method
  return clsOrMethod[1].initializer;
};

module.exports.mockTrackEvent = mockTrackEvent;

Originally posted by @tizmagik in https://github.com/nytimes/react-tracking/issues/112#issuecomment-522647123

tizmagik avatar Aug 19 '19 17:08 tizmagik

Hi team,

We've been integrating this wonderful package into our own application but have faced some issues with fixing broken unit tests as a result. We tried using the mock above to set the tracking default prop for our class components but it didn't work. I'm almost certain that the issue is that the defaultProps for our components are defined outside of the class definition, a common pattern for React developers.

Here's an example:

@track({ module: 'MyComponent' })
export class MyComponent extends Component {
  render() {
    ...
  }
}

MyComponent.propTypes = {
  tracking: TrackingPropType,
};

MyComponent.defaultProps = {
  tracking: null,
};

export default connect(mapStateToProps)(MyComponent);

When we move the defaultProps (and propTypes) definitions inside of the class definition, it works. For now we're just passing in a mock tracking prop ourselves in our tests which works however we're still getting this warning for some reason: prop type 'tracking' is invalid; it must be a function, usually from the 'prop-types' package, but received 'undefined'.

EDIT: the prop-type warning was due to the mock using module.exports and us importing with import from.

marcelosedano avatar Apr 10 '20 00:04 marcelosedano

Hmm it may be because of the connect() wrapper? Could you try something like this?:

@track({ module: 'MyComponent' })
export class MyComponent extends Component {
  render() {
    ...
  }
}

const ConnectedMyComponent = connect(mapStateToProps)(MyComponent);

ConnectedMyComponent.propTypes = {
  tracking: TrackingPropType,
};

ConnectedMyComponent.defaultProps = {
  tracking: null,
};

export default ConnectedMyComponent;

If not, maybe you could create a repro in Codesandbox or something? I can try and take a closer look when I get a chance.

tizmagik avatar Apr 11 '20 04:04 tizmagik

Hi team,

We've been integrating this wonderful package into our own application but have faced some issues with fixing broken unit tests as a result. We tried using the mock above to set the tracking default prop for our class components but it didn't work. I'm almost certain that the issue is that the defaultProps for our components are defined outside of the class definition, a common pattern for React developers.

Here's an example:

@track({ module: 'MyComponent' })
export class MyComponent extends Component {
  render() {
    ...
  }
}

MyComponent.propTypes = {
  tracking: TrackingPropType,
};

MyComponent.defaultProps = {
  tracking: null,
};

export default connect(mapStateToProps)(MyComponent);

When we move the defaultProps (and propTypes) definitions inside of the class definition, it works. For now we're just passing in a mock tracking prop ourselves in our tests which works however we're still getting this warning for some reason: prop type 'tracking' is invalid; it must be a function, usually from the 'prop-types' package, but received 'undefined'.

EDIT: the prop-type warning was due to the mock using module.exports and us importing with import from.

Hi, could you elaborate on the EDIT? What did you do to fix it? I'm newbie to react, just got a task to fix this problem. I also got a TypeError bug: TypeError: (0 , _tracking.track) is not a function.

Thanks a lot in advance!

l225li avatar Aug 07 '20 04:08 l225li

Hey @l225li are you still having this issue? Could you share more of your code or maybe create a Codesandbox example so I can take a closer look?

tizmagik avatar Aug 11 '20 18:08 tizmagik

@tizmagik cc @l225li

With respect to the comment from @l225li, I also observed this issue recently when integrating react-tracking. In my case, our project is a typescript project using stateless functional components and ES module imports, and using jest for testing.

For tracking implemented at the component level, a lot of the code looks like this...

import track from 'react-tracking'

const MyComponent = () => { ... }

export default track({
  event: 'my-component.presented'
})(MyComponent)

The key for mocking, then, is to mock the default exported function (track).

By default, jest mocks do not support ES module semantics; when react-tracking was not mocked for ES module syntax, I observed variants of the error described by @l225li.

 FAIL  src/components/pages/Foo/Bar/Baz/__tests__/Baz.test.tsx
  ● Test suite failed to run

    TypeError: (0 , _reactTracking.default)(...) is not a function

      120 | }
      121 | 
    > 122 | export default track({
          |                ^
      123 |   event: 'foo-bar.baz.presented',
      124 | })(Baz)
      125 | 

      at Object.<anonymous> (src/components/pages/Foo/Bar/Baz/Baz.tsx:123:12)
      at Object.<anonymous> (src/components/pages/Foo/Bar/Baz/__tests__/Baz.test.tsx:10:1)

This prevented me from mocking react-tracking using a manual mock; however, I was able to get a satisfactory mocking solution using the following configuration.

jest.mock('react-tracking', () => {
    const trackEvent = jest.fn()
    return {
        __esModule: true,
        default: jest.fn(() => (id) => id),
        useTracking: jest.fn(() => ({
            trackEvent,
        })),
    }
})

Hope this helps.

Aside: really love the work NYT team has done with this library. In recent years I have spent a lot of time working on Android (where NYT team also shines), and was very pleasantly surprised to find this library, which shares a core philosophy and solves foundational problems I encountered in past projects in the mobile space. Great job.

wokkaflokka avatar Aug 19 '20 16:08 wokkaflokka

Thank you @wokkaflokka , that's very helpful. And thank you for the kind words! 🤗

tizmagik avatar Aug 19 '20 19:08 tizmagik

To disable react-tracking in test, we've used something like this:

// lib/tracking/index.js
import tracking, { useTracking } from 'react-tracking';

let exportedTracking = tracking
let exportedUseTracking = useTracking

if (process.env.NODE_ENV == 'test') {
  const Identity = (Component) => (Component)
  exportedTracking = () => (Identity)
  exportedUseTracking = () => {
    trackEvent: () => {},
    getTrackingData: () => ({})
  }
}

export { exportedTracking as tracking, exportedUseTracking as useTracking }
import { tracking, useTracking } from 'lib/tracking'

It doesn't solve testability, just avoid dealing with renamed HOC classes and such in test.

huguesbr avatar Nov 03 '20 14:11 huguesbr