<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ACzero (Liang)</title>
    <link>https://ruby-china.org/ACzero</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>写测试的基础入门</title>
      <description>&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;本文主要写给想接触测试但不知道如何下手的人。在第一次接触测试的时候，我是一头雾水的：测试到底要怎样写？接下来我会介绍 BDD 风格的测试写法，并介绍一些技巧与建议。示例分为 ruby 和 javascript 两个版本，分别使用&lt;a href="http://rspec.info/" rel="nofollow" target="_blank" title=""&gt;RSpec&lt;/a&gt;和&lt;a href="http://sinonjs.org/" rel="nofollow" target="_blank" title=""&gt;Sinon&lt;/a&gt;两个测试框架，在文中我将使用 ruby 版本，但你可以去下载对应的示例：&lt;a href="https://github.com/ACzero/rspec_test_example" rel="nofollow" target="_blank" title=""&gt;ruby 示例&lt;/a&gt;，&lt;a href="https://github.com/ACzero/mocha_test_example" rel="nofollow" target="_blank" title=""&gt;javascript 示例&lt;/a&gt;（PS：本文的例子省略了加载部分的代码，如需要能运行的代码请查看示例）。&lt;/p&gt;
&lt;h2 id="测试的种类"&gt;测试的种类&lt;/h2&gt;
&lt;p&gt;相信你在网上能找到各种介绍测试种类的文章，测试种类很多，如：单元测试、集成测试、验收测试等等。单元测试的测试对象是单独的模块或类的功能；而集成测试的测试对象则是需要多个模块、类共同工作的功能；验收测试描述最终用户行为，如用户点击某个地方会触发什么行为，通过测试重现这个场景。&lt;/p&gt;

&lt;p&gt;下面的示例都是单元测试，但是不用担心，只要掌握了基本的写法和技巧，写起来是没有太大区别的。&lt;/p&gt;
&lt;h2 id="测试的基本步骤"&gt;测试的基本步骤&lt;/h2&gt;
&lt;p&gt;一般来说测试的步骤分为：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;准备前置条件 (Setup)&lt;/li&gt;
&lt;li&gt;执行 (Exercise)&lt;/li&gt;
&lt;li&gt;验证 (Verification)&lt;/li&gt;
&lt;li&gt;清理数据 (Teardown)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;当然也可能存在其他步骤不同的风格。参考下面一个 rails 中的测试示例，并看看这些步骤都做了什么：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rails_helper'&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Teacher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :model&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s2"&gt;"create a new teacher"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"will strip and remove space in name"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="c1"&gt;# Setup&lt;/span&gt;
      &lt;span class="n"&gt;school&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;School&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'test'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;# Exercise&lt;/span&gt;
      &lt;span class="n"&gt;teacher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Teacher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;' a bc d'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;school: &lt;/span&gt;&lt;span class="n"&gt;school&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;# Verification&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;teacher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'abcd'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;# Teardown&lt;/span&gt;
      &lt;span class="n"&gt;school&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
      &lt;span class="n"&gt;teacher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy!&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;准备前置条件&lt;/strong&gt;：准备测试环境，一般是指插入测试用的数据，以及配置好一些变量等。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;执行&lt;/strong&gt;：执行需要测试的功能。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;验证&lt;/strong&gt;：验证功能执行之后各对象的状态（或行为）是否跟预期的一致。测试框架一般都会提供丰富的断言方法（后面会介绍）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;清理数据&lt;/strong&gt;：删除本次测试中创建的数据，以及还原配置为默认值，使其不影响后面要运行的测试。&lt;/p&gt;
&lt;h2 id="描述你的测试"&gt;描述你的测试&lt;/h2&gt;
&lt;p&gt;假如你看过 BDD 风格的测试代码，那你肯定能看到&lt;code&gt;describe&lt;/code&gt;，&lt;code&gt;context&lt;/code&gt;，&lt;code&gt;it&lt;/code&gt;等方法，这些方法其实并没有什么特殊的功能，其实就是用来描述测试的 DSL。&lt;/p&gt;

&lt;p&gt;首先是我们要测试的是&lt;code&gt;Calculator&lt;/code&gt;这个 module，包含一个&lt;code&gt;is_odd?&lt;/code&gt;方法用来检验参数是不是奇数。对应的测试代码如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Calculator&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'.is_odd?'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when argument is odd'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'will not raise error'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_odd?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;not_to&lt;/span&gt; &lt;span class="n"&gt;raise_error&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'return true'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_odd?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when argument is even'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'will not raise error'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_odd?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;not_to&lt;/span&gt; &lt;span class="n"&gt;raise_error&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'return false'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_odd?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先指出，这里 describe、context、it 后面跟的字符串参数只起到描述作用，甚至可以不填。我们逐一查看，我们用&lt;code&gt;describe&lt;/code&gt;描述要测试的是 Calculator 模块，然后又用&lt;code&gt;describe&lt;/code&gt;表示要测试这个模块的&lt;code&gt;is_odd?&lt;/code&gt;方法。接下来的&lt;code&gt;context&lt;/code&gt;代表条件，此处是“当参数为奇数”，在这个&lt;code&gt;context&lt;/code&gt;方法的块中包含了两个&lt;code&gt;it&lt;/code&gt;方法，在里面就是我们要执行的测试。将这段测试代码用语言来描述，就是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;测试 Calculator 的&lt;code&gt;is_odd?&lt;/code&gt;方法，当参数为奇数时，不应该抛出异常。&lt;/li&gt;
&lt;li&gt;测试 Calculator 的&lt;code&gt;is_odd?&lt;/code&gt;方法，当参数为奇数时，返回&lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;测试 Calculator 的&lt;code&gt;is_odd?&lt;/code&gt;方法，当参数为偶数时，不应该抛出异常。&lt;/li&gt;
&lt;li&gt;测试 Calculator 的&lt;code&gt;is_odd?&lt;/code&gt;方法，当参数为偶数时，返回&lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;实际上，当你的测试失败时，rspec 就会根据你的描述打印出对应的信息，来帮助你快速定位到哪里出错：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Failures:

  1&lt;span class="o"&gt;)&lt;/span&gt; Calculator.is_odd? when argument is even &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="nb"&gt;false
     &lt;/span&gt;Failure/Error: expect&lt;span class="o"&gt;(&lt;/span&gt;Calculator.is_odd?&lt;span class="o"&gt;(&lt;/span&gt;2&lt;span class="o"&gt;))&lt;/span&gt;.to be &lt;span class="nb"&gt;false

       &lt;/span&gt;expected &lt;span class="nb"&gt;false
            &lt;/span&gt;got &lt;span class="nb"&gt;true&lt;/span&gt;
     &lt;span class="c"&gt;# ./example1/calculator_spec.rb:26:in `block (4 levels) in &amp;lt;top (required)&amp;gt;'&lt;/span&gt;

