Unit testing Meteor/React app issue with subscription ready [solved]


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


#14

So now I have the following component:


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

/**
 * Get the click count from Oracle.
 * @param  {Object} that Component instance.
 */
const getCountOracle = (that) => {
  Meteor.call('getCount', (error, records) => {
    if (error) {
      throw new Error('getCount failed');
    }

    console.log('getCount', records[0]);

    that.setState({ oracleRecord: records[0] });
  });
};

/**
 * Increment the click count in Oracle.
 * @param  {Object} that Component instance.
 */
const incrementCountOracle = (that) => {
  Meteor.call('incrementCount', (error, output) => {
    if (error) {
      throw new Error('incrementCount failed');
    }

    getCountOracle(that);

    console.log('incrementCount', output);
  });
};

/**
 * Top level component.
 * @return {Object} JSX transpiled code.
 */
class App extends React.Component {
  /**
   * Component constructor
   */
  constructor() {
    super();

    this.state = {
      oracleRecord: null,
    };

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

  /**
   * React lifecycle hook for getting the initial click count from Oracle.
   */
  componentDidMount() {
    console.log('did mount');

    getCountOracle(this);
  }

  /**
   * Event handler for button onClick events.
   */
  handleOnClick(event) {
    event.preventDefault();

    if (event.target.classList.contains('mongo')) {
      Meteor.call('counts.increment', MongoCounts.findOne()._id);
    } else {
      incrementCountOracle(this);
    }
  }

  /**
   * React component render method.
   * @return {Object} Transpiled JSX object.
   */
  render() {
    if (!this.props.mongoReady) {
      return <div>Loading Mongo ...</div>;
    }

    if (!this.state.oracleRecord) {
      return <div>Loading Oracle ...</div>;
    }

    return (
      <div className="app">
        <HelloWorld />
        <ClickMe
          db="mongo"
          count={this.props.mongoRecord.count}
          handleOnClick={this.handleOnClick}
        />
        <ClickMe
          db="oracle"
          count={this.state.oracleRecord.COUNT}
          handleOnClick={this.handleOnClick}
        />
      </div>
    );
  }
}

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

const tracker = () => {
  const props = {};
  let mongoHandle;

  if (Meteor.isClient) {
    mongoHandle = Meteor.subscribe('counts');
  }

  if (Meteor.isServer || (mongoHandle && mongoHandle.ready())) {
    if (mongoHandle) {
      props.mongoReady = mongoHandle.ready();
    } else {
      props.mongoReady = false;
    }

    props.mongoRecord = MongoCounts.findOne();
  }

  return props;
};

export default withTracker(tracker)(App);

And the following 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';
import HelloWorld from './HelloWorld';
import ClickMe from './ClickMe';

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 two ClickMe components', () => {
    assert.equal(2, wrapper.find(ClickMe).length);
  });
});

Which fails with the following output:

I20181119-15:08:56.325(1)? --------------------------------
I20181119-15:08:56.325(1)? ----- RUNNING SERVER TESTS -----
I20181119-15:08:56.325(1)? --------------------------------
I20181119-15:08:56.325(1)?
I20181119-15:08:56.325(1)?
I20181119-15:08:56.325(1)?
I20181119-15:08:56.325(1)?   App component
=> Meteor server restarted
I20181119-15:08:56.326(1)?     ✓ should render without blowing up
I20181119-15:08:56.326(1)?     1) should have one HelloWorld component
I20181119-15:08:56.326(1)?     2) should have two ClickMe components
I20181119-15:08:56.326(1)?
I20181119-15:08:56.326(1)?   ClickMe component
I20181119-15:08:56.326(1)?     ✓ should render without blowing up
I20181119-15:08:56.327(1)?
I20181119-15:08:56.327(1)?   HelloWorld component
I20181119-15:08:56.327(1)?     ✓ should render without blowing up
I20181119-15:08:56.327(1)?
I20181119-15:08:56.327(1)?
I20181119-15:08:56.327(1)?   3 passing (46ms)
I20181119-15:08:56.327(1)?   2 failing
I20181119-15:08:56.327(1)?
I20181119-15:08:56.327(1)?   1) App component
I20181119-15:08:56.327(1)?        should have one HelloWorld component:
I20181119-15:08:56.327(1)?
I20181119-15:08:56.328(1)?       AssertionError: expected 1 to equal 0
I20181119-15:08:56.328(1)?       + expected - actual
I20181119-15:08:56.328(1)?
I20181119-15:08:56.328(1)?       -1
I20181119-15:08:56.328(1)?       +0
I20181119-15:08:56.328(1)?
I20181119-15:08:56.328(1)?       at Test.it (imports/ui/components/App.test.jsx:25:12)
I20181119-15:08:56.328(1)?       at run (packages/meteortesting:mocha-core/server.js:34:29)
I20181119-15:08:56.328(1)?       at Context.wrappedFunction (packages/meteortesting:mocha-core/server.js:63:33)
I20181119-15:08:56.328(1)?       at run (packages/meteortesting:mocha-core/server.js:52:15)
I20181119-15:08:56.328(1)?
I20181119-15:08:56.329(1)?   2) App component
I20181119-15:08:56.329(1)?        should have two ClickMe components:
I20181119-15:08:56.329(1)?
I20181119-15:08:56.329(1)?       AssertionError: expected 2 to equal 0
I20181119-15:08:56.329(1)?       + expected - actual
I20181119-15:08:56.329(1)?
I20181119-15:08:56.329(1)?       -2
I20181119-15:08:56.329(1)?       +0
I20181119-15:08:56.329(1)?
I20181119-15:08:56.329(1)?       at Test.it (imports/ui/components/App.test.jsx:29:12)
I20181119-15:08:56.330(1)?       at run (packages/meteortesting:mocha-core/server.js:34:29)
I20181119-15:08:56.330(1)?       at Context.wrappedFunction (packages/meteortesting:mocha-core/server.js:63:33)
I20181119-15:08:56.330(1)?       at run (packages/meteortesting:mocha-core/server.js:52:15)

I am pretty sure the issue stems from the with_tracker wrapper somehow (removing the wrapper make the test pass, but breaks other things)?


#15

This comment from before still applies.


#16

Hm, how to fix this situation …?


#17

Read what the guide has to say about testing


#18

Methinks that the issue is that the component renders before the database and query results are ready.


#19

Doh! substitute shallow with render - the proper Enzyme function to use here - and it works again.