Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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) 
like image 620
Reece Long Avatar asked Sep 30 '16 22:09

Reece Long


1 Answers

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; 
like image 152
Sagar V Avatar answered Nov 02 '22 02:11

Sagar V