Eric H
Eric H

Reputation: 1676

Jest not auto mocking common js modules

I am trying to wrap my head around testing Flux and React applications using Jest.

I started using Scotch.io tutorial as my starting point and will be creating a full test suite to gain knowledge in how to structure my first real react and flux app.

You can find my code on github to see where I am at.

The test that does not run is FluxProduct-test.jsx

jest.dontMock('../FluxProduct');

var React
  , TestUtils
  , FluxProduct
  , FluxCartActions;

describe('FluxProduct', function() {


  var SAMPLE = {
    product: {
      id: '1',
      name: 'Name',
      image: 'image.png',
      description: 'this description',
      variants: [
        {
          sku: '123',
          type: 'First',
          price: 1.99,
          inventory: 1
        },
        {
          sku: '456',
          type: 'Second',
          price: 2.99,
          inventory: 3
        },
        {
          sku: '789',
          type: 'Third',
          price: 3.99,
          inventory: 2
        }
      ]
    },
  };


  function getElement(product, className)
  {
    return product.getDOMNode().getElementsByClassName(className)[0];
  }

  function getElementByTag(product, tagName)
  {
    return product.getDOMNode().getElementsByTagName(tagName)[0];
  }

  function selectIsActive(select, text)
  {
    for( var i = 0; i < select.options.length; i++)
    {
      if (select.options[i].textContent == text)
      {
        return true
      }
    }
    return false;
  }

  beforeEach(function(){
    React = require('react/addons')
    , TestUtils = React.addons.TestUtils
    , FluxProduct = require('../FluxProduct')
    , FluxCartActions = require('../../actions/FluxCartActions');
  });

  it('should have the display all of the fields', function() {
    var cartItems = [];
    var selected = SAMPLE.product.variants[1];
    var product = TestUtils.renderIntoDocument(
      <FluxProduct selected={selected} product={SAMPLE.product} cartitems={cartItems} />
    );

    expect(getElement(product, 'name').textContent).toEqual(SAMPLE.product.name);
    expect(getElement(product, 'description').textContent).toEqual(SAMPLE.product.description);
    expect(getElement(product, 'price').textContent).toEqual('Price: $' + selected.price);
    expect(selectIsActive(getElementByTag(product, 'select'), selected.type)).toEqual(true);
  });

  it('should allow to add another variant', function() {
    var cartItems = [];
    var selected = SAMPLE.product.variants[1];
    var targetVariantIndex = 2;
    var targetVariant = SAMPLE.product.variants[targetVariantIndex];
    var product = TestUtils.renderIntoDocument(
      <FluxProduct selected={selected} product={SAMPLE.product} cartitems={cartItems} />
    );
    var selectElement = getElementByTag(product, 'select');
    var addToCartBtn = getElementByTag(product, 'select');

    TestUtils.Simulate.change(selectElement, { target: { value: targetVariantIndex } });

    expect(selectIsActive(selectElement, targetVariant.type)).toEqual(true);

    TestUtils.Simulate.click(addToCartBtn);

    expect(FluxCartActions.addToCart.mock.calls.length).toBe(1);
    expect(FluxCartActions.addToCart.mock.calls[0][0]).toBe(targetVariant.sku);
    expect(FluxCartActions.addToCart.mock.calls[0][0]).toBe({
      name: targetVariant.name,
      type: targetVariant.type,
      price: targetVariant.price
    });
  });
});

It comes back with "TypeError: Cannot read property 'calls' of undefined" on line 100.

When I log out FluxActions it seems that it is not being automocked, which is the reason mock is undefined and accessing the calls property is throwing the error.

fyi: Jest requires Node 0.10, does not run on 0.12

Helpful reference files:

FluxProduct.jsx

var React = require('react');
var FluxCartActions = require('../actions/FluxCartActions');

// Flux product view
var FluxProduct = React.createClass({

  // Add item to cart via Actions
  addToCart: function(event){
    var sku = this.props.selected.sku;
    var update = {
      name: this.props.product.name,
      type: this.props.selected.type,
      price: this.props.selected.price
    }
    FluxCartActions.addToCart(sku, update);
    FluxCartActions.updateCartVisible(true);
  },

  // Select product variation via Actions
  selectVariant: function(event){
    FluxCartActions.selectProduct(event.target.value);
  },

  // Render product View
  render: function() {
    var ats = (this.props.selected.sku in this.props.cartitems) ?
      this.props.selected.inventory - this.props.cartitems[this.props.selected.sku].quantity :
      this.props.selected.inventory;
    return (
      <div className="flux-product">
        <img src={'assets/' + this.props.product.image}/>
        <div className="flux-product-detail">
          <h1 className="name">{this.props.product.name}</h1>
          <p className="description">{this.props.product.description}</p>
          <p className="price">Price: ${this.props.selected.price}</p>
          <select onChange={this.selectVariant}>
            {this.props.product.variants.map(function(variant, index){
              return (
                <option key={index} value={index}>{variant.type}</option>
              )
            })}
          </select>
          <button type="button" onClick={this.addToCart} disabled={ats  > 0 ? '' : 'disabled'}>
            {ats > 0 ? 'Add To Cart' : 'Sold Out'}
          </button>
        </div>
      </div>
    );
  },

});

