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

22

我在过去几周学习了React、Babel、Semantic UI和Jest。在浏览器中呈现我的组件时,并没有遇到太多问题,但是在使用Jest编写单元测试时,我确实遇到了呈现问题。

被测试系统(SUT)如下:

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;

相关的测试文件如下:

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();
});

当我运行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)

我需要有经验的帮助来颁发这个赏金,因为我对这个主题一无所知。有人可以帮忙吗? - Pekka
以下是一个类似的问题,可能有所帮助:https://dev59.com/-F4d5IYBdhLWcg3wFPM7 - Martina
2个回答

3
你做得很对,只是有一个简单的错误。

你需要告诉 jest 不要模拟 jQuery。

为了清楚起见,

来自https://www.phpied.com/jest-jquery-testing-vanilla-app/下的第4个子标题Testing Vanilla

[它讲述了如何测试一个 Vanilla 应用程序,但它完美地描述了 Jest]

Jest 的问题在于它会模拟一切。这对于单元测试来说是无价的。但这也意味着当你不想模拟某些东西时,你需要声明它。

就是这样。
jest.unmock(moduleName)

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

此API最常见的用途是为了指定给定测试意图测试的模块(因此不希望自动模拟)。它返回可链接的jest对象。

注意:以前它是 dontMock

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

在require中使用const而不是var。即

const $ = require('jquery');

所以代码看起来像这样。
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;

0
在你的 Jest 配置中添加...
"setupFiles": ["./jestsetup.js"]

jestsetup.js 中,您需要将 $jQuery 添加为全局变量。
import $ from 'jquery';
global.$ = $;
global.jQuery = $;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接