前言

原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

本文原文地址,原文链接:http://huxinmin.com

未经作者同意,谢绝转载,原文链接:http://huxinmin.com

  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 本文原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

  • 本文原文地址,原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 本文原文地址,原文链接:http://huxinmin.com
  • 本文原文地址,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com
    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

      本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 本文原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com本文出自,原文链接:http://huxinmin.com
    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

      本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com
    本文原文地址,原文链接:http://huxinmin.com
    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com
  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com
  • 本文原文地址,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

      本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
    本文原文地址,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com
    本文原文地址,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com
    本文原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

  • 本文原文地址,原文链接:http://huxinmin.com
  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    本文原文地址,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com
  • 本文原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com
    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com
    本文原文地址,原文链接:http://huxinmin.com本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
      本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 本文原文地址,原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com
    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

  • 本文出自,原文链接:http://huxinmin.com
  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 本文原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

  • 本文原文地址,原文链接:http://huxinmin.com
    • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
      本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
      本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
  • 本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

  • 谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com
  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
  • 本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com

    未经作者同意,谢绝转载,原文链接:http://huxinmin.com

  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com
    本文出自,原文链接:http://huxinmin.com
    原文出自[胡新敏的个人博客] 转载请保留原文链接:http://huxinmin.com
  • 禁止非法采集,原文地址,原文链接:http://huxinmin.com
  • 未经作者同意,谢绝转载,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文原文地址,原文链接:http://huxinmin.com

    本文出自,原文链接:http://huxinmin.com

    谢绝转载,原文地址在胡新敏的个人博客,原文链接:http://huxinmin.com

    禁止非法采集,原文地址,原文链接:http://huxinmin.com
      本文禁止任何形式的非法采集,原文地址:胡新敏的个人博客,原文链接:http://huxinmin.com
    禁止非法采集,原文地址,原文链接:http://huxinmin.com

    前面的文章已经介绍过了前端的一些常见的测试工具,以及测试的必要性,那么今天就来学习一下Jest这个测试框架。

    Jest简介

    Jest是Fackbook开发的一个开源的Js的单元测试工具,在Jasmine基础之上演变开发而来。

    Why Jest

    市面上已经有了那么多成熟的测试工具,比如Mocha,Jasmine,Qunit等等,为什么要选择Jest呢?我觉得Jest相比于别的测试工具有着以下的一些优点:

    • 安装配置简单,非常容易上手,几乎是零配置的,通过npm 命令安装就可以直接运行了
    • 内置了常用的测试工具,比如自带断言、测试覆盖率工具等等,不用再安装别的测试工具
    • Jest可以利用其特有的快照测试功能,通过比对 UI 代码生成的快照文件,实现对 React 等常见框架的自动测试。
    • Jest的测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,测试速度比较快。
    • 目前在 Github 上其 star 数已经破万;而除了 Facebook 外,业内其他公司也开始从其它测试框架转向 Jest ,未来 Jest 的发展趋势仍会比较迅猛。

    Getting Started

    安装

    yarn add --dev jest
    npm install --save-dev jest
    

    然后新建一个需要进行测试的文件sum.js

    function sum(a, b) {
      return a + b;
    }
    module.exports = sum;
    

    编写测试用例sum.test.js

    const sum = require('./sum');
    test('adds 1 + 2 to equal 3', () => {
      expect(sum(1, 2)).toBe(3);
    });
    

    package.json中编写命令:

    {
      "scripts": {
        "test": "jest"
      }
    }
    

    运行测试:

    Finally, run yarn test and Jest will print this message:
    PASS  ./sum.test.js
    ✓ adds 1 + 2 to equal 3 (5ms)
    

    Jest配置

    有三种方法可以对Jest进行配置:

    • package.json中指定jest字段进行配置:
      {
      "name": "my-project",
      "jest": {
        "verbose": true
      }
      }
      
    • 在项目根目录新建jest.config.js进行配置:
      module.exports = {
      verbose: true,
      };
      
    • 在命令行中使用--config指定配置Json文件,注意不能包含jest字段:
      {
      "bail": true,
      "verbose": true
      }
      

    常见配置项:

    • automock [boolean][default:false] -- 是否自动mock引入的模块
    • browser [boolean][default:false] -- 解析模块时遵循package.json中Browserify的browser字段
    • globals [object] [default: {}] -- 测试环境中所需的环境变量
    • runner [string] [default: "jest-runner"] -- 自定义测试运行器,例如:jest-runner-eslintjest-runner-mochajest-runner-tscjest-runner-prettier
    • testEnvironment [string] [Default: "jsdom"] -- 测试运行的环境,默认是通过Jsdom生成的浏览器环境,你也可以使用node来指定node环境。
    • verbose [boolean] [default: false] -- 指示每个测试用例是否报告状态
    • 更多配置项请参考https://facebook.github.io/jest/docs/en/configuration.html

    Jest CLI

    你也可以直接使用命令行运行Jest命令:

    jest [--options]
    

    例子:

    jest   //运行所有测试
    jest path/to/my-test.js  //测试指定文件
    jest -o  //测试git/hu中改动的没有提交的文件
    jest --findRelatedTests path/to/fileA.js path/to/fileB.js  //测试相关的文件
    jest -t name-of-spec  //运行describe或者test中符合名字的测试用例
    jest --watch       //runs jest -o by default
    jest --watchAll    //runs all tests
    

    常见的options

    • --bail缩写-b一旦测试用例失败立刻退出
    • --cache是否开启缓存,默认是true,取消缓存使用--no-cache,如果你想要观测缓存的话,使用--showConfig然后看cacheDirectory的值,你还可以使用--clearCache清除缓存
    • --colors强制开启输出高亮
    • --config=<path>缩写-c,指定配置文件
    • --coverage输出测试覆盖率
    • --expand缩写-e显示全部的diffs和错误而不是修补
    • --json打印出测试结果的Json形式
    • --notify激活通知选项
    • --onlyChanged缩写-o,只针对改动的文件运行测试,只在git/hg仓库中,并且只有静态依赖时有用
    • --verbose分层显示每个独立的测试用例
    • --version缩写-v输出版本号
    • 更多请参考https://facebook.github.io/jest/docs/en/cli.html

    Matchers

    Jest中的Matchers类似于断言库里面的断言判断。下面是一些常见的Matchers:

    • toBe()判断是否是预期的值(不可用于浮点数,浮点数请用toBeCloseTo)
    • toBeCloseTo(number, numDigits)判断浮点数运算是否相等
    • toEqual()判断两个对象是否相等
    • any(constructor)匹配任何由给定构造器生成的对象
    • toBeNull()匹配Null
    • toBeUndefined()匹配undefined
    • toBeDefined()检查是否定义
    • toBeTruthy()不关心值是多少,只关心转换为布尔值以后是否为真
    • toBeFalsy()不关心值是多少,只关心转换为布尔值以后是否为假
    • toBeGreaterThan()大于
    • toBeGreaterThanOrEqual()大于等于
    • toBeLessThan()小于
    • toBeLessThanOrEqual()小于等于
    • toMatch()匹配正则
    • toContain()判断数组里是否包含某个元素
    • toThrow()函数调用后是否会抛出错误
    • toHaveBeenCalled()函数是否被调用
    • not取反
    • resolves获取promise对象的resolves值
    • 更多请参考https://facebook.github.io/jest/docs/en/expect.html

    测试异步代码

    Jest有4种方法可以测试异步的代码:

    1. 回调函数 在test语句中加入done参数,然后在回调函数中测试语句结束后执行done()即可:

      test('the data is peanut butter', done => {
      function callback(data) {
       expect(data).toBe('peanut butter');
       done();
      }
      
      fetchData(callback);
      });
      
    2. Promises 只要在test语句中直接返回一个promise即可,如果promise状态为rejected,则测试用例自动失败。
      test('the data is peanut butter', () => {
      expect.assertions(1);
      return fetchData().then(data => {
       expect(data).toBe('peanut butter');
      });
      });
      
      你也可以使用catch来期望rejected的出现
      test('the fetch fails with an error', () => {
      expect.assertions(1);
      return fetchData().catch(e => expect(e).toMatch('error'));
      });
      
      需要注意的是,需要添加expect.assertions(number)来验证断言的次数,不添加的话,则无法使测试用例自动失败。
    3. .resolves / .rejects
      test('the data is peanut butter', () => {
      expect.assertions(1);
      return expect(fetchData()).resolves.toBe('peanut butter');
      });
      test('the fetch fails with an error', () => {
      expect.assertions(1);
      return expect(fetchData()).rejects.toMatch('error');
      });
      
    4. Async/Await
      test('the data is peanut butter', async () => {
      expect.assertions(1);
      const data = await fetchData();
      expect(data).toBe('peanut butter');
      });
      test('the fetch fails with an error', async () => {
      expect.assertions(1);
      try {
       await fetchData();
      } catch (e) {
       expect(e).toMatch('error');
      }
      });
      
      async/await还可以和resolves/rejects组合使用
      test('the data is peanut butter', async () => {
      expect.assertions(1);
      await expect(fetchData()).resolves.toBe('peanut butter');
      });
      test('the fetch fails with an error', async () => {
      expect.assertions(1);
      await expect(fetchData()).rejects.toMatch('error');
      });
      

    钩子函数

    beforeEach()  -- 每个test执行之前执行
    afterEach()   -- 每个test执行之后执行
    beforeAll()   -- 在所有test执行之前执行一次
    afterAll()    -- 在所有test执行之后执行一次
    

    默认地,这些钩子函数的作用域是整个文件,你也可以使用describe来进行分组,外部的before先于describe内部的before运行,after后于describe内部的after运行。下面的例子可以很清楚帮助理解钩子函数运行的先后:

    beforeAll(() => console.log('1 - beforeAll'));
    afterAll(() => console.log('1 - afterAll'));
    beforeEach(() => console.log('1 - beforeEach'));
    afterEach(() => console.log('1 - afterEach'));
    test('', () => console.log('1 - test'));
    describe('Scoped / Nested block', () => {
      beforeAll(() => console.log('2 - beforeAll'));
      afterAll(() => console.log('2 - afterAll'));
      beforeEach(() => console.log('2 - beforeEach'));
      afterEach(() => console.log('2 - afterEach'));
      test('', () => console.log('2 - test'));
    });
    // 1 - beforeAll
    // 1 - beforeEach
    // 1 - test
    // 1 - afterEach
    // 2 - beforeAll
    // 1 - beforeEach
    // 2 - beforeEach
    // 2 - test
    // 2 - afterEach
    // 1 - afterEach
    // 2 - afterAll
    // 1 - afterAll
    

    Jest会先执行所有的describe然后再按顺序执行test:

    describe('outer', () => {
      console.log('describe outer-a');
      describe('describe inner 1', () => {
        console.log('describe inner 1');
        test('test 1', () => {
          console.log('test for describe inner 1');
          expect(true).toEqual(true);
        });
      });
      console.log('describe outer-b');
      test('test 1', () => {
        console.log('test for describe outer');
        expect(true).toEqual(true);
      });
      describe('describe inner 2', () => {
        console.log('describe inner 2');
        test('test for describe inner 2', () => {
          console.log('test for describe inner 2');
          expect(false).toEqual(false);
        });
      });
      console.log('describe outer-c');
    });
    // describe outer-a
    // describe inner 1
    // describe outer-b
    // describe inner 2
    // describe outer-c
    // test for describe inner 1
    // test for describe outer
    // test for describe inner 2
    

    Mock Functions

    在写单元测试的时候我们通常会根据接口来Mock接口的实现,比如你要测试某个类中的某个方法,而这个方法又依赖了外部的一些接口的实现,从单元测试的角度来说我只关心测试的方法的内部逻辑,却并不关注与当前类本身依赖的实现,所以我们通常会Mock掉依赖接口的返回,因为我们的测试重点在于特定的方法,在Jest中同样提供了Mock的功能。

    Mock 函数可以轻松地测试代码之间的连接——这通过擦除函数的实际实现,捕获对函数的调用 ( 以及在这些调用中传递的参数) ,在使用 new 实例化时捕获构造函数的实例,或允许测试时配置返回值的形式来实现。Jest中有两种方式的Mock Function,一种是利用Jest提供的Mock Function创建,另外一种是手动创建来覆写本身的依赖实现。

    假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一个回调函数,代码如下:

    function forEach(items, callback) {
      for (let index = 0; index < items.length; index++) {
        callback(items[index]);
      }
    }
    

    为了测试此函数,我们可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用。

    const mockCallback = jest.fn();
    forEach([0, 1], mockCallback);
    // The mock function is called twice
    expect(mockCallback.mock.calls.length).toBe(2);
    // The first argument of the first call to the function was 0
    expect(mockCallback.mock.calls[0][0]).toBe(0);
    // The first argument of the second call to the function was 1
    expect(mockCallback.mock.calls[1][0]).toBe(1);
    

    几乎所有的Mock Function都带有 .mock的属性,它保存了此函数被调用的信息。 .mock 属性还追踪每次调用时 this的值,所以也让检视 this 的值成为可能:

    const myMock = jest.fn();
    const a = new myMock();
    const b = {};
    const bound = myMock.bind(b);
    bound();
    console.log(myMock.mock.instances);
    // > [ <a>, <b> ]
    

    在测试中,需要对函数如何被调用,或者实例化做断言时,这些 mock 成员变量很有帮助意义︰

    // The function was called exactly once
    expect(someMockFunction.mock.calls.length).toBe(1);
    // The first arg of the first call to the function was 'first arg'
    expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
    // The second arg of the first call to the function was 'second arg'
    expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
    // This function was instantiated exactly twice
    expect(someMockFunction.mock.instances.length).toBe(2);
    // The object returned by the first instantiation of this function
    // had a `name` property whose value was set to 'test'
    expect(someMockFunction.mock.instances[0].name).toEqual('test');
    

    Mock 函数也可以用于在测试期间将测试值注入您的代码︰

    const myMock = jest.fn();
    console.log(myMock());
    // > undefined
    myMock
      .mockReturnValueOnce(10)
      .mockReturnValueOnce('x')
      .mockReturnValue(true);
    console.log(myMock(), myMock(), myMock(), myMock());
    // > 10, 'x', true, true
    

    如果你需要定义一个模拟的函数,它从另一个模块中创建的默认实现,mockImplementation方法非常有用︰

    // foo.js
    module.exports = function() {
      // some implementation;
    };
    // test.js
    jest.mock('../foo'); // this happens automatically with automocking
    const foo = require('../foo');
    // foo is a mock function
    foo.mockImplementation(() => 42);
    foo();
    // > 42
    

    当你需要多个函数调用产生不同的结果时,可以使用 mockImplementationOnce 方法:

    const myMockFn = jest
      .fn(() => 'default')
      .mockImplementationOnce(() => 'first call')
      .mockImplementationOnce(() => 'second call');
    console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
    // > 'first call', 'second call', 'default', 'default'
    

    对于有通常链接的方法(因此总是需要返回this)的情况,我们有一个语法糖的API以.mockReturnThis()函数的形式来简化它,它也位于所有模拟器上:

    const myObj = {
      myMethod: jest.fn().mockReturnThis(),
    };
    // is the same as
    const otherObj = {
      myMethod: jest.fn(function() {
        return this;
      }),
    };
    

    你也可以给你的Mock Function起一个准确的名字,这样有助于你在测试错误的时候在输出窗口定位到具体的Function

    const myMockFn = jest
      .fn()
      .mockReturnValue('default')
      .mockImplementation(scalar => 42 + scalar)
      .mockName('add42');
    

    最后,为了更简单地说明如何调用mock函数,我们为您添加了一些自定义匹配器函数:

    // The mock function was called at least once
    expect(mockFunc).toBeCalled();
    // The mock function was called at least once with the specified args
    expect(mockFunc).toBeCalledWith(arg1, arg2);
    // The last call to the mock function was called with the specified args
    expect(mockFunc).lastCalledWith(arg1, arg2);
    // All calls and the name of the mock is written as a snapshot
    expect(mockFunc).toMatchSnapshot();
    

    这些匹配器是真的只是语法糖的常见形式的检查 .mock 属性。 你总可以手动自己如果是更合你的口味,或如果你需要做一些更具体的事情︰

    // The mock function was called at least once
    expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
    // The mock function was called at least once with the specified args
    expect(mockFunc.mock.calls).toContain([arg1, arg2]);
    // The last call to the mock function was called with the specified args
    expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
      arg1,
      arg2,
    ]);
    // The first arg of the last call to the mock function was `42`
    // (note that there is no sugar helper for this specific of an assertion)
    expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
    // A snapshot will check that a mock was invoked the same number of times,
    // in the same order, with the same arguments. It will also assert on the name.
    expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
    expect(mockFunc.mock.getMockName()).toBe('a mock name');
    

    Jest平台

    您可以挑选Jest的特定功能,并将它们作为独立软件包使用:

    • jest-changed-files 匹配git/hg仓库中改动的文件
      const {getChangedFilesForRoots} = require('jest-changed-files');
      // print the set of modified files since last commit in the current repo
      getChangedFilesForRoots(['./'], {
      lastCommit: true,
      }).then(result => console.log(result.changedFiles));
      
    • jest-diff 可视化差异对比
      const diff = require('jest-diff');
      const a = {a: {b: {c: 5}}};
      const b = {a: {b: {c: 6}}};
      const result = diff(a, b);
      // print diff
      console.log(result);
      Expected
      Received
      Object {
        "a": Object {
          "b": Object {
           "c": 5,
           "c": 6,
          },
        },
      }
      
    • jest-docblock 提取解析注释,导出各种功能来操作注释块内的数据。
      const {parseWithComments} = require('jest-docblock');
      const code = `
      /**
      * This is a sample
      *
      * @flow
      */
      console.log('Hello World!');
      `;
      const parsed = parseWithComments(code);
      // prints an object with two attributes: comments and pragmas.
      console.log(parsed);
      { comments: '/**\nThis is a sample\n\n/\n \n console.log(\'Hello World!\');',
      pragmas: { flow: '' } }
      
    • jest-get-type 获取原生的js的数据类型
      const getType = require('jest-get-type');
      const array = [1, 2, 3];
      const nullValue = null;
      const undefinedValue = undefined;
      // prints 'array'
      console.log(getType(array));
      // prints 'null'
      console.log(getType(nullValue));
      // prints 'undefined'
      console.log(getType(undefinedValue));
      
    • jest-validate 验证用户提交的配置项
    • jest-worker 执行并行任务
    • pretty-format 格式美化
      const prettyFormat = require('pretty-format');
      const val = {object: {}};
      val.circularReference = val;
      val[Symbol('foo')] = 'foo';
      val.map = new Map([['prop', 'value']]);
      val.array = [-0, Infinity, NaN];
      console.log(prettyFormat(val));
      Object {
      "array": Array [
        -0,
        Infinity,
        NaN,
      ],
      "circularReference": [Circular],
      "map": Map {
        "prop" => "value",
      },
      "object": Object {},
      Symbol(foo): "foo",
      }
      

    Es6 Class Mocks

    ES6的类是一个构造函数的语法糖,所以你可以使用mock函数来mock ES6的类。下面我们将创建两个类一个是SoundPlayer类,一个SoundPlayerConsumer类依赖了SoundPlayer类。

    // sound-player.js
    export default class SoundPlayer {
      constructor() {
        this.foo = 'bar';
      }
      playSoundFile(fileName) {
        console.log('Playing sound file ' + fileName);
      }
    }
    
    // sound-player-consumer.js
    import SoundPlayer from './sound-player';
    export default class SoundPlayerConsumer {
      constructor() {
        this.soundPlayer = new SoundPlayer();
      }
      playSomethingCool() {
        const coolSoundFileName = 'song.mp3';
        this.soundPlayer.playSoundFile(coolSoundFileName);
      }
    }
    

    下面介绍4种方法来创建ES6的Mock:

    1. 自动mock

      调用jest.mock('./sound-player')方法会返回一个自动的mock,你可以用来监听构造函数和它所有的方法,它会替换构造函数和方法并且总是会返回undefined,方法的调用会存储在theAutomaticMock.mock.instances[index].methodName.mock.calls
      import SoundPlayer from './sound-player';
      import SoundPlayerConsumer from './sound-player-consumer';
      jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
      beforeEach(() => {
      // Clear all instances and calls to constructor and all methods:
      SoundPlayer.mockClear();
      });
      it('We can check if the consumer called the class constructor', () => {
      const soundPlayerConsumer = new SoundPlayerConsumer();
      expect(SoundPlayer).toHaveBeenCalledTimes(1);
      });
      it('We can check if the consumer called a method on the class instance', () => {
      // Show that mockClear() is working:
      expect(SoundPlayer).not.toHaveBeenCalled();
      const soundPlayerConsumer = new SoundPlayerConsumer();
      // Constructor should have been called again:
      expect(SoundPlayer).toHaveBeenCalledTimes(1);
      const coolSoundFileName = 'song.mp3';
      soundPlayerConsumer.playSomethingCool();
      // mock.instances is available with automatic mocks:
      const mockSoundPlayerInstance = SoundPlayer.mock.instances[0];
      const mockPlaySoundFile = mockSoundPlayerInstance.playSoundFile;
      expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
      // Equivalent to above check:
      expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
      expect(mockPlaySoundFile).toHaveBeenCalledTimes(1);
      });
      
    2. 手动mock

      你也可以创建一个_mock_文件夹,可以自己定义mock函数的实现。
      // __mocks__/sound-player.js
      // Import this named export into your test file:
      export const mockPlaySoundFile = jest.fn();
      const mock = jest.fn().mockImplementation(() => {
      return {playSoundFile: mockPlaySoundFile};
      });
      export default mock;
      
      然后导入mock函数:
      // sound-player-consumer.test.js
      import SoundPlayer, {mockPlaySoundFile} from './sound-player';
      import SoundPlayerConsumer from './sound-player-consumer';
      jest.mock('./sound-player'); // SoundPlayer is now a mock constructor
      beforeEach(() => {
      // Clear all instances and calls to constructor and all methods:
      SoundPlayer.mockClear();
      mockPlaySoundFile.mockClear();
      });
      it('We can check if the consumer called the class constructor', () => {
      const soundPlayerConsumer = new SoundPlayerConsumer();
      expect(SoundPlayer).toHaveBeenCalledTimes(1);
      });
      it('We can check if the consumer called a method on the class instance', () => {
      const soundPlayerConsumer = new SoundPlayerConsumer();
      const coolSoundFileName = 'song.mp3';
      soundPlayerConsumer.playSomethingCool();
      expect(mockPlaySoundFile).toHaveBeenCalledWith(coolSoundFileName);
      });
      
    3. 调用jest.mock()并传递一个工厂函数参数

      你可以使用jest.mock(path, moduleFactory)来创建一个mock函数,但是这个工厂函数必须要返回一个mock函数,也就是higher-order function (HOF)
      import SoundPlayer from './sound-player';
      const mockPlaySoundFile = jest.fn();
      jest.mock('./sound-player', () => {
      return jest.fn().mockImplementation(() => {
       return {playSoundFile: mockPlaySoundFile};
      });
      });
      
      使用工厂函数参数的不足之处在于,jest.mock()函数会在一开始就被调用,所以你无法一开始就定义一个变量然后在工厂函数中使用。
    4. 使用mockImplementation()或者mockImplementationOnce()来改变mock

      你可以使用mockImplementation()或者mockImplementationOnce()在钩子函数中稍后来改变mock函数的实现:
      import SoundPlayer from './sound-player';
      jest.mock('./sound-player');
      describe('When SoundPlayer throws an error', () => {
      beforeAll(() => {
       SoundPlayer.mockImplementation(() => {
         return {
           playSoundFile: () => {
             throw new Error('Test error');
           },
         };
       });
      });
      it('Should throw an error when calling playSomethingCool', () => {
       const soundPlayerConsumer = new SoundPlayerConsumer();
       expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
      });
      });
      

    整合Webpack

    只需要在package.json对jest字段进行一些配置即可:

    // package.json
    {
      "jest": {
        "modulePaths": ["/shared/vendor/modules"],
        "moduleFileExtensions": ["js", "jsx"],
        "moduleDirectories": ["node_modules", "bower_components", "shared"],
        "moduleNameMapper": {
          "^react(.*)$": "<rootDir>/vendor/react-master$1",  //webpack中的alias字段
          "^config$": "<rootDir>/configs/app-config.js",  //webpack中的alias字段
          "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js", //静态资源
          "\\.(gif|ttf|eot|svg)$": "<rootDir>/__mocks__/fileMock.js"//静态资源
        }
      }
    }
    

    对应静态资源文件的mock文件:

    // __mocks__/styleMock.js
    module.exports = {};
    
    // __mocks__/fileMock.js
    module.exports = 'test-file-stub';
    

    迁移

    如果你使用的是 AVA, Chai, Expect.js (by Automattic), Jasmine, Mocha, proxyquire, Should.js或者Tape这些测试工具,想要迁移到Jest也很简单,只需要安装 jest-codemods 即可。

    npm install -g jest-codemods
    

    然后在项目根目录运行:

    jest-codemods
    

    参考资料