nytimes / react-tracking

🎯 Declarative tracking for React apps.
https://open.nytimes.com/introducing-react-tracking-declarative-tracking-for-react-apps-2c76706bb79a
Other
1.87k stars 123 forks source link

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

Open tizmagik opened 4 years ago

tizmagik commented 4 years ago

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

marcelosedano commented 4 years ago

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.

tizmagik commented 4 years ago

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.

l225li commented 3 years ago

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!

tizmagik commented 3 years ago

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?

wokkaflokka commented 3 years ago

@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.

tizmagik commented 3 years ago

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

huguesbr commented 3 years ago

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.