Finished &lt;span class="k"&gt;in &lt;/span&gt;0.0201 seconds &lt;span class="o"&gt;(&lt;/span&gt;files took 0.56765 seconds to load&lt;span class="o"&gt;)&lt;/span&gt;
4 examples, 1 failure

Failed examples:

rspec ./example1/calculator_spec.rb:25 &lt;span class="c"&gt;# Calculator.is_odd? when argument is even return false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="断言(assertion)"&gt;断言 (assertion)&lt;/h2&gt;
&lt;p&gt;你在上面的例子已经看到我们用&lt;code&gt;expect&lt;/code&gt;来验证测试结果了，这是其中一种验证风格，此外还有其他的风格，如 assert。不过他们做的事情都是一样的。&lt;/p&gt;
&lt;h3 id="相等性检验"&gt;相等性检验&lt;/h3&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'expect equality'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;not_to&lt;/span&gt; &lt;span class="n"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两段测试，需要注意的是这里的&lt;code&gt;eq&lt;/code&gt;和&lt;code&gt;equal&lt;/code&gt;方法是用什么方式检验相等性的（根据具体语言有所不同），从文档可以知道&lt;code&gt;eq&lt;/code&gt;方法通过调用&lt;code&gt;==&lt;/code&gt;方法来验证，而&lt;code&gt;equal&lt;/code&gt;则通过调用&lt;code&gt;equal?&lt;/code&gt;方法来验证。则上面两个验证等价于作了这样的验证：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# expect(foo).to eq(1)&lt;/span&gt;
&lt;span class="n"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# expect(foo).not_to equal([1, 2, 3])&lt;/span&gt;
&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal?&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在进行相等性检验时建议先认真阅读文档。&lt;/p&gt;
&lt;h3 id="检验异常抛出"&gt;检验异常抛出&lt;/h3&gt;
&lt;p&gt;当需要检验是否抛出某异常（或没有抛出异常时），也有对应的方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'exception'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;raise_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NoMethodError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="检验状态变化"&gt;检验状态变化&lt;/h3&gt;
&lt;p&gt;rspec 提供了方便的写法来检验对象状态变化：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'state change'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外还有很多不同的 helper，请查阅文档。&lt;/p&gt;
&lt;h2 id="测试替身(Test Double)"&gt;测试替身 (Test Double)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Double&lt;/code&gt;一词来源于拍电影中常用的&lt;code&gt;stunt double(替身演员)&lt;/code&gt;，顾名思义，替身的作用是用于替换掉功能中的某个部分，通常会应用在下列情况中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;要测试的功能需要访问一些外部服务（如 web API）&lt;/li&gt;
&lt;li&gt;要测试的功能由几个模块共同工作，但可能有一个甚至多个模块还没完成。&lt;/li&gt;
&lt;li&gt;想要验证功能中的模块是不是按预期被调用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;使用替身可以屏蔽掉这些外部依赖，让测试关注点回到要测试的功能本身。&lt;/p&gt;

&lt;p&gt;替身的种类有很多，这里介绍常见的三种：&lt;code&gt;stub&lt;/code&gt;、&lt;code&gt;spy&lt;/code&gt;、&lt;code&gt;mock&lt;/code&gt;。要注意关于这三者的定义有很多不同见解，这里的定义是我参考了&lt;code&gt;Sinon&lt;/code&gt;和&lt;code&gt;RSpec&lt;/code&gt;两个框架总结出来的。为避免先入为主，你可以先自己搜索一下。&lt;/p&gt;
&lt;h3 id="Stub"&gt;Stub&lt;/h3&gt;
&lt;p&gt;stub 的作用是为特定的方法调用设置返回值。&lt;/p&gt;

&lt;p&gt;比如说当你的测试会调用&lt;code&gt;File.read('fname')&lt;/code&gt;，但你实际并不想他真的去读取一个文件，那就可以用 stub 屏蔽掉真正的读取操作并设置一个 string 对象作为返回值。&lt;/p&gt;

&lt;p&gt;下面的例子再介绍一个用法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Calendar&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;today_day_off?&lt;/span&gt;
    &lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saturday?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sunday?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类&lt;code&gt;Calendar&lt;/code&gt;中有一个&lt;code&gt;#today_day_off?&lt;/code&gt;方法判断今日是否休息日，但是因为 Calendar 类使用&lt;code&gt;Date.today()&lt;/code&gt;方法去获取当前日期，这导致运行结果会跟测试运行时的日期有关，这显然不是我们想要的。因此我们在测试中使用了&lt;code&gt;stub&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Calendar&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'.today_day_off?'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when today is sunday'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="c1"&gt;# before中的内容会在该块中每个测试运行前执行&lt;/span&gt;
      &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="c1"&gt;# stub Date.today&lt;/span&gt;
        &lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:today&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2017-07-23'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'return true'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today_day_off?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when today is monday'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="c1"&gt;# stub Date.today&lt;/span&gt;
        &lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:today&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2017-07-24'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'return false'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today_day_off?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意在&lt;code&gt;before&lt;/code&gt;的块中我们 stub 了 Date 的&lt;code&gt;today&lt;/code&gt;方法，使其返回一个我们指定的 Date 对象。这样我们就可以测试 Calendar 在 7 月 23 号和 7 月 24 号时运行的行为。&lt;/p&gt;

&lt;p&gt;还有一点，使用 stub 的前提是你必须清楚你测试的对象的内部实现（这里是 Calendar 类），才能对内部的方法进行 stub。&lt;/p&gt;
&lt;h3 id="Spy"&gt;Spy&lt;/h3&gt;
&lt;p&gt;spy 的作用是记录对象的行为，可用于验证在对象上的方法调用。&lt;/p&gt;

