Unit testing Meteor/React app issue with subscription ready


#1

So the last two Unit tests fail - the reason I suspect is that the subcomponents have not been rendered since the App is waiting for a subscription to be ready. How to make the Unit tests wait for the subscription to be ready to allow testing?:

Unit test code:

/* global describe it beforeEach */

import React from 'react';
import { assert } from 'chai';
import { configure, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import App from './App';

configure({ adapter: new Adapter() });

describe('App component', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<App />);
  });

  it('should render without blowing up', () => {
    assert.equal(1, wrapper.length);
  });

  it('should have one HelloWorld component', () => {
    assert.equal(1, wrapper.find('HelloWorld').length);
  });

  it('should have one ClickMe component', () => {
    assert.equal(1, wrapper.find('ClickMe').length);
  });
});

App component code:

import { Meteor } from 'meteor/meteor';
import React from 'react';
import Counts from '../../api/counts';
import HelloWorld from './HelloWorld';
import ClickMe from './ClickMe';

/**
 * Top level component.
 */
class App extends React.Component {
  /**
   * Constructor for App component.
   * @param {Object} props React properties.
   */
  constructor(props) {
    super(props);

    this.handleOnClick = this.handleOnClick.bind(this);
  }

  /**
   * React life cycle method to handle collection subscription.
   */
  componentDidMount() {
    this.sub = Meteor.subscribe('counts', () => {
      const record = Counts.findOne();
      const count = record ? record.count : 0;

      this.setState({ count });
    });
  }

  /**
   * Event handler for button onClick events.
   */
  handleOnClick() {
    let newCount = 1;
    const record = Counts.findOne();

    if (record) {
      Meteor.call('counts.increment', record._id);
      newCount = record.count + 1;
    } else {
      Meteor.call('counts.insert', newCount);
    }

    this.setState({ count: newCount });
  }

  /**
   * Component render method.
   * @return {Object} JSX transpiled code.
   */
  render() {
    if (!this.state) {
      return <div>Loading ...</div>;
    }

    return (
      <div className="app">
        <HelloWorld />
        <ClickMe count={this.state.count} handleOnClick={this.handleOnClick} />
      </div>
    );
  }
}

export default App;

#2

Have you done the Meteor React tutorial? I would strongly recommend rethinking your current approach and using the best practices laid out there. For example, your subscribe isn’t reactive and you’re not stopping the subscription when the component unmounts.


#3

I have done the tutorial a while back - however - now I got annoyed with the meteor-react-data withTracker component wrapper. It’s syntax is unclean (IMHO) and it is unclear what it does exactly - and it breaks the unit tests…


#4

Unclean syntax or wrong code. Your call.


#5

Yeah, I better experiment some more …


#6

OK, so now the component code looks like this, but the Unit tests (unchanged) have the same problem:

import { Meteor } from 'meteor/meteor';
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types';
import Counts from '../../api/counts';
import HelloWorld from './HelloWorld';
import ClickMe from './ClickMe';

/**
 * Event handler for button onClick events.
 */
const handleOnClick = () => {
  Meteor.call('counts.increment', Counts.findOne()._id);
};

/**
 * Top level component.
 * @return {Object} JSX transpiled code.
 */
const App = (props) => {
  if (!props.countRecord) {
    return <div>Loading ...</div>;
  }

  return (
    <div className="app">
      <HelloWorld />
      <ClickMe count={props.countRecord.count} handleOnClick={handleOnClick} />
    </div>
  );
};

App.propTypes = {
  countRecord: PropTypes.instanceOf(Object),
  count: PropTypes.number,
};

export default withTracker(() => {
  Meteor.subscribe('counts');

  return {
    countRecord: Counts.findOne(),
  };
})(App);

The Unit test stacktrace for this assertion assert.equal(1, wrapper.find('HelloWorld').length); is this:

