如何使用Jest正确模拟第三方库(如jQuery和语义UI)?

时间:2023-01-18 00:26:35

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.

在过去的几周里,我一直在学习React,Babel,Semantic UI和Jest。我没有真正遇到太多问题,我的组件没有在浏览器中呈现,但我在使用Jest编写单元测试时遇到了渲染问题。

The SUT is as follows:

SUT如下:

EditUser.jsx

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

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)

2 个解决方案

#1


2  

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

你是以正确的方式做到这一点,但一个简单的错误。

You have to tell jest not to mock jquery

你必须告诉开玩笑不要模仿jquery

To be clear,

要清楚,

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

来自https://www.phpied.com/jest-jquery-testing-vanilla-app/第4副标题测试香草

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

[它谈到测试一个Vanilla应用程序,但它完美地描述了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.

关于Jest的事情就是嘲笑一切。这对单元测试来说是无价的。但这也意味着你需要在不想要嘲笑的时候申报。

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

来自Facebook的文档unmock表示模块系统永远不应该从require()返回指定模块的模拟版本(例如它应该总是返回真实模块)。

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

此API的最常见用途是指定要测试的给定测试的模块(因此不希望自动模拟)。

It returns the jest object for chaining.

它返回链接的jest对象。

Note : Previously it was dontMock.

注意:以前是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 .

当使用babel-jest时,对unmock的调用将自动提升到代码块的顶部。如果要明确避免此行为,请使用dontMock。您可以在Github中查看Facebook的文档页面中的完整文档。

Also use const instead of var in require. That is

在require中也使用const而不是var。那是

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;

#2


0  

In your jest configuration put ..

在您的开玩笑配置中..

"setupFiles": ["./jestsetup.js"]

In jestsetup.js you need to add $ and jQuery as global..

在jestsetup.js中你需要添加$和jQuery作为全局..

import $ from 'jquery';
global.$ = $;
global.jQuery = $;

#1


2  

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

你是以正确的方式做到这一点,但一个简单的错误。

You have to tell jest not to mock jquery

你必须告诉开玩笑不要模仿jquery

To be clear,

要清楚,

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

来自https://www.phpied.com/jest-jquery-testing-vanilla-app/第4副标题测试香草

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

[它谈到测试一个Vanilla应用程序,但它完美地描述了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.

关于Jest的事情就是嘲笑一切。这对单元测试来说是无价的。但这也意味着你需要在不想要嘲笑的时候申报。

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

来自Facebook的文档unmock表示模块系统永远不应该从require()返回指定模块的模拟版本(例如它应该总是返回真实模块)。

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

此API的最常见用途是指定要测试的给定测试的模块(因此不希望自动模拟)。

It returns the jest object for chaining.

它返回链接的jest对象。

Note : Previously it was dontMock.

注意:以前是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 .

当使用babel-jest时,对unmock的调用将自动提升到代码块的顶部。如果要明确避免此行为,请使用dontMock。您可以在Github中查看Facebook的文档页面中的完整文档。

Also use const instead of var in require. That is

在require中也使用const而不是var。那是

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;

#2


0  

In your jest configuration put ..

在您的开玩笑配置中..

"setupFiles": ["./jestsetup.js"]

In jestsetup.js you need to add $ and jQuery as global..

在jestsetup.js中你需要添加$和jQuery作为全局..

import $ from 'jquery';
global.$ = $;
global.jQuery = $;