<p>这里我记录下搭建基于 karma+mocha+webpack3+vue2 的测试环境。因为之前折腾了一段时间,发现的坑挺深的,防止后面再次掉进坑里,留个笔记。如果这边文章能解决大家遇到的问题那就更好了?</p> <h2 id="articleHeader0">1、需要安装哪些包</h2> <blockquote> <p>以下列出来的包安装在项目中即可,还有几个包需要全局安装:<br /><a href="http://www.js-code.com/tag/babel" title="babel" target="_blank">babel</a>、mocha、karma</p> </blockquote> <p><strong><a href="http://www.js-code.com/tag/babel" title="babel" target="_blank">babel</a> 相关的:</strong></p> <ul> <li><a href="http://www.js-code.com/tag/babel" title="浏览关于“babel”的文章" target="_blank" class="tag_link">babel</a>-core</li> <li>babel-plugin-syntax-jsx // 支持 jsx 语法</li> <li>babel-plugin-transform-runtime // 描述太晦涩, 官方文档: <a href="https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-runtime" rel="nofollow noreferrer" target="_blank">https://github.com/babel/babe...</a> </li> <li>babel-plugin-transform-vue-jsx // 支持 vue2.x jsx 语法</li> <li>babel-preset-env // 可以根据您所支持的环境自动确定您需要的Babel插件和polyfills(.babelrc 配置)</li> <li>babel-preset-es2015</li> <li>babel-preset-stage-2</li> <li>babel-runtime</li> <li>babel-polyfill // 支持老版本浏览器</li> </ul> <p><strong>karma、mocha、断言 包</strong></p> <ul> <li>karma</li> <li>mocha</li> <li>chai // BDD 、TDD 断言框架</li> <li>chalk // 终端里可以给输出内容添加颜色</li> <li>karma-mocha</li> <li>karma-coverage // 生成代码覆盖率</li> <li>karma-phantomjs-launcher // phantomjs 启动器</li> <li>karma-phantomjs-shim // 可以支持 phantomjs 默认不支持的功能,如 <code>Object.assign</code> 等...</li> <li>karma-sinon-chai</li> <li>karma-spec-reporter // 终端里输出测试结果</li> <li>karma-webpack // karma 支持 webpack 插件,有了它就不会报 <code>找不到requirejs</code>的错误信息了</li> <li>phantomjs-prebuilt // phantomjs 通过 npm 安装的版本</li> <li>sinon // 测试辅助工具,提供 spy、stub、mock 三种测试手段,模拟特定场景</li> <li>sinon-chai</li> </ul> <p><strong>webpack 相关</strong></p> <ul> <li>webpack</li> <li>babel-loader</li> <li>css-loader</li> <li>istanbul-instrumenter-loader // 代码覆盖率统计工具</li> <li>karma-sourcemap-loader</li> <li>style-loader</li> <li>url-loader</li> <li>vue-loader</li> <li>vue-style-loader</li> <li>extract-text-webpack-plugin</li> </ul> <p><strong>vue 核心包</strong></p> <ul> <li>vue</li> <li>vue-template-compiler</li> <li>vue-router</li> </ul> <h2 id="articleHeader1">2、如何配置</h2> <p>上面那么一大坨包安装好了,接下来该配置。配置主要是两个,一是 karma 的配置文件,另一个是 karma 需要的webpack 配置文件。webpack 的配置文件是为了解析那些需要测试的源文件的,说白了就是 vue 相关的文件,然后再给karma 的单元测试用例去识别。</p> <p><strong>webpack3 配置文件</strong><br />我是手动创建一个webpack.test.config.js 文件,然后内容配置如下</p> <blockquote> <p>webpack 相关知识可以参考我<a href="https://github.com/huangshuwei/blog/issues/1" rel="nofollow noreferrer" target="_blank">之前写的一篇</a></p> </blockquote> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="var path = require(&quot;path&quot;) var webpack = require(&quot;webpack&quot;) var ExtractTextPlugin = require('extract-text-webpack-plugin') function resolve(dir) { return path.join(__dirname, '..', dir) } var webpackConfig = { module: { rules: [ // babel-loader { test: /.js$/, use: 'babel-loader', include: [resolve('src'), resolve('test')] }, // 为了统计代码覆盖率,对 js 文件加入 istanbul-instrumenter-loader { test: /.(js)$/, exclude: /node_modules/, include: /src|packages/, enforce: 'post', use: [{ loader: &quot;istanbul-instrumenter-loader&quot;, options: { esModules: true }, }] }, // vue loader { test: /.vue$/, use: [{ loader: 'vue-loader', options: { // 为了统计代码覆盖率,对 vue 文件加入 istanbul-instrumenter-loader preLoaders: { js: 'istanbul-instrumenter-loader?esModules=true' } } }] }, // css loader { test: /.css$/, use: ExtractTextPlugin.extract({ use: 'css-loader', fallback: 'vue-style-loader' }) }, // img loader { test: /.(png|gif|jpe?g)(?S*)?$/, use: [{loader: 'url-loader'}] }, // font loader { test: /.(eot|woff|woff2|ttf|svg)(?S*)?$/, use: [{loader: 'url-loader'}] }, ] }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), // 调用组件的时候方便点 } }, plugins: [ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '&quot;production&quot;' } }) ] } module.exports = webpackConfig" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">"path"</span>) <span class="hljs-keyword">var</span> webpack = <span class="hljs-built_in">require</span>(<span class="hljs-string">"webpack"</span>) <span class="hljs-keyword">var</span> ExtractTextPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'extract-text-webpack-plugin'</span>) <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">resolve</span>(<span class="hljs-params">dir</span>) </span>{ <span class="hljs-keyword">return</span> path.join(__dirname, <span class="hljs-string">'..'</span>, dir) } <span class="hljs-keyword">var</span> webpackConfig = { <span class="hljs-attr">module</span>: { <span class="hljs-attr">rules</span>: [ <span class="hljs-comment">// babel-loader</span> { <span class="hljs-attr">test</span>: <span class="hljs-regexp">/.js$/</span>, <span class="hljs-attr">use</span>: <span class="hljs-string">'babel-loader'</span>, <span class="hljs-attr">include</span>: [resolve(<span class="hljs-string">'src'</span>), resolve(<span class="hljs-string">'test'</span>)] }, <span class="hljs-comment">// 为了统计代码覆盖率,对 js 文件加入 istanbul-instrumenter-loader</span> { <span class="hljs-attr">test</span>: <span class="hljs-regexp">/.(js)$/</span>, <span class="hljs-attr">exclude</span>: <span class="hljs-regexp">/<a href="http://www.js-code.com/tag/node" title="浏览关于“node”的文章" target="_blank" class="tag_link">node</a>_modules/</span>, <span class="hljs-attr">include</span>: <span class="hljs-regexp">/src|packages/</span>, <span class="hljs-attr">enforce</span>: <span class="hljs-string">'post'</span>, <span class="hljs-attr">use</span>: [{ <span class="hljs-attr">loader</span>: <span class="hljs-string">"istanbul-instrumenter-loader"</span>, <span class="hljs-attr">options</span>: { <span class="hljs-attr">esModules</span>: <span class="hljs-literal">true</span> }, }] }, <span class="hljs-comment">// vue loader</span> { <span class="hljs-attr">test</span>: <span class="hljs-regexp">/.vue$/</span>, <span class="hljs-attr">use</span>: [{ <span class="hljs-attr">loader</span>: <span class="hljs-string">'vue-loader'</span>, <span class="hljs-attr">options</span>: { <span class="hljs-comment">// 为了统计代码覆盖率,对 vue 文件加入 istanbul-instrumenter-loader</span> preLoaders: { <span class="hljs-attr">js</span>: <span class="hljs-string">'istanbul-instrumenter-loader?esModules=true'</span> } } }] }, <span class="hljs-comment">// css loader</span> { <span class="hljs-attr">test</span>: <span class="hljs-regexp">/.css$/</span>, <span class="hljs-attr">use</span>: ExtractTextPlugin.extract({ <span class="hljs-attr">use</span>: <span class="hljs-string">'css-loader'</span>, <span class="hljs-attr">fallback</span>: <span class="hljs-string">'vue-style-loader'</span> }) }, <span class="hljs-comment">// img loader</span> { <span class="hljs-attr">test</span>: <span class="hljs-regexp">/.(png|gif|jpe?g)(?S*)?$/</span>, <span class="hljs-attr">use</span>: [{<span class="hljs-attr">loader</span>: <span class="hljs-string">'url-loader'</span>}] }, <span class="hljs-comment">// font loader</span> { <span class="hljs-attr">test</span>: <span class="hljs-regexp">/.(eot|woff|woff2|ttf|svg)(?S*)?$/</span>, <span class="hljs-attr">use</span>: [{<span class="hljs-attr">loader</span>: <span class="hljs-string">'url-loader'</span>}] }, ] }, <span class="hljs-attr">resolve</span>: { <span class="hljs-attr">extensions</span>: [<span class="hljs-string">'.js'</span>, <span class="hljs-string">'.vue'</span>, <span class="hljs-string">'.json'</span>], <span class="hljs-attr">alias</span>: { <span class="hljs-string">'vue$'</span>: <span class="hljs-string">'vue/dist/vue.esm.js'</span>, <span class="hljs-string">'@'</span>: resolve(<span class="hljs-string">'src'</span>), <span class="hljs-comment">// 调用组件的时候方便点</span> } }, <span class="hljs-attr">plugins</span>: [ <span class="hljs-keyword">new</span> webpack.DefinePlugin({ <span class="hljs-string">'process.env'</span>: { <span class="hljs-attr">NODE_ENV</span>: <span class="hljs-string">'"production"'</span> } }) ] } <span class="hljs-built_in">module</span>.<a href="http://www.js-code.com/tag/export" title="浏览关于“export”的文章" target="_blank" class="tag_link">export</a>s = webpackConfig</code></pre> <p><strong>karma配置文件</strong></p> <p>直接 <code>cd</code> 到你的项目,然后执行下面命令,会提示你是否使用 require.js、浏览器环境、测试脚本存放位置等等。</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="$ karma init" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs shell"><code style="word-break: break-word; white-space: initial;"><span class="hljs-meta">$</span><span class="bash"> karma init</span></code></pre> <p>之后就会生成一个karma.config.js 文件,配置形式我是直接仿照vue-cli 官方的例子:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="var webpackConfig = require('../../build/webpack.test.config'); module.exports = function (config) { config.set({ // to run in additional browsers: // 1. install corresponding karma launcher // http://karma-runner.github.io/0.13/config/browsers.html // 2. add it to the `browsers` array below. browsers: ['PhantomJS'], frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], reporters: ['spec', 'coverage'], files: ['index.js'], preprocessors: { './index.js': ['webpack', 'sourcemap'] }, webpackMiddleware: { noInfo: true }, // 不显示 `webpack` 打包日志信息 webpackServer: { noInfo: true }, webpack: webpackConfig, coverageReporter: { dir: './coverage', reporters: [ { type: 'lcov', subdir: '.' }, { type: 'text-summary' } ] } }) }" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">var</span> webpackConfig = <span class="hljs-built_in">require</span>(<span class="hljs-string">'../../build/webpack.test.config'</span>); <span class="hljs-built_in">module</span>.exports = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">config</span>) </span>{ config.set({ <span class="hljs-comment">// to run in additional browsers:</span> <span class="hljs-comment">// 1. install corresponding karma launcher</span> <span class="hljs-comment">// http://karma-runner.github.io/0.13/config/browsers.html</span> <span class="hljs-comment">// 2. add it to the `browsers` array below.</span> browsers: [<span class="hljs-string">'PhantomJS'</span>], <span class="hljs-attr">frameworks</span>: [<span class="hljs-string">'mocha'</span>, <span class="hljs-string">'sinon-chai'</span>, <span class="hljs-string">'phantomjs-shim'</span>], <span class="hljs-attr">reporters</span>: [<span class="hljs-string">'spec'</span>, <span class="hljs-string">'coverage'</span>], <span class="hljs-attr">files</span>: [<span class="hljs-string">'index.js'</span>], <span class="hljs-attr">preprocessors</span>: { <span class="hljs-string">'./index.js'</span>: [<span class="hljs-string">'webpack'</span>, <span class="hljs-string">'sourcemap'</span>] }, <span class="hljs-attr">webpackMiddleware</span>: { <span class="hljs-attr">noInfo</span>: <span class="hljs-literal">true</span> }, <span class="hljs-comment">// 不显示 `webpack` 打包日志信息</span> webpackServer: { <span class="hljs-attr">noInfo</span>: <span class="hljs-literal">true</span> }, <span class="hljs-attr">webpack</span>: webpackConfig, <span class="hljs-attr">coverageReporter</span>: { <span class="hljs-attr">dir</span>: <span class="hljs-string">'./coverage'</span>, <span class="hljs-attr">reporters</span>: [ { <span class="hljs-attr">type</span>: <span class="hljs-string">'lcov'</span>, <span class="hljs-attr">subdir</span>: <span class="hljs-string">'.'</span> }, { <span class="hljs-attr">type</span>: <span class="hljs-string">'text-summary'</span> } ] } }) }</code></pre> <h2 id="articleHeader2">3、搭建目录结构</h2> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="├─build │ webpack.test.config.js │ ├─src │ ├─package.json │ └─test └─unit │ index.js │ karma.config.js │ ├─coverage │ └─specs *.spec.js" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs css"><code>├─<span class="hljs-selector-tag">build</span> │ <span class="hljs-selector-tag">webpack</span><span class="hljs-selector-class">.test</span><span class="hljs-selector-class">.config</span><span class="hljs-selector-class">.js</span> │ ├─<span class="hljs-selector-tag">src</span> │ ├─<span class="hljs-selector-tag">package</span><span class="hljs-selector-class">.json</span> │ └─<span class="hljs-selector-tag">test</span> └─<span class="hljs-selector-tag">unit</span> │ <span class="hljs-selector-tag">index</span><span class="hljs-selector-class">.js</span> │ <span class="hljs-selector-tag">karma</span><span class="hljs-selector-class">.config</span><span class="hljs-selector-class">.js</span> │ ├─<span class="hljs-selector-tag">coverage</span> │ └─<span class="hljs-selector-tag">specs</span> *<span class="hljs-selector-class">.spec</span><span class="hljs-selector-class">.js</span></code></pre> <p>测试文件相关都放置在 test/unit 下,入口文件为 index.js,每个vue 组件对应的测试用例名为<code>组件名称.spec.js</code>,根据 <code>istanbul-instrumenter-loader </code> 文档的说明,测试总入口文件 index.js 内容如下:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="import Vue from 'vue' Vue.config.productionTip = false // 测试所有以 .spec.js 名称结尾的文件 // require all test files (files that ends with .spec.js) const testsContext = require.context('./specs', true, /.spec$/) testsContext.keys().forEach(testsContext) // 要求除main.js之外的所有src文件进行覆盖 // require all src files except main.js for coverage. // you can also change this to match only the subset of files that // you want coverage for. const srcContext = require.context('../../src', true, /^./(?!main(.js)?$)/) srcContext.keys().forEach(srcContext) " title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">import</span> <a href="http://www.js-code.com/tag/vue" title="浏览关于“Vue”的文章" target="_blank" class="tag_link">Vue</a> <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span> Vue.config.productionTip = <span class="hljs-literal">false</span> <span class="hljs-comment">// 测试所有以 .spec.js 名称结尾的文件</span> <span class="hljs-comment">// require all test files (files that ends with .spec.js)</span> <span class="hljs-keyword"><a href="http://www.js-code.com/tag/const" title="浏览关于“const”的文章" target="_blank" class="tag_link">const</a></span> testsContext = <span class="hljs-built_in">require</span>.context(<span class="hljs-string">'./specs'</span>, <span class="hljs-literal">true</span>, /.spec$/) testsContext.keys().forEach(testsContext) <span class="hljs-comment">// 要求除main.js之外的所有src文件进行覆盖</span> <span class="hljs-comment">// require all src files except main.js for coverage.</span> <span class="hljs-comment">// you can also change <a href="http://www.js-code.com/tag/this" title="浏览关于“this”的文章" target="_blank" class="tag_link">this</a> to match only the subset of files that</span> <span class="hljs-comment">// you want coverage for.</span> <span class="hljs-keyword">const</span> srcContext = <span class="hljs-built_in">require</span>.context(<span class="hljs-string">'../../src'</span>, <span class="hljs-literal">true</span>, /^./(?!main(.js)?$)/) srcContext.keys().forEach(srcContext) </code></pre> <h2 id="articleHeader3">4、简单的 vue 组件单元测试</h2> <p>我们在 src 目录下创建一个 <code>Hello.vue</code> 组件</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text=" <template></p> <div>msg</div> <p></template><br /> <script> <a href="http://www.js-code.com/tag/export" title="export" target="_blank">export</a> default { name: 'hello', data () { return { msg: 'Welcome to Your <a href="http://www.js-code.com/tag/vue" title="Vue" target="_blank">Vue</a>.js App' } } } </script>" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="javascript"> &lt;template&gt; <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name"><a href="http://www.js-code.com/tag/div" title="浏览关于“div”的文章" target="_blank" class="tag_link">div</a></span>&gt;</span>msg<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span> &lt;<span class="hljs-regexp">/template&gt; &lt;script&gt; export default { name: 'hello', data () { return { msg: 'Welcome to Your Vue.js App' } } } &lt;/</span>script&gt;</code></pre> <p>然后在 test/unit/specs 下创建一个 <code>Hello.spec.js</code> 文件,再写个简单的单元测试用例:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="import Vue from 'vue' import Hello from '@/components/Hello' describe('Hello.vue', () => {<br /> it('should render correct contents', () => {<br /> const Constructor = Vue.extend(Hello)<br /> const vm = new Constructor().$mount()<br /> expect(vm.$el.querySelector('.hello h1').textContent)<br /> .to.equal('Welcome to Your Vue.js App')<br /> })<br /> })<br /> " title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="javascript"><span class="hljs-keyword">import</span> Vue <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span> <span class="hljs-keyword">import</span> Hello <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/Hello'</span> describe(<span class="hljs-string">'Hello.vue'</span>, () =&gt; { it(<span class="hljs-string">'should render correct contents'</span>, () =&gt; { <span class="hljs-keyword">const</span> Constructor = Vue.extend(Hello) <span class="hljs-keyword">const</span> vm = <span class="hljs-keyword">new</span> Constructor().$mount() expect(vm.$el.querySelector(<span class="hljs-string">'.hello h1'</span>).textContent) .to.equal(<span class="hljs-string">'Welcome to Your Vue.js App'</span>) }) }) </code></pre> <h2 id="articleHeader4">5、测试输出</h2> <p>在 package.json 中配置命令:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span><br /> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text=" // package.json &quot;scripts&quot;: { &quot;test&quot;: &quot;karma start test/unit/karma.config.js --single-run&quot; }" title="" data-original-title="复制"></span> </div> </p></div> <pre class="hljs objectivec"><code> <span class="hljs-comment">// package.json</span> <span class="hljs-string">"scripts"</span>: { <span class="hljs-string">"test"</span>: <span class="hljs-string">"karma start test/unit/karma.config.js --single-run"</span> }</code></pre> <p>输出结果:<br /><span class="img-wrap"><img data-src="/img/remote/1460000011484309?w=765&amp;h=339" src="https://static.segmentfault.com/v-5cc2cd8e/global/img/squares.svg" alt="2" title="2" style="cursor: pointer;"></span></p> <p>同时在 test/unit/coverage 生成测试报告。以上就是一个简单的 vue 单元测试实例。最后奉上<a href="https://github.com/huangshuwei/karma-mocha-webpack3-vue2" rel="nofollow noreferrer" target="_blank">源代码</a></p> <blockquote> <p>首发于我的<a href="https://github.com/huangshuwei/blog/issues/2" rel="nofollow noreferrer" target="_blank">个人博客</a></p> </blockquote>

本文固定链接: http://www.js-code.com/vue-js/vue-js_16832.html