AssertionError: expected 1 to equal 0
    at Context.it (app/app.js?hash=0e2d748afc088ec653ed9179f1280dd6996b6ded:155:12)
    at callFn (packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:4623:21)
    at Test.Runnable.run (packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:4615:7)
    at Runner.runTest (packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:5151:10)
    at http://localhost:3100/packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:5269:12
    at next (packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:5065:14)
    at http://localhost:3100/packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:5075:7
    at next (packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:4999:14)
    at http://localhost:3100/packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:5038:7
    at done (packages/meteortesting_mocha-core.js?hash=ae586378424da85180afe3acf5f3fdc0f48b54b1:4570:5)

#7

Hm, and the console says this:

=> Meteor server restarted
I20181107-10:34:30.225(1)?     1) "before each" hook: wrappedFunction for "should render without blowing up"
I20181107-10:34:30.225(1)?
I20181107-10:34:30.226(1)?   ClickMe component
I20181107-10:34:30.226(1)?     ✓ should render without blowing up
I20181107-10:34:30.226(1)?
I20181107-10:34:30.226(1)?   HelloWorld component
I20181107-10:34:30.226(1)?     ✓ should render without blowing up
I20181107-10:34:30.226(1)?
I20181107-10:34:30.226(1)?
I20181107-10:34:30.226(1)?   2 passing (36ms)
I20181107-10:34:30.227(1)?   1 failing
I20181107-10:34:30.227(1)?
I20181107-10:34:30.227(1)?   1) App component
I20181107-10:34:30.227(1)?        "before each" hook: wrappedFunction for "should render without blowing up":
I20181107-10:34:30.227(1)?      TypeError: Meteor.subscribe is not a function
I20181107-10:34:30.227(1)?       at App.jsx.module.exportDefault.withTracker (imports/ui/components/App.jsx:39:10)
I20181107-10:34:30.227(1)?       at ReactMeteorDataComponent.getMeteorData (packages/react-meteor-data/ReactMeteorData.jsx:181:16)
I20181107-10:34:30.227(1)?       at MeteorDataManager.calculateData (packages/react-meteor-data/ReactMeteorData.jsx:34:24)
I20181107-10:34:30.227(1)?       at ReactMeteorDataComponent.componentWillMount (packages/react-meteor-data/ReactMeteorData.jsx:130:45)
I20181107-10:34:30.228(1)?       at ReactShallowRenderer._mountClassComponent (/Users/iehdk/install/src/meteor-react-app/node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:487:26)
I20181107-10:34:30.228(1)?       at ReactShallowRenderer.render (/Users/iehdk/install/src/meteor-react-app/node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:448:14)
I20181107-10:34:30.228(1)?       at /Users/iehdk/install/src/meteor-react-app/node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:476:35
I20181107-10:34:30.228(1)?       at withSetStateAllowed (/Users/iehdk/install/src/meteor-react-app/node_modules/enzyme-adapter-utils/build/Utils.js:132:16)
I20181107-10:34:30.228(1)?       at Object.render (/Users/iehdk/install/src/meteor-react-app/node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:475:68)
I20181107-10:34:30.228(1)?       at new ShallowWrapper (/Users/iehdk/install/src/meteor-react-app/node_modules/enzyme/build/ShallowWrapper.js:204:22)
I20181107-10:34:30.228(1)?       at shallow (/Users/iehdk/install/src/meteor-react-app/node_modules/enzyme/build/shallow.js:21:10)
I20181107-10:34:30.228(1)?       at Hook.beforeEach (imports/ui/components/App.test.jsx:15:15)
I20181107-10:34:30.228(1)?       at run (packages/meteortesting:mocha-core/server.js:34:29)
I20181107-10:34:30.228(1)?

#8

Your code looks much better, but Meteor.subscribe only exists on the client. Try this:

if (Meteor.isClient) Meteor.subscribe('counts')

#9

Hm, doesn’t seem to change anything ?


#10

So it works when you browse to it manually but the unit tests don’t work?


#11

Yup, that is the case.


#12

I suspect that the unit tests aren’t properly emulating the client environment. Nothing about your testing code implies it’s connecting to the meteor server at all, so even if Meteor.subscribe were defined, it doesn’t know what to connect to.

Have you already read what the guide has to say on testing react components?


#13

IIRC the guide on testing goes down the road of using Sinon to moch and snub database connectivity - I need to recheck tomorrow.