Monday, March 6, 2017

How do I properly mock third party libraries (like jQuery and Semantic UI) using Jest?

Leave a Comment

I have been learning React, Babel, Semantic UI, and Jest over the last couple of weeks. I haven't really run into too many issues with my components not rendering in the browser, but I have run into issues with rendering when writing unit tests with Jest.

The SUT is as follows:

EditUser.jsx

var React = require('react'); var { browserHistory, Link } = require('react-router'); var $ = require('jquery');  import Navigation from '../Common/Navigation';  const apiUrl = process.env.API_URL; const phoneRegex = /^[(]{0,1}[0-9]{3}[)]{0,1}[-\s\.]{0,1}[0-9]{3}[-\s\.]{0,1}[0-9]{4}$/;  var EditUser = React.createClass({   getInitialState: function() {     return {       email: '',       firstName: '',       lastName: '',       phone: '',       role: ''     };   },   handleSubmit: function(e) {     e.preventDefault();      var data = {       "email": this.state.email,       "firstName": this.state.firstName,       "lastName": this.state.lastName,       "phone": this.state.phone,       "role": this.state.role     };      if($('.ui.form').form('is valid')) {       $.ajax({         url: apiUrl + '/api/users/' + this.props.params.userId,         dataType: 'json',         contentType: 'application/json',         type: 'PUT',         data: JSON.stringify(data),         success: function(data) {           this.setState({data: data});           browserHistory.push('/Users');           $('.toast').addClass('happy');           $('.toast').html(data["firstName"] + ' ' + data["lastName"] + ' was updated successfully.');           $('.toast').transition('fade up', '500ms');           setTimeout(function(){               $('.toast').transition('fade up', '500ms').onComplete(function() {                   $('.toast').removeClass('happy');               });           }, 3000);         }.bind(this),         error: function(xhr, status, err) {           console.error(this.props.url, status, err.toString());           $('.toast').addClass('sad');           $('.toast').html("Something bad happened: " + err.toString());           $('.toast').transition('fade up', '500ms');           setTimeout(function(){               $('.toast').transition('fade up', '500ms').onComplete(function() {                   $('.toast').removeClass('sad');               });           }, 3000);         }.bind(this)       });     }   },   handleChange: function(e) {     var nextState = {};     nextState[e.target.name] = e.target.value;     this.setState(nextState);   },   componentDidMount: function() {     $('.dropdown').dropdown();      $('.ui.form').form({       fields: {             firstName: {               identifier: 'firstName',               rules: [                     {                       type: 'empty',                       prompt: 'Please enter a first name.'                     },                     {                       type: 'doesntContain[<script>]',                       prompt: 'Please enter a valid first name.'                     }                 ]             },             lastName: {               identifier: 'lastName',               rules: [                     {                       type: 'empty',                       prompt: 'Please enter a last name.'                     },                     {                       type: 'doesntContain[<script>]',                       prompt: 'Please enter a valid last name.'                     }                 ]             },             email: {               identifier: 'email',               rules: [                     {                       type: 'email',                       prompt: 'Please enter a valid email address.'                     },                     {                       type: 'empty',                       prompt: 'Please enter an email address.'                     },                     {                       type: 'doesntContain[<script>]',                       prompt: 'Please enter a valid email address.'                     }                 ]             },             role: {               identifier: 'role',               rules: [                     {                       type: 'empty',                       prompt: 'Please select a role.'                     }                 ]             },             phone: {               identifier: 'phone',               optional: true,               rules: [                     {                       type: 'minLength[10]',                       prompt: 'Please enter a valid phone number of at least {ruleValue} digits.'                     },                     {                       type: 'regExp',                       value: phoneRegex,                       prompt: 'Please enter a valid phone number.'                     }                 ]             }         }     });      $.ajax({       url: apiUrl + '/api/users/' + this.props.params.userId,       dataType:'json',       cache: false,       success: function(data) {         this.setState({data: data});         this.setState({email: data.email});         this.setState({firstName: data.firstName});         this.setState({lastName: data.lastName});         this.setState({phone: data.phone});         this.setState({role: data.role});       }.bind(this),       error: function(xhr, status, err) {         console.error(this.props.url, status, err.toString());       }.bind(this)     });    },   render: function () {     return (       <div className="container">         <Navigation active="Users"/>         <div className="ui segment">             <h2>Edit User</h2>             <div className="required warning">                 <span className="red text">*</span><span> Required</span>             </div>             <form className="ui form" onSubmit={this.handleSubmit} data={this.state}>                 <h4 className="ui dividing header">User Information</h4>                 <div className="ui three column grid field">                     <div className="row fields">                         <div className="column field required">                             <label>First Name</label>                             <input type="text" name="firstName" value={this.state.firstName}                                 onChange={this.handleChange}/>                         </div>                         <div className="column field required">                             <label>Last Name</label>                             <input type="text" name="lastName" value={this.state.lastName}                                 onChange={this.handleChange}/>                         </div>                         <div className="column field required">                             <label>Email</label>                             <input type="text" name="email" value={this.state.email}                                 onChange={this.handleChange}/>                         </div>                     </div>                 </div>                 <div className="ui three column grid field">                     <div className="row fields">                         <div className="column field required">                             <label>User Role</label>                             <select className="ui dropdown" name="role"                                 onChange={this.handleChange} value={this.state.role}>                                 <option value="SuperAdmin">Super Admin</option>                             </select>                         </div>                         <div className="column field">                             <label>Phone</label>                             <input name="phone" value={this.state.phone}                                 onChange={this.handleChange}/>                         </div>                     </div>                 </div>                 <div className="ui three column grid">                     <div className="row">                         <div className="right floated column">                             <div className="right floated large ui buttons">                                 <Link to="/Users" className="ui button">Cancel</Link>                                 <button className="ui button primary" type="submit">Save</button>                             </div>                         </div>                     </div>                 </div>                 <div className="ui error message"></div>             </form>         </div>       </div>     );   } });  module.exports = EditUser; 