module.exports = FluxProduct;

FluxCartActions.js

var AppDispatcher = require('../dispatcher/AppDispatcher');
var FluxCartConstants = require('../constants/FluxCartConstants');

// Define action methods
var FluxCartActions = {

  // Receive inital product data
  receiveProduct: function(data) {
    AppDispatcher.handleAction({
      actionType: FluxCartConstants.RECEIVE_DATA,
      data: data
    })
  },

  // Set currently selected product variation
  selectProduct: function(index) {
    AppDispatcher.handleAction({
      actionType: FluxCartConstants.SELECT_PRODUCT,
      data: index
    })
  },

  // Add item to cart
  addToCart: function(sku, update) {
    AppDispatcher.handleAction({
      actionType: FluxCartConstants.CART_ADD,
      sku: sku,
      update: update
    })
  },

  // Remove item from cart
  removeFromCart: function(sku) {
    AppDispatcher.handleAction({
      actionType: FluxCartConstants.CART_REMOVE,
      sku: sku
    })
  },

  // Update cart visibility status
  updateCartVisible: function(cartVisible) {
    AppDispatcher.handleAction({
      actionType: FluxCartConstants.CART_VISIBLE,
      cartVisible: cartVisible
    })
  }

};

module.exports = FluxCartActions;

Here is my package.json file:

{
  "name": "flux-pricing",
  "version": "0.0.1",
  "description": "Pricing component with flux",
  "main": "app/assets/javascripts/cart.js",
  "dependencies": {
    "flux": "^2.0.0",
    "react": "^0.12.0",
    "underscore": "^1.7.0"
  },
  "devDependencies": {
    "browserify": "~>6.3.0",
    "envify": "~3.0.0",
    "jest-cli": "^0.4.0",
    "react-tools": "^0.12.2",
    "reactify": "^1.0",
    "watchify": "~2.1.0"
  },
  "scripts": {
    "start": "watchify -o app/assets/javascripts/app.js -v -d .",
    "build": "browserify . | uglifyjs -cm > app/assets/javascripts/app.min.js",
    "test": "jest"
  },
  "jest": {
    "rootDir": "app/assets/javascripts",
    "scriptPreprocessor": "<rootDir>/__tests__/preprocessor.js",
    "testFileExtensions": [
      "js",
      "jsx"
    ],
    "unmockedModulePathPatterns": ["react"],
    "testPathIgnorePatterns": [
      "preprocessor.js",
      "node_modules"
    ]
  },
  "browserify": {
    "transform": [
      "reactify",
      "envify"
    ]
  }
}

I run tests by running npm test in the app directory.

Upvotes: 2

Views: 2903

Answers (2)

Sean Adkinson
Sean Adkinson

Reputation: 8615

I think the issue could be that you are requiring the mocks in beforeEach, which could be un-doing the auto-genMockFunction that jest does for you. I just changed some tests I had written to require the modules in the beforeEach the same way, and they broke as well, as if the functions weren't mocks.

Try just requiring the dependencies once outside the tests.

var React = require('react/addons')
    , TestUtils = React.addons.TestUtils
    , FluxProduct = require('../FluxProduct')
    , FluxCartActions = require('../../actions/FluxCartActions');

describe(...);

Presumably, you are doing this to "reset" the mocks so that tests don't have side-effects, but I'd be surprised if jest doesn't already handle that for you.

Upvotes: 2

Michel Uncini
Michel Uncini

Reputation: 319

Yeah I had the same problem not so long ago. Whenever I trigger something from an event the automatic mocking of Jest doesn't seem to work. Maybe it is worth to open an issue on Github.

In the mean time I solved the problem by manual mocking the action at the top of the page:

jest.mock('../../actions/FluxCartActions');

Not a very elegant solution within Jest but it works. Also I think that you meant this:

var addToCartBtn = getElementByTag(product, 'button');

instead of

var addToCartBtn = getElementByTag(product, 'select');

So you can simulate the click of the button and call addToCart.

Upvotes: 1

Related Questions