&lt;p&gt;看以下例子，这里有个&lt;code&gt;MyHelper&lt;/code&gt;的 module：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MyHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;average_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fdiv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要测试内部是否是使用&lt;code&gt;reduce&lt;/code&gt;方法来计算总和的，则可以使用 spy&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;MyHelper&lt;/span&gt;

&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;MyHelper&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'#average_of'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'use reduce to sum'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;arr_spy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spy&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="n"&gt;average_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr_spy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr_spy&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;have_received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中我们先创建了一个数组对象的 spy 对象，这个 spy 对象的行为跟数组一致，但是会记录进行过的方法调用，把 arr_spy 作为 average_of 的参数调用后，通过检查 arr_spy 这个对象是否被调用过&lt;code&gt;reduce&lt;/code&gt;方法就可以达到目的。此外，spy 还可以验证方法调用接收了什么参数。&lt;/p&gt;
&lt;h3 id="mock"&gt;mock&lt;/h3&gt;
&lt;p&gt;mock 的功能是设置响应（stub）以及验证预期行为（spy）。一般使用的时候会生成一个 mock 对象，然后再设置该对象的方法响应。&lt;/p&gt;

&lt;p&gt;mock 主要用于依赖的模块没有完成时，能正常运行测试。参考下面例子：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_enough?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="vi"&gt;@valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;valid?&lt;/span&gt;
    &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="vi"&gt;@valid&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们有一个&lt;code&gt;Order&lt;/code&gt;类，需要使用&lt;code&gt;Warehouse&lt;/code&gt;对象进行初始化。当&lt;code&gt;Warehouse&lt;/code&gt;这个类还没实现的时候，我们就可以使用 mock 先制造一个"替身"：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'create new order'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when inventory is enough'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'order is valid'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'warehouse'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:has_enough?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:remove&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="s1"&gt;'when inventory is not enough'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'order is invalid'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'warehouse'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:has_enough?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid?&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 RSpec 中使用&lt;code&gt;double&lt;/code&gt;方法创建 mock 对象，要注意的是 mock 在使用上跟 spy 有一些不同，我们需要先为 mock 对象设置方法及其响应，mock 对象会验证这些方法调用，假如到测试结束时，设置的方法都没有被调用，测试就会报错。而且这个验证是在执行前就定义了，因此我们不需要额外去验证。&lt;/p&gt;
&lt;h3 id="关于测试替身"&gt;关于测试替身&lt;/h3&gt;
&lt;p&gt;上面讲了这么多，或许你已经晕了。对&lt;code&gt;stub&lt;/code&gt;、&lt;code&gt;spy&lt;/code&gt;和&lt;code&gt;mock&lt;/code&gt;的定义有各种不同的观点，以至于 RSpec 的文档都没有对其作定义，而是扔给你&lt;a href="http://www.rubydoc.info/gems/rspec-mocks/frames#Further_Reading" rel="nofollow" target="_blank" title=""&gt;一些文章&lt;/a&gt;让你自己去纠结。&lt;/p&gt;

&lt;p&gt;我建议不需要太执着于这些替身的定义，更重要的是去实现&lt;strong&gt;我们的需求&lt;/strong&gt;。先搞清楚我们需要如何使用替身，然后选择测试框架给我们提供的方法去实现就足够了。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;这些都是我在刚开始学习写测试的时候疑惑的地方，特别是对于替身的使用让我纠结了很久。而对于其他没有说的问题，有些是我还没遇到的，有些则是我认为不会太难理解的，例如准备测试数据 (fixture 或 factory) 等。&lt;/p&gt;
&lt;h2 id="参考链接"&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://martinfowler.com/articles/mocksArentStubs.html" rel="nofollow" target="_blank" title=""&gt;Mocks Aren't Stubs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://robots.thoughtbot.com/spy-vs-spy" rel="nofollow" target="_blank" title=""&gt;Spy vs Spy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://relishapp.com/rspec/" rel="nofollow" target="_blank" title=""&gt;RSpec doc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://sinonjs.org/releases/v2.3.8/" rel="nofollow" target="_blank" title=""&gt;Sinon doc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;假如文章中有什么错误或值得讨论的地方，非常欢迎大家指出&lt;img title=":smile:" alt="😄" src="https://twemoji.ruby-china.com/2/svg/1f604.svg" class="twemoji"&gt; &lt;/p&gt;</description>
      <author>ACzero</author>
      <pubDate>Mon, 24 Jul 2017 17:50:51 +0800</pubDate>
      <link>https://ruby-china.org/topics/33614</link>
      <guid>https://ruby-china.org/topics/33614</guid>
    </item>
    <item>
      <title>为 Rails 项目搭建 Jenkins CI 服务器</title>
      <description>&lt;h2 id="概述"&gt;概述&lt;/h2&gt;
&lt;p&gt;最近重新搭建了一次 Jenkins CI 服务，并尝试了使用 pipeline 配置任务，结合以前 Jenkins 的配置经验，稍微做了一下总结。本文将讲述如何从头开始搭建一个 Jenkins 服务器并进行相关配置，接着会介绍配置 Jenkins 任务的几种方式，最后会列举一个 rails + jenkins + bitbucket 的 CI 任务配置实例。想先看效果的话建议直接跳到最后一节&lt;img title=":joy:" alt="😂" src="https://twemoji.ruby-china.com/2/svg/1f602.svg" class="twemoji"&gt; &lt;/p&gt;
&lt;h2 id="搭建jenkins服务器"&gt;搭建 jenkins 服务器&lt;/h2&gt;
&lt;p&gt;以下搭建步骤是在 ubuntu14.04 下进行，其他环境的搭建步骤参考官方的&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins" rel="nofollow" target="_blank" title=""&gt;文档&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="安装jenkins"&gt;安装 jenkins&lt;/h3&gt;
&lt;p&gt;参照&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu" rel="nofollow" target="_blank" title=""&gt;文档&lt;/a&gt;，在 ubuntu 下安装步骤比较简单：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; - https://pkg.jenkins.io/debian/jenkins-ci.org.key | &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-key add -
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo deb http://pkg.jenkins.io/debian-stable binary/ &amp;gt; /etc/apt/sources.list.d/jenkins.list'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;jenkins
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="jenkins访问配置"&gt;jenkins 访问配置&lt;/h3&gt;
&lt;p&gt;Jenkins 默认使用&lt;code&gt;8080&lt;/code&gt;端口，若要使用其他端口可以在&lt;code&gt;/etc/default/jenkins&lt;/code&gt;文件下修改这一行：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP_PORT=8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了访问 Jenkins 服务器，我们要将 80 端口的请求代理到 Jenkins 使用的端口，参考官方提供的 Nginx 配置：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upstream app_server {
    # 若jenkins使用其他端口，127.0.0.1:8080要作对应修改
    server 127.0.0.1:8080 fail_timeout=0;
}

server {
    listen 80;
    listen [::]:80 default ipv6only=on;
    server_name ci.yourcompany.com;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;

        if (!-f $request_filename) {
            proxy_pass http://app_server;
            break;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如你需要使用 ssl，参考这个&lt;a href="https://gist.github.com/rdegges/913102#gistcomment-198697" rel="nofollow" target="_blank" title=""&gt;配置&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="注意事项"&gt;注意事项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Jenkins 安装完成后会自动创建一个名为&lt;code&gt;jenkins&lt;/code&gt;的用户，用户的根目录位于&lt;code&gt;/var/lib/jenkins&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Jenkins 在执行任务时会以这个&lt;code&gt;jenkins&lt;/code&gt;用户执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="Jenkins初始化与配置"&gt;Jenkins 初始化与配置&lt;/h2&gt;&lt;h3 id="初始化"&gt;初始化&lt;/h3&gt;
&lt;p&gt;上一步 Jenkins 访问配置完成之后，访问 Jenkins 服务器的地址&lt;code&gt;ci.yourcompany.com&lt;/code&gt;。首次访问需要进行初始化：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-20/unlock.png" title="" alt="unlock"&gt;&lt;/p&gt;

&lt;p&gt;根据提示执行以下命令取得初始化密码。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /var/lib/jenkins/secrets/initialAdminPassword
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入密码后需要设置 Jenkins 的管理员账号，并且选择需要安装的插件，插件可以在进入系统之后管理，因此此处按默认配置即可。&lt;/p&gt;

&lt;p&gt;至此 Jenkins 服务器已经可用，接下来介绍一些常用但非必要的配置。&lt;/p&gt;
&lt;h3 id="shell配置"&gt;shell 配置&lt;/h3&gt;
&lt;p&gt;Jenkins 在任务运行过程中可以执行 shell 脚本，但在使用前请先检查默认配置的 shell 是否是你想要的。进入 Manage Jenkins -&amp;gt; System Configuration，检查&lt;code&gt;Shell&lt;/code&gt;选项。&lt;/p&gt;
&lt;h3 id="ssh密钥配置"&gt;ssh 密钥配置&lt;/h3&gt;
&lt;p&gt;在 CI 执行的过程中需要使用到的密钥可以在首页的&lt;code&gt;Credentials&lt;/code&gt;中配置，在对应的 Domain 下选择&lt;code&gt;Add credentials&lt;/code&gt;，进入如下页面：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-20/ssh_cert.png" title="" alt="ssh_cert"&gt;&lt;/p&gt;

&lt;p&gt;我们需要添加 ssh 密钥的配置，类型选择&lt;code&gt;SSH Username with private key&lt;/code&gt;。private key 的提供方式有三种，第一种&lt;code&gt;Enter directly&lt;/code&gt;是直接填到 Jenkins 的配置中，后两种需要在服务器上生成 ssh 密钥。例如采用第三种&lt;code&gt;From the Jenkins master ~/.ssh&lt;/code&gt;，需要像这样生成 ssh 密钥：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su jenkins
&lt;span class="nv"&gt;$ &lt;/span&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择好配置之后点&lt;code&gt;OK&lt;/code&gt;，以后在编写任务的时候就可以使用该密钥。&lt;/p&gt;
&lt;h3 id="管理项目配置文件"&gt;管理项目配置文件&lt;/h3&gt;
&lt;p&gt;项目中的密钥配置不会放在代码库中，可以使用 Jenkins 的插件&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/Config+File+Provider+Plugin" rel="nofollow" target="_blank" title=""&gt;Config File Provider Plugin&lt;/a&gt;。安装插件后进入 Manage Jenkins -&amp;gt; Managed files 即可创建配置文件。&lt;/p&gt;
&lt;h2 id="编写Jenkins任务"&gt;编写 Jenkins 任务&lt;/h2&gt;
&lt;p&gt;编写 Jenkins 任务主要有两种方式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用 Jenkins 提供的 freestyle project&lt;/li&gt;
&lt;li&gt;用户编写 pipeline&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;freestyle project 上手简单，基本上参考一个例子就能掌握，但需要按照给定的步骤执行 CI 流程，不够灵活。pipeline 使用 Groovy 编写，需要用户掌握一点 Groovy 语法，而且插件文档不太完善，某些插件可能没提供在 pipeline 中使用的文档，但 pipeline 最大的优势是灵活，用户可以自行控制整个 CI 的流程。下面将以配置一个 Rails 项目的 CI 任务来讲解。&lt;/p&gt;
&lt;h3 id="Freestyle project"&gt;Freestyle project&lt;/h3&gt;
&lt;p&gt;新建一个&lt;code&gt;freestyle project&lt;/code&gt;，进入配置。整个任务可以分为六个部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General&lt;/li&gt;
&lt;li&gt;Source Code Management&lt;/li&gt;
&lt;li&gt;Build Triggers&lt;/li&gt;
&lt;li&gt;Build Enviroments&lt;/li&gt;
&lt;li&gt;Build&lt;/li&gt;
&lt;li&gt;Post-build Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;首先要说明的是，对于每个任务，Jenkins 都会生成一个 workspace，并会把项目代码 clone 到 workspace 中，在配置中的路径的当前路径均是对应 workspace 的目录 (即项目的根目录)。&lt;/p&gt;
&lt;h4 id="General"&gt;General&lt;/h4&gt;
&lt;p&gt;项目的基础信息配置，在每个选项旁都有一个问号，点开后可以看到详细的文档介绍。&lt;/p&gt;
&lt;h4 id="Source Code Management"&gt;Source Code Management&lt;/h4&gt;
&lt;p&gt;选择拉取代码的方式。我们这里选择 Git，使用 SSH 方式拉取，&lt;code&gt;Repository URL&lt;/code&gt;填写项目的 SSH 地址，Credentials 处需要选择之前添加的 SSH 密钥 (PS: 不要忘了在代码托管服务中添加该 SSH 密钥的公钥)。配置如图：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-20/source_code_management.png" title="" alt="source_code_management"&gt;&lt;/p&gt;
&lt;h4 id="Build Triggers"&gt;Build Triggers&lt;/h4&gt;
&lt;p&gt;该任务的触发方式，不作设置的话只能手动触发，但可以配置为在某个任务结束之后触发，或者是在代码托管收到 commit 时通知 Jenkins，可能需要另外安装插件。此外还能选择采用轮询的方式 (不推荐)。&lt;/p&gt;
&lt;h4 id="Build Enviroments"&gt;Build Enviroments&lt;/h4&gt;
&lt;p&gt;这个步骤会为 build 准备环境，此处我们先安装&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/RVM+Plugin" rel="nofollow" target="_blank" title=""&gt;RVM Plugin&lt;/a&gt;，之后我们就能看到&lt;code&gt;Run the build in a RVM-managed environment&lt;/code&gt;的选项，作以下配置：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-20/rvm.png" title="" alt="rvm"&gt;&lt;/p&gt;
&lt;h4 id="Build"&gt;Build&lt;/h4&gt;
&lt;p&gt;该部分配置 CI 过程执行什么的地方 (例如跑测试，生成报告等等)。点击&lt;code&gt;add build step&lt;/code&gt;添加构建步骤。如果安装了&lt;code&gt;Config File Provider Plugin&lt;/code&gt;，则可以选择&lt;code&gt;Provide Configuration files&lt;/code&gt;，选择配置文件后填好路径，这样在任务执行时配置文件就会被复制到目标路径。配置如图：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-20/provide_config_file.png" title="" alt="provide_config_file"&gt;&lt;/p&gt;

&lt;p&gt;接下来需要编写 shell 脚本，添加&lt;code&gt;Execute shell&lt;/code&gt;，编写以下 shell script:&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler
bundle &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# 重新建立数据库&lt;/span&gt;
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rake db:drop &lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test
&lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rake db:create &lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test
&lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rake db:migrate &lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# 执行测试&lt;/span&gt;
bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rake &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="Post-build Actions"&gt;Post-build Actions&lt;/h4&gt;
&lt;p&gt;Build 结束后要执行的动作，一般可以用于向代码托管服务提交构建结果，发送 email，发布 HTML 页面等等，有一部分需要插件。这个例子就不配置了。&lt;/p&gt;

&lt;p&gt;以上就是 Freestyle project 的简单配置流程，基本可以满足接收通知-&amp;gt;构建-&amp;gt;测试-&amp;gt;结果反馈的流程，而且也有各种插件支持。&lt;/p&gt;
&lt;h3 id="Pipeline"&gt;Pipeline&lt;/h3&gt;
&lt;p&gt;新建一个&lt;code&gt;Pipeline&lt;/code&gt;项目，可以看到只有四个部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;General&lt;/li&gt;
&lt;li&gt;Build Triggers&lt;/li&gt;
&lt;li&gt;Advanced Project Options&lt;/li&gt;
&lt;li&gt;Pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;General 和 Build Triggers 跟 freestyle project 中的是一样的，Advanced Project Options 默认只有一个设置显示名。基本上全部的配置都集中在 Pipeline。pipeline 的脚本可以直接写在 Jenkins 任务的配置中，也可以放在项目根目录&lt;code&gt;Jenkinsfile&lt;/code&gt;文件里。最佳实践是在项目中创建&lt;code&gt;Jenkinsfile&lt;/code&gt;并加入版本控制中。&lt;/p&gt;
&lt;h4 id="Pipeline script"&gt;Pipeline script&lt;/h4&gt;
&lt;p&gt;用户可以使用两种方式编写&lt;a href="https://jenkins.io/doc/book/pipeline/#overview" rel="nofollow" target="_blank" title=""&gt;Pipeline script&lt;/a&gt;：声名式 (declarative) 和脚本式 (scripted)。声明式使用 DSL 来描述 pipeline，而脚本式则完全是使用 Groovy 来编写。个人认为声明式并没有怎样简化 pipeline 的编写，只是提高了可读性。总体看来脚本式和声明式差不多，下面编写流程将采用脚本式 pipeline，对声明式感兴趣的话可以浏览&lt;a href="https://jenkins.io/doc/book/pipeline/syntax/" rel="nofollow" target="_blank" title=""&gt;官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;对于脚本式 pipeline，结构大体上是这样的：&lt;/p&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Build'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'make'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'make check'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Deploy'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'make publish'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户可以定义各种阶段 (stage) 进行不同的工作。这里先介绍一下怎样快速上手 pipeline。在项目的页面左侧能找到&lt;code&gt;Pipeline Syntax&lt;/code&gt;的选项，可以进入一个生成 pipeline script 的页面，相当于查看文档，当安装了插件后，部分插件的方法也会被添加到这里。用的比较多的是 Jenkins 提供的&lt;code&gt;sh&lt;/code&gt;方法，可以执行给定的 shell 脚本，并且可以设置把 stdout 作为返回值，具体参考&lt;a href="https://jenkins.io/doc/pipeline/steps/workflow-durable-task-step/#code-sh-code-shell-script" rel="nofollow" target="_blank" title=""&gt;文档&lt;/a&gt;。pipeline 生成页面如图：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-20/pipeline_generator.png" title="" alt="pipeline_generator"&gt;&lt;/p&gt;

&lt;p&gt;接下来编写一个跟上面的 freestyle project 相同的 pipeline 任务。首先必须确保 CI 运行环境，要先安装 rvm。可以在 pipeline 中进行安装，我选择的是在服务器上直接装好：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su jenkins
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;gpg &lt;span class="nt"&gt;--keyserver&lt;/span&gt; hkp://keys.gnupg.net &lt;span class="nt"&gt;--recv-keys&lt;/span&gt; 409B6B1796C275462A1703113804BB82D39DC0E3
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="se"&gt;\c&lt;/span&gt;url &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://get.rvm.io | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;pipeline 的结构如下：&lt;/p&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;rails_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Preparation'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
      &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
      &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先看 Preparation 阶段。首先要做的工作是从代码库中 clone 代码，从 generator 生成如下的方法调用：&lt;/p&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="nl"&gt;branch:&lt;/span&gt; &lt;span class="s1"&gt;'master'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;credentialsId:&lt;/span&gt; &lt;span class="s1"&gt;'b33c1c57-737d-4195-a6f5-446a9d000f2a'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;url:&lt;/span&gt; &lt;span class="s1"&gt;'git@jenkins/example.git'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来是准备测试环境，为了让 pipeline 更加灵活，可以通过项目里的.ruby-version 文件安装和使用指定版本的 ruby。假如想要使用特定版本的 bundler 进行&lt;code&gt;bundle install&lt;/code&gt;，可以通过检测&lt;code&gt;Gemfile.lock&lt;/code&gt;中的信息来获取 bundler 的版本 (适用于大于 1.10 版本的 bundler 生成的&lt;code&gt;Gemfile.lock&lt;/code&gt;)。上述步骤完成后需要先删除上次任务执行时的数据库并重新创建。编写如下 pipeline:&lt;/p&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Preparation'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
        &lt;span class="c1"&gt;// 拉取代码&lt;/span&gt;
        &lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="nl"&gt;branch:&lt;/span&gt; &lt;span class="s1"&gt;'master'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;credentialsId:&lt;/span&gt; &lt;span class="s1"&gt;'b33c1c57-737d-4195-a6f5-446a9d000f2a'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;url:&lt;/span&gt; &lt;span class="s1"&gt;'git@jenkins/example.git'&lt;/span&gt;

        &lt;span class="c1"&gt;// 获取项目ruby版本&lt;/span&gt;
        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;rubyVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;script:&lt;/span&gt; &lt;span class="s1"&gt;'cat .ruby-version'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;returnStdout:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'source $HOME/.bashrc'&lt;/span&gt;

        &lt;span class="c1"&gt;// 假如没有安装所需版本ruby，用rvm进行安装&lt;/span&gt;
        &lt;span class="c1"&gt;// "${foo}"为groovy的字符串插值方式&lt;/span&gt;
        &lt;span class="c1"&gt;// """也是groovy中表示字符串的方式&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"""if ! rvm ${rubyVersion} do ruby -v &amp;amp;&amp;gt; /dev/null; then
        rvm get stable
        rvm install ${rubyVersion}
        fi"""&lt;/span&gt;

        &lt;span class="c1"&gt;// 获取bundler版本并进行安装&lt;/span&gt;
        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;bundlerVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;script:&lt;/span&gt; &lt;span class="s1"&gt;'''rvm all do ruby -e 'puts $&amp;lt;.read[/BUNDLED WITH\\n   (\\S+)$/, 1] || "&amp;lt;1.10"' Gemfile.lock'''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;returnStdout:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do gem install bundler --conservative --no-document -v ${bundlerVersion}"&lt;/span&gt;
        &lt;span class="c1"&gt;// 在CI环境下适合使用--deployment选项，具体看:http://bundler.io/v1.14/man/bundle-install.1.html#DEPLOYMENT-MODE&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle install --deployment --retry=3"&lt;/span&gt;

        &lt;span class="c1"&gt;// 使用Config File Provider Plugin提供的方法复制配置文件&lt;/span&gt;
        &lt;span class="n"&gt;configFileProvider&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="n"&gt;configFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fileId:&lt;/span&gt; &lt;span class="s1"&gt;'cd003fbc-d1ae-496b-b68c-b08f0640a286'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;targetLocation:&lt;/span&gt; &lt;span class="s1"&gt;'config/secrets.yml'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;variable:&lt;/span&gt; &lt;span class="s1"&gt;'SECRET_FILE'&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;configFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fileId:&lt;/span&gt; &lt;span class="s1"&gt;'fff7f49d-254b-478e-ab7c-f4587927cdbb'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;targetLocation:&lt;/span&gt; &lt;span class="s1"&gt;'config/database.yml'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;variable:&lt;/span&gt; &lt;span class="s1"&gt;'DATABASE_FILE'&lt;/span&gt;&lt;span class="o"&gt;)]){}&lt;/span&gt;

        &lt;span class="c1"&gt;// 重新建立测试用数据库&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle exec rake db:drop db:create db:migrate RAILS_ENV=${rails_env}"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是测试步骤的 pipeline&lt;/p&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;rails_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Preparation'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;rubyVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;script:&lt;/span&gt; &lt;span class="s1"&gt;'cat .ruby-version'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;returnStdout:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle exec rake test RAILS_ENV=${rails_env}"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本例子测试步骤比较简单，这点因项目测试使用的工具不同而不同。至此跟上述 freestyle project 相同功能的 pipeline script 编写完成。&lt;/p&gt;
&lt;h4 id="Pipeline script from SCM"&gt;Pipeline script from SCM&lt;/h4&gt;
&lt;p&gt;这是官方推荐的使用方式，适合为一个项目的每个分支配置不同的 pipeline，例如测试分支在 commit 后进行测试并部署到测试服务器，发布分支在 commit 后直接部署到生产服务器。编写方式跟 Pipeline script 大体相同，唯一不同的只有拉取代码的配置。当选择了&lt;code&gt;Pipeline script from SCM&lt;/code&gt;后，可以配置 SCM 和脚本的文件名，默认为&lt;code&gt;Jenkinsfile&lt;/code&gt;。 &lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-20/pipeline_from_scm.png" title="" alt="pipeline_from_scm"&gt;&lt;/p&gt;

&lt;p&gt;然后脚本要作如下修改：&lt;/p&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 从设置好的SCM拉取代码到workspace&lt;/span&gt;
    &lt;span class="n"&gt;checkout&lt;/span&gt; &lt;span class="n"&gt;scm&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;rails_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;rubyVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;script:&lt;/span&gt; &lt;span class="s1"&gt;'cat .ruby-version'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;returnStdout:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Preparation'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
        &lt;span class="c1"&gt;// 此处删去拉取代码的方法&lt;/span&gt;

        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'source $HOME/.bashrc'&lt;/span&gt;
        &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
        &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="Bitbucket+Pipeline实现rails项目CI服务"&gt;Bitbucket+Pipeline 实现 rails 项目 CI 服务&lt;/h2&gt;
&lt;p&gt;首先介绍这个 CI 服务的工作流，先上图：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-22/ci_workflow.png" title="" alt="ci_workflow"&gt;&lt;/p&gt;

&lt;p&gt;当 bitbucket 上有新的 commit 或者 pull request 时，会通知 Jenkins 服务器，CI 工作流开始。首先是&lt;code&gt;Preparation&lt;/code&gt;阶段，拉取代码后执行 bundle install 等命令，为 rails 项目准备测试环境。接着是&lt;code&gt;Test&lt;/code&gt;阶段，运行项目对应的测试，输出测试结果。接着是&lt;code&gt;Report&lt;/code&gt;阶段，可以使用&lt;code&gt;brakeman&lt;/code&gt;，&lt;code&gt;rubocop&lt;/code&gt;和&lt;code&gt;simplecov&lt;/code&gt;等项目检测工具，生成 HTML 页面报告。最后检测当前分支，若是 staging 用的分支则部署到 staging 环境，其他分支则跳过。最后将 CI 结果返回给 bitbucket，要注意的是，只要其中某个 stage 出错，就会直接返回 CI 结果。 &lt;/p&gt;

&lt;p&gt;在 Jenkins 后台可以查看生成的 HTML 报告：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-22/html_reports.png" title="" alt="html_reports"&gt;&lt;/p&gt;

&lt;p&gt;在 bitbucket 每个 commit 可以查看 build 结果：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-22/bitbucket_commit.png" title="" alt="bitbucket_commit"&gt;&lt;/p&gt;
&lt;h3 id="项目具体配置"&gt;项目具体配置&lt;/h3&gt;
&lt;p&gt;接下来介绍配置步骤。使用&lt;code&gt;Multibranch Pipeline&lt;/code&gt;类型的项目，这种项目能跟踪整个 repo 的提交。创建成功后，必须配置的只有&lt;code&gt;Branch Sources&lt;/code&gt;，使用 bitbucket 的话需要安装&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/BitBucket+Plugin" rel="nofollow" target="_blank" title=""&gt;Bitbucket Plugin&lt;/a&gt;。配置如图：&lt;/p&gt;

&lt;p&gt;&lt;img src="http://aczero.github.io/assets/2017-03-22/multibranch.png" title="" alt="ci_workflow"&gt;&lt;/p&gt;

&lt;p&gt;需要注意的是，bitbucket 的&lt;code&gt;Scan Credentials&lt;/code&gt;仅支持使用 bitbucket 账号，建议使用一个只有读权限的账号。相应填入&lt;code&gt;owner&lt;/code&gt;和&lt;code&gt;Repoitory Name&lt;/code&gt;信息，&lt;code&gt;Property strategy&lt;/code&gt;可以指定哪些 branch 不执行 ci。最后说一下&lt;code&gt;Auto-register webhook&lt;/code&gt;这个 checkbox，Jenkins 配置完成后，用户还需要在 bitbucket 配置 webhook 通知 Jenkins，这个配置需要项目管理员权限，因此建议不要勾选这个 checkbox 而是手动去配置。文档给出的配置要点为：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL: [JENKINS_ROOT_URL]/bitbucket-scmsource-hook/notify&lt;/li&gt;
&lt;li&gt;Check "Push", "Pull Request Created" and "Pull Request Updated" in the triggers section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;该配置在项目首页 -&amp;gt; Settings -&amp;gt; Webhooks。创建一个 webhook，url 按上面指示填写，Triggers 选择&lt;code&gt;Choose from a full list of triggers&lt;/code&gt;然后按指示勾选即可。&lt;/p&gt;

&lt;p&gt;完成以上步骤后项目配置就完成了，对于其他 SCM，请根据文档配置。&lt;/p&gt;
&lt;h3 id="pipeline脚本"&gt;pipeline 脚本&lt;/h3&gt;
&lt;p&gt;直接贴出 Jenkinsfile，具体解释在脚本的注释部分&lt;/p&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="c1"&gt;// plugins:&lt;/span&gt;
&lt;span class="c1"&gt;// SSH Agent Plugin&lt;/span&gt;
&lt;span class="c1"&gt;// Config File Provider Plugin&lt;/span&gt;
&lt;span class="c1"&gt;// HTML Publisher plugin&lt;/span&gt;

&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;checkout&lt;/span&gt; &lt;span class="n"&gt;scm&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;rails_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;
  &lt;span class="c1"&gt;// 获取项目的ruby版本&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;rubyVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;script:&lt;/span&gt; &lt;span class="s1"&gt;'cat .ruby-version'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;returnStdout:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Preparation'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
    &lt;span class="c1"&gt;// 若提示command not found等错误，检查shell配置:https://issues.jenkins-ci.org/browse/JENKINS-29877&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'source $HOME/.bashrc'&lt;/span&gt;

    &lt;span class="c1"&gt;// 安装对应版本的ruby&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"""if ! rvm ${rubyVersion} do ruby -v &amp;amp;&amp;gt; /dev/null; then
    rvm get stable
    rvm install ${rubyVersion}
    fi"""&lt;/span&gt;

    &lt;span class="c1"&gt;// 安装对应bundler&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;bundlerVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;script:&lt;/span&gt; &lt;span class="s1"&gt;'''rvm all do ruby -e 'puts $&amp;lt;.read[/BUNDLED WITH\\n   (\\S+)$/, 1] || "&amp;lt;1.10"' Gemfile.lock'''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;returnStdout:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do gem install bundler --conservative --no-document -v ${bundlerVersion}"&lt;/span&gt;
    &lt;span class="c1"&gt;// --deployment http://bundler.io/deploying.html#deploying-your-application&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle install --deployment --retry=3"&lt;/span&gt;

    &lt;span class="c1"&gt;// 复制配置文件，使用Config File Provider Plugin&lt;/span&gt;
    &lt;span class="n"&gt;configFileProvider&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="n"&gt;configFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fileId:&lt;/span&gt; &lt;span class="s1"&gt;'cd003fbc-d1ae-496b-b68c-b08f0640a286'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;targetLocation:&lt;/span&gt; &lt;span class="s1"&gt;'config/secrets.yml'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;variable:&lt;/span&gt; &lt;span class="s1"&gt;'SECRET_FILE'&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;configFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fileId:&lt;/span&gt; &lt;span class="s1"&gt;'fff7f49d-254b-478e-ab7c-f4587927cdbb'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;targetLocation:&lt;/span&gt; &lt;span class="s1"&gt;'config/database.yml'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;variable:&lt;/span&gt; &lt;span class="s1"&gt;'DATABASE_FILE'&lt;/span&gt;&lt;span class="o"&gt;)]){&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 重建数据库&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle exec rake db:drop db:create db:migrate RAILS_ENV=${rails_env}"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle exec rake test RAILS_ENV=${rails_env}"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Report'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
    &lt;span class="c1"&gt;// 生成brakeman和rubocop报告，以HTML输出&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle exec brakeman --summary -o brakeman.html"&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle exec rubocop --out rubocop.html || true"&lt;/span&gt;

    &lt;span class="c1"&gt;// 使用HTML Publisher plugin，在项目页面生成HTML展示页面链接。&lt;/span&gt;
    &lt;span class="n"&gt;publishHTML&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nl"&gt;allowMissing:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;alwaysLinkToLastBuild:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;keepAll:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportDir:&lt;/span&gt; &lt;span class="s1"&gt;'./'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportFiles:&lt;/span&gt; &lt;span class="s1"&gt;'brakeman.html'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportName:&lt;/span&gt; &lt;span class="s1"&gt;'Brake Report'&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;publishHTML&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nl"&gt;allowMissing:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;alwaysLinkToLastBuild:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;keepAll:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportDir:&lt;/span&gt; &lt;span class="s1"&gt;'coverage/'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportFiles:&lt;/span&gt; &lt;span class="s1"&gt;'index.html'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportName:&lt;/span&gt; &lt;span class="s1"&gt;'SimpleCov Report'&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;publishHTML&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="nl"&gt;allowMissing:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;alwaysLinkToLastBuild:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;keepAll:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportDir:&lt;/span&gt; &lt;span class="s1"&gt;'./'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportFiles:&lt;/span&gt; &lt;span class="s1"&gt;'rubocop.html'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;reportName:&lt;/span&gt; &lt;span class="s1"&gt;'Rubocop Report'&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BRANCH_NAME&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s1"&gt;'master'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Deploy'&lt;/span&gt;&lt;span class="o"&gt;){&lt;/span&gt;
      &lt;span class="c1"&gt;// 使用SSH Agent Plugin提供ssh key，用capistrano进行staging环境的部署&lt;/span&gt;
      &lt;span class="n"&gt;sshagent&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'b33c1c57-737d-4195-a6f5-446a9d000f2a'&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"rvm ${rubyVersion} do bundle exec cap staging deploy"&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'not on master, skip deploy'&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Preparation&lt;/code&gt;和&lt;code&gt;Test&lt;/code&gt;部分在之前的 pipeline 介绍中已经解释过。&lt;code&gt;Report&lt;/code&gt;部分可以使用一些项目检测工具如&lt;a href="https://github.com/bbatsov/rubocop" rel="nofollow" target="_blank" title=""&gt;Rubocop&lt;/a&gt;，&lt;a href="https://github.com/colszowka/simplecov" rel="nofollow" target="_blank" title=""&gt;Simplecov&lt;/a&gt;和&lt;a href="https://github.com/presidentbeef/brakeman" rel="nofollow" target="_blank" title=""&gt;Brakeman&lt;/a&gt;等输出 html 页面。在 pipeline 中没有去生成 simplecov 的报告，因为在测试的阶段就会生成。然后使用&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/HTML+Publisher+Plugin" rel="nofollow" target="_blank" title=""&gt;HTML Publisher plugin&lt;/a&gt;发布页面。&lt;/p&gt;
&lt;h3 id="注意事项"&gt;注意事项&lt;/h3&gt;
&lt;p&gt;使用 HTML Publisher plugin 需要修改 Jenkins 默认的&lt;a href="https://en.wikipedia.org/wiki/Content_Security_Policy" rel="nofollow" target="_blank" title=""&gt;CSP&lt;/a&gt;配置 (会产生安全问题，在不了解的情况下不建议修改)，否则发布出来的页面的 css 和 js 都会被禁用。修改的方法查看&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/Configuring+Content+Security+Policy" rel="nofollow" target="_blank" title=""&gt;官方文档&lt;/a&gt;，具体如何配置要根据发布的页面对应设置。设置可以在 Manage Jenkins -&amp;gt; Script Console 中执行脚本来设置，但在 Jenkins 重启后将会重新从配置文件加载，所以想保留修改的话要在&lt;code&gt;/etc/default/jenkins&lt;/code&gt;中作为参数传递。像这样修改 JAVA_ARGS 参数：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JAVA_ARGS="-Djava.awt.headless=true -Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; img-src 'self'\""
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;看到这里，我想你应该已经掌握了 Jenkins 的使用方法。使用下来个人感觉 Jenkins 是一个很完善的自动化工具，你可以用 Jenkins 来实现 CI,CD 等工作流。Jenkins 还有非常多的插件，因此可以快速实现你的工作流，对于 Github，Bitbucket 都有比较完善的支持。Pipeline 的引入又提供了极大的自由度，折腾起来还是非常有意思的。但 Jenkins 有一部分插件的文档不完善，而官方的文档也是略为简略，所以坑还是有不少的。对于 Jenkins，我也只有配置简单工作流的经验，欢迎指出文章中的不足之处，也欢迎一起交流下使用心得。&lt;/p&gt;
&lt;h2 id="参考链接"&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mattbrictson.com/rails-continuous-integration" rel="nofollow" target="_blank"&gt;https://mattbrictson.com/rails-continuous-integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jenkins.io/doc/" rel="nofollow" target="_blank"&gt;https://jenkins.io/doc/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mattbrictson.com/rails-continuous-integration" rel="nofollow" target="_blank"&gt;https://mattbrictson.com/rails-continuous-integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.jenkins-ci.org/display/JENKINS/Configuring+Content+Security+Policy" rel="nofollow" target="_blank"&gt;https://wiki.jenkins-ci.org/display/JENKINS/Configuring+Content+Security+Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://go.cloudbees.com/docs/cloudbees-documentation/cje-user-guide/index.html" rel="nofollow" target="_blank"&gt;https://go.cloudbees.com/docs/cloudbees-documentation/cje-user-guide/index.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>ACzero</author>
      <pubDate>Wed, 22 Mar 2017 21:53:49 +0800</pubDate>
      <link>https://ruby-china.org/topics/32610</link>
      <guid>https://ruby-china.org/topics/32610</guid>
    </item>
  </channel>
</rss>