The associated test file is as follows:

EditUser.test.js

var React = require('react'); var Renderer = require('react-test-renderer'); var jQuery = require('jquery'); require('../../../semantic/dist/components/dropdown');  import EditUser from '../../../app/components/Users/EditUser';  it('renders correctly', () => {     const component = Renderer.create(         <EditUser />     ).toJSON();     expect(component).toMatchSnapshot(); }); 

The issue that I am seeing when I run jest:

 FAIL  test/components/Users/EditUser.test.js   ● Test suite failed to run      ReferenceError: jQuery is not defined        at Object.<anonymous> (semantic/dist/components/dropdown.min.js:11:21523)       at Object.<anonymous> (test/components/Users/EditUser.test.js:6:370)       at process._tickCallback (node.js:369:9) 

3 Answers

Answers 1

You are doing it in right way but one simple mistake.

You have to tell jest not to mock jquery

To be clear,

from https://www.phpied.com/jest-jquery-testing-vanilla-app/ under 4th subtitle Testing Vanilla

[It talks about testing a Vanilla app, but it perfectly describe about Jest]

The thing about Jest is that it mocks everything. Which is priceless for unit testing. But it also means you need to declare when you don't want something mocked.

That is

jest.unmock(moduleName) 

From Facebook's documentation
unmock Indicates that the module system should never return a mocked version of the specified module from require() (e.g. that it should always return the real module).

The most common use of this API is for specifying the module a given test intends to be testing (and thus doesn't want automatically mocked).

It returns the jest object for chaining.

Note : Previously it was dontMock.

When using babel-jest, calls to unmock will automatically be hoisted to the top of the code block. Use dontMock if you want to explicitly avoid this behavior.
You can see the full documentation here Facebook's Documentation Page in Github .

Also use const instead of var in require. That is

const $ = require('jquery'); 

So the code looks like

jest.unmock('jquery'); // unmock it. In previous versions, use dontMock instead var React = require('react'); var { browserHistory, Link } = require('react-router'); const $ = require('jquery');  import Navigation from '../Common/Navigation';  const apiUrl = process.env.API_URL; const phoneRegex = /^[(]{0,1}[0-9]{3}[)]{0,1}[-\s\.]{0,1}[0-9]{3}[-\s\.]{0,1}[0-9]{4}$/;  var EditUser = React.createClass({   getInitialState: function() {     return {       email: '',       firstName: '',       lastName: '',       phone: '',       role: ''     };   },   handleSubmit: function(e) {     e.preventDefault();      var data = {       "email": this.state.email,       "firstName": this.state.firstName,       "lastName": this.state.lastName,       "phone": this.state.phone,       "role": this.state.role     };      if($('.ui.form').form('is valid')) {       $.ajax({         url: apiUrl + '/api/users/' + this.props.params.userId,         dataType: 'json',         contentType: 'application/json',         type: 'PUT',         data: JSON.stringify(data),         success: function(data) {           this.setState({data: data});           browserHistory.push('/Users');           $('.toast').addClass('happy');           $('.toast').html(data["firstName"] + ' ' + data["lastName"] + ' was updated successfully.');           $('.toast').transition('fade up', '500ms');           setTimeout(function(){               $('.toast').transition('fade up', '500ms').onComplete(function() {                   $('.toast').removeClass('happy');               });           }, 3000);         }.bind(this),         error: function(xhr, status, err) {           console.error(this.props.url, status, err.toString());           $('.toast').addClass('sad');           $('.toast').html("Something bad happened: " + err.toString());           $('.toast').transition('fade up', '500ms');           setTimeout(function(){               $('.toast').transition('fade up', '500ms').onComplete(function() {                   $('.toast').removeClass('sad');               });           }, 3000);         }.bind(this)       });     }   },   handleChange: function(e) {     var nextState = {};     nextState[e.target.name] = e.target.value;     this.setState(nextState);   },   componentDidMount: function() {     $('.dropdown').dropdown();      $('.ui.form').form({       fields: {             firstName: {               identifier: 'firstName',               rules: [                     {                       type: 'empty',                       prompt: 'Please enter a first name.'                     },                     {                       type: 'doesntContain[<script>]',                       prompt: 'Please enter a valid first name.'                     }                 ]             },             lastName: {               identifier: 'lastName',               rules: [                     {                       type: 'empty',                       prompt: 'Please enter a last name.'                     },                     {                       type: 'doesntContain[<script>]',                       prompt: 'Please enter a valid last name.'                     }                 ]             },             email: {               identifier: 'email',               rules: [                     {                       type: 'email',                       prompt: 'Please enter a valid email address.'                     },                     {                       type: 'empty',                       prompt: 'Please enter an email address.'                     },                     {                       type: 'doesntContain[<script>]',                       prompt: 'Please enter a valid email address.'                     }                 ]             },             role: {               identifier: 'role',               rules: [                     {                       type: 'empty',                       prompt: 'Please select a role.'                     }                 ]             },             phone: {               identifier: 'phone',               optional: true,               rules: [                     {                       type: 'minLength[10]',                       prompt: 'Please enter a valid phone number of at least {ruleValue} digits.'                     },                     {                       type: 'regExp',                       value: phoneRegex,                       prompt: 'Please enter a valid phone number.'                     }                 ]             }         }     });      $.ajax({       url: apiUrl + '/api/users/' + this.props.params.userId,       dataType:'json',       cache: false,       success: function(data) {         this.setState({data: data});         this.setState({email: data.email});         this.setState({firstName: data.firstName});         this.setState({lastName: data.lastName});         this.setState({phone: data.phone});         this.setState({role: data.role});       }.bind(this),       error: function(xhr, status, err) {         console.error(this.props.url, status, err.toString());       }.bind(this)     });    },   render: function () {     return (       <div className="container">         <Navigation active="Users"/>         <div className="ui segment">             <h2>Edit User</h2>             <div className="required warning">                 <span className="red text">*</span><span> Required</span>             </div>             <form className="ui form" onSubmit={this.handleSubmit} data={this.state}>                 <h4 className="ui dividing header">User Information</h4>                 <div className="ui three column grid field">                     <div className="row fields">                         <div className="column field required">                             <label>First Name</label>                             <input type="text" name="firstName" value={this.state.firstName}                                 onChange={this.handleChange}/>                         </div>                         <div className="column field required">                             <label>Last Name</label>                             <input type="text" name="lastName" value={this.state.lastName}                                 onChange={this.handleChange}/>                         </div>                         <div className="column field required">                             <label>Email</label>                             <input type="text" name="email" value={this.state.email}                                 onChange={this.handleChange}/>                         </div>                     </div>                 </div>                 <div className="ui three column grid field">                     <div className="row fields">                         <div className="column field required">                             <label>User Role</label>                             <select className="ui dropdown" name="role"                                 onChange={this.handleChange} value={this.state.role}>                                 <option value="SuperAdmin">Super Admin</option>                             </select>                         </div>                         <div className="column field">                             <label>Phone</label>                             <input name="phone" value={this.state.phone}                                 onChange={this.handleChange}/>                         </div>                     </div>                 </div>                 <div className="ui three column grid">                     <div className="row">                         <div className="right floated column">                             <div className="right floated large ui buttons">                                 <Link to="/Users" className="ui button">Cancel</Link>                                 <button className="ui button primary" type="submit">Save</button>                             </div>                         </div>                     </div>                 </div>                 <div className="ui error message"></div>             </form>         </div>       </div>     );   } });  module.exports = EditUser; 

Answers 2

From your error stack, it seems like the semantic dropdown is looking for jQuery, which has not been previously loaded. I think that if you change:

var jQuery = require('jquery'); 

To

require('jquery'); 

That would load it for the tests and not place it in a variable, making it available for the semantic dropdown as well.

Answers 3

You probably don't need to import jquery in your test file, since you are already importing in the EditUser component.

You should also have a look at enzyme. You can do shallow rendering, or full DOM rendering. More info here. Whatever frameworks you are using within your React components, you can easily test your components output with enzyme.

Some simple examples are below:

import React from 'react'; import { mount, shallow, render } from 'enzyme'; import EditUser from './EditUser';  describe('EditUser', function() {    // Ensure component mounts OK   it('EditUser should mount', function() {     const user = shallow(       <EditUser />     );     expect(user).not.toBe.undefined;   });    // Ensure error is thrown when invalid data is passed in to a prop   it('Prop with invalid data throw error', function() {     expect(() => {       shallow(<EditUser prop="invalid data" />);     }).toThrow();   });    // Ensure that a specific prop is of type function   it('Should ensure that some prop is a function', () => {     const propClick = function() {console.log('click')};     const item = mount(       <EditUser someProp={propClick} />     );     expect(typeof(item.props().someProp) === "function");   }); }); 

I use enzyme(with Jest) in all my React projects, and it seems to be the easiest to work with, and supports modern test runners.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment