<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>测试 node of Ruby China Forum</title>
    <link>https://ruby-china.org/</link>
    <description>Recent Topic in 测试 of Ruby China Forum.</description>
    <item>
      <title>如何进行测试驱动开发</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;前言&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;​最近在读《匠艺整洁之道 - 程序员的职业修养》这本书，作者鲍勃大叔开篇就用了大量的示例来讨论与演示为什么需要和如何操作测试驱动开发。我是之前写过测试，但从不知道测试如何驱动开发，大部分情况下也只是先写生产代码，写好后再测试，看看是否能调通，再修改代码中隐藏的问题。&lt;/p&gt;

&lt;p&gt;而测试驱动开发会扭转这种思维方式，是先写测试，再写对应的生产代码。这一开始让人会觉得很奇怪，感觉并且很麻烦，但当你尝试一下，就会发现这事儿挺有趣，且神奇。&lt;/p&gt;

&lt;p&gt;温馨提示：&lt;a href="https://learning.oreilly.com/videos/clean-craftsmanship-disciplines/9780137676385/9780137676385-CCC1_clean_craftsmanship_stack/" rel="nofollow" target="_blank" title=""&gt;Clean Craftsmanship: Disciplines, Standards, and Ethics (Companion Videos)&lt;/a&gt;，这个是该书示例操作的视频说明，&lt;strong&gt;非常非常非常有用！！！&lt;/strong&gt; 第一篇是讲述如何通过测试驱动开发来写一个栈结构。我严重怀疑大叔是不是一边写代码一边喝啤酒。整个过程伴随大叔魔性的笑声和宛如魔法般的操作，就好像他在不停调戏编译器。你会通过这个视频感受到如何从零开始，进行测试驱动开发。
​
下面将讲讲一些基础知识。&lt;/p&gt;
&lt;h2 id="TDD纪律"&gt;TDD 纪律&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建测试集，方便重构，并且其可信程度达到系统可部署的水平，也就是说测试集通过，系统就可以部署。

&lt;ol&gt;
&lt;li&gt;我不知道有多少人像我一样，由于老系统没有测试，导致每次上线都很害怕，担心修改引入新的问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;创建足够解耦，可测试，可重构的代码&lt;/li&gt;
&lt;li&gt;创建极短的反馈循环周期&lt;/li&gt;
&lt;li&gt;创建相互解耦的测试代码和生产代码&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面的纪律需要下面展示的法则作为基础，如果没有一些技巧和知识就很难遵守这些法则。&lt;/p&gt;
&lt;h2 id="TDD三法则"&gt;TDD 三法则&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;第一法则&lt;/strong&gt;：在编写因为缺少生产代码而必然会失败的测试之前，绝不编写生产代码
&lt;strong&gt;第二法则&lt;/strong&gt;：只写刚好导致失败或者通不过编译的测试，编写生产代码来解决失败的问题
&lt;strong&gt;第三法则&lt;/strong&gt;:  只写刚好能解决当前测试失败问题的生产代码。测试通过后，立即写其他测试代码
整个过程看起来好像是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;写一行测试代码，无法通过编译&lt;/li&gt;
&lt;li&gt;写一行生产代码，编译成功&lt;/li&gt;
&lt;li&gt;写另一行测试代码，无法通过编译&lt;/li&gt;
&lt;li&gt;再写生产代码，编译成功&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;blockquote&gt;
&lt;p&gt;遵守上述法则后，有以下好处&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;提高效率，减少调试的时间&lt;/li&gt;
&lt;li&gt;将产出一套低层次文档&lt;/li&gt;
&lt;li&gt;好玩&lt;/li&gt;
&lt;li&gt;将产出一套测试集，让你有信息部署系统&lt;/li&gt;
&lt;li&gt;将创建较少耦合的代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="简单示例"&gt;简单示例&lt;/h2&gt;&lt;h3 id="栈"&gt;栈&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这里建议先看我上面提供的链接，作者是用 Java 写的，我这里用 Ruby(核心思想是一致的)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'minitest/autorun'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StackTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_nothing&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;/p&gt;

&lt;hr&gt;

&lt;p&gt;规则 1：先编写测试，逼着自己写将要写的代码&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;​ 我们知道我们需要一个栈&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stack_test.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'minitest/autorun'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StackTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_canCreateStack&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&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;Stack&lt;/code&gt;这个类结构，所以我们写两行生产代码来通过测试&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stack.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 这次我们在&lt;code&gt;stack_test.rb&lt;/code&gt;引入一下，就可以通过测试了。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;规则 2：让测试失败，让测试通过，清理代码&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;​ 这时候我们发现我们还没有断言行为呢，比如当刚创建一个栈时，这个栈应该是空的。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stack_test.rb&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'minitest/autorun'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'stack'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StackTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_nothing&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;test_canCreateStack&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty?&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="c1"&gt;# =&amp;gt; 报错：NoMethodError: undefined method `isEmpty?'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 我们迅速补上生产代码，来解决这个问题&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stack.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isEmpty?&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 这样改后，方法可以找到了，但断言失败了，这里我们是故意的，为什么这么做呢？第一法则：测试必须失败，为啥呢？因为当测试应该失败时，我们就能看到它失败，我们测试了自己的测试。当我们将上面的方法返回 true 时，就能测试另一半了。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stack.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isEmpty?&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 这时候执行测试，测试通过，万事大吉，你一定会骂这不是作弊吗，但等等，至少我们只用几秒就能测出该通过时通过，该失败时失败。&lt;/p&gt;

&lt;p&gt;​ 下一个要测的是，栈需要能 push 吧&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stack_test.rb&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'minitest/autorun'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'stack'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StackTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_canPush&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stack.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isEmpty?&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;def&lt;/span&gt; &lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ele&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;​ 执行测试，测试通过，但这里我们没有断言啊，那既然 push 了，栈就不应为空吧&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;'minitest/autorun'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'stack'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StackTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_canPush&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;refute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty?&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;​ 执行测试，断言失败，因为我们返回的一直是 true，那改改代码吧&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;Stack&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:empty&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&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;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isEmpty?&lt;/span&gt;
    &lt;span class="n"&gt;empty&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;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&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;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 我们抽离出一个实例变量来保存是否为空，并在 push 后直接暴力设置为 false。这时候测试又能通过了
我们发现没写一个测试方法，都要创建一个栈，太麻烦了，于是我们重构一下，使用&lt;code&gt;setup方法&lt;/code&gt;&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;'minitest/autorun'&lt;/span&gt;
&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'stack'&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StackTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;
    &lt;span class="vi"&gt;@stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&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;test_nothing&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;test_canCreateStack&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack.isEmpty&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;def&lt;/span&gt; &lt;span class="nf"&gt;test_canPush&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;refute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack.isEmpty&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;​ 测试依然能通过，不过 canPush 这个测试名不太好，我们改改（test_操作_结果）&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_afterOnePush_isEmpty&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;refute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack.isEmpty&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 当然测试还是能通过&lt;/p&gt;

&lt;p&gt;​ 这时候我们测试，栈 push 一次，也能 pop 一次吧，并且这时候栈应该为空&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_afterOnePushAndOnePop_isEmpty&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.pop&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack.isEmpty&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;Stack&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pop&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&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;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 现在测试又通过了，现在我们测试两次 push 之后，栈的尺寸应该是 2&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_afterTwoPushs_sizeIsTwo&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&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="vi"&gt;@stack.size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&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;Stack&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ss"&gt;:size&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="vi"&gt;@size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;isEmpty?&lt;/span&gt;
    &lt;span class="n"&gt;empty&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;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@size&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&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;def&lt;/span&gt; &lt;span class="nf"&gt;pop&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&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;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 我们定义 size 实例变量来保存状态，并在每次 push 时，size+1，这样断言通过。为了测试完整，我们再加一个测试&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_afterOnePush_isNotEmpty&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;refute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@stack.isEmpty&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
    &lt;span class="n"&gt;assert_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="vi"&gt;@stack.size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 测试通过。回到第一法则，如果对空栈执行 pop 操作，应该会有个异常吧&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_poppingEmptyStack_raisesUnderflow&lt;/span&gt;
    &lt;span class="n"&gt;assert_raises&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="vi"&gt;@stack.pop&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;/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;Underflow&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;StandardError&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 断言失败，我们再修改生产代码&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'underflow'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pop&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Underflow&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isEmpty?&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&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;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 测试通过，再测试：当栈 push 一个数据时，也应该 pop 出相同的数据吧&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_afterPushingX_willPopX&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.push&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;assert_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="vi"&gt;@stack.pop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 测试失败，调整生产代码，我们加一个实例变量保存信息&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'underflow'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ss"&gt;:size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ss"&gt;:element&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="vi"&gt;@size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;isEmpty?&lt;/span&gt;
    &lt;span class="n"&gt;empty&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;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@size&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="vi"&gt;@element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ele&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;pop&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Underflow&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isEmpty?&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="vi"&gt;@element&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;/p&gt;

&lt;hr&gt;

&lt;p&gt;规则 3：别挖金子&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;​ 在最开始尝试 TDD 时，你会急于解决较难或有趣的问题，你可能先写 FILO 行为，这个就是挖金子，我们有意避免测试与栈行为有关的东西，专注与周边行为。例如栈是否为空或栈大小。
为什么避免挖金子，因为如果过早挖金子，可能忽略周边所有细节。现在我们根据第一法子，编写 FILO 测试&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_afterPushingXAndY_willPopYThenX&lt;/span&gt;
    &lt;span class="vi"&gt;@stack.push&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="vi"&gt;@stack.push&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="n"&gt;assert_equal&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="vi"&gt;@stack.pop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert_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="vi"&gt;@stack.pop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​ 测试失败，我们发现抛出了&lt;code&gt;Underflow&lt;/code&gt;,我们修改一下&lt;code&gt;isEmpty?&lt;/code&gt;方法&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'underflow'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;isEmpty?&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&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;test_afterOnePushAndOnePop_isEmpty&lt;/code&gt;这个测试中报错，好吧，我们在 pop 时没有 size-1&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'underflow'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pop&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Underflow&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isEmpty?&lt;/span&gt;
    &lt;span class="vi"&gt;@size&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="vi"&gt;@element&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;​ 就剩下最后一个断言失败了，FILO 行为，我们开始修改&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'underflow'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ss"&gt;:size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ss"&gt;:elements&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="vi"&gt;@size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="vi"&gt;@elements&lt;/span&gt; &lt;span class="o"&gt;=&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;def&lt;/span&gt; &lt;span class="nf"&gt;isEmpty?&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&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;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@size&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="vi"&gt;@elements&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ele&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;pop&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Underflow&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isEmpty?&lt;/span&gt;
    &lt;span class="n"&gt;ele&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="vi"&gt;@size&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="vi"&gt;@empty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;ele&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;/p&gt;

&lt;p&gt;​ 也许你可以花点儿时间，利用测试驱动开发自己写一个队列结构。 &lt;/p&gt;</description>
      <author>qinsicheng</author>
      <pubDate>Sun, 10 Sep 2023 16:56:11 +0800</pubDate>
      <link>https://ruby-china.org/topics/43323</link>
      <guid>https://ruby-china.org/topics/43323</guid>
    </item>
    <item>
      <title>请问怎么让 CircleCI 跑子目录的测试代码？</title>
      <description>&lt;p&gt;&lt;a href="https://github.com/CircleCI-Public/circleci-demo-ruby-rails/blob/master/.circleci/config.yml" rel="nofollow" target="_blank" title=""&gt;CircleCI&lt;/a&gt; 是一个分布式的跑测试的云产品，本来一个小时的测试，它拆成 80 份，几分钟就可以跑完。此外它和 Ruby on Rails 的集成非常方便，官方还提供了实例代码。只要在代码的根目录，添加一个 &lt;code&gt;.circleci/config.yml&lt;/code&gt; 文件就可以了。每次有人创建 PR，会自动触发 CI。&lt;/p&gt;

&lt;p&gt;但是我最近遇到了一个问题，我的新项目是前后端分离的，前端一个文件夹，后端一个文件夹。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;后端是 RoR，用来做 API&lt;/li&gt;
&lt;li&gt;前端 react&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有没有人设置过 CircleCI 去跑两个文件夹中的测试吗？我折腾了半天，还是没搞定。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/xiaoronglv/9f85bf8f-36da-45c8-85ed-8ea73ddfff4d.png!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>xiaoronglv</author>
      <pubDate>Thu, 10 Mar 2022 15:30:12 +0800</pubDate>
      <link>https://ruby-china.org/topics/42198</link>
      <guid>https://ruby-china.org/topics/42198</guid>
    </item>
    <item>
      <title>【招聘】广州 测试开发工程师（风控）年薪:30-40 万</title>
      <description>&lt;p&gt;岗位职责：&lt;/p&gt;

&lt;p&gt;1.负责指定业务线的质量保障工作，包括功能测试、自动化测试、性能测试等全方位、全生命周期的质量活动;&lt;/p&gt;

&lt;p&gt;2.深入理解和分析业务显性、隐性需求，制定合理的质量策略，充分识别过程中的风险，确保产品安全发布;&lt;/p&gt;

&lt;p&gt;3.推动业务线内的质量改进，提升团队质量意识，帮助开发一起完善业务监控，执行故障演练;&lt;/p&gt;

&lt;p&gt;4.负责自动化测试框架的搭建以及相关测试效率提升工具的开发;&lt;/p&gt;

&lt;p&gt;5.负责平台化的整体框架和开发;&lt;/p&gt;

&lt;p&gt;6.负责参与持续集成整体开发流程的评审和设计工作。&lt;/p&gt;

&lt;p&gt;任职资格：&lt;/p&gt;

&lt;p&gt;1.计算机相关专业大专统招及以上学历;&lt;/p&gt;

&lt;p&gt;2.至少三年以上的软件开发、自动化测试或白盒测试的工作经验;&lt;/p&gt;

&lt;p&gt;3.精通 C/C++/python/java 等至少一种编程语言，熟悉 Linux 操作系统，熟悉数据库操作;&lt;/p&gt;

&lt;p&gt;4.良好的测试分析和设计测试方法能力，能解决复杂的质量问题;&lt;/p&gt;

&lt;p&gt;5.较强的学习能力，善于团队合作，理解和适应变化，能主动思考总结，自我驱动;&lt;/p&gt;

&lt;p&gt;6.有电商系统的测试经验者优先。&lt;/p&gt;</description>
      <author>Don</author>
      <pubDate>Tue, 14 Dec 2021 17:37:44 +0800</pubDate>
      <link>https://ruby-china.org/topics/41984</link>
      <guid>https://ruby-china.org/topics/41984</guid>
    </item>
    <item>
      <title>RSpec XPath 问题</title>
      <description>&lt;p&gt;很奇怪的问题，测试页面是否有 head，竟然测不通！&lt;img title=":joy:" alt="😂" src="https://twemoji.ruby-china.com/2/svg/1f602.svg" class="twemoji"&gt; &lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&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_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/html/head"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报错：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Failure/Error: expect(page).to have_selector(:xpath, "/html/head")
       expected to find visible xpath "/html/head" but there were no matches. Also found "", which matched the selector but not all filters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;难道我的页面没有 head? 可是我检查过，响应很正常，head 和 body 都是有的。&lt;/p&gt;

&lt;p&gt;body 和 title 是可以测通的。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&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_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/html/body"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&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_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@home.title&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;' - '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app_name'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>axiwei</author>
      <pubDate>Tue, 29 Dec 2020 12:53:34 +0800</pubDate>
      <link>https://ruby-china.org/topics/40758</link>
      <guid>https://ruby-china.org/topics/40758</guid>
    </item>
    <item>
      <title>js.erb 测试问题</title>
      <description>&lt;h3 id="Controller:"&gt;Controller:&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;render template: "replies/return_to"
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="views:"&gt;views:&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/views/replies/return_to.js.erb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开发和产品模式跑都是正常通过，但是在写 Controller 测试是报错&lt;/p&gt;
&lt;h3 id="错误消息"&gt;错误消息&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"#&amp;lt;ActionView::MissingTemplate: Missing template replies/return_to with {:locale=&amp;gt;[:\"zh-CN\"], :formats=&amp;gt;[:html], :variants=&amp;gt;[], :handlers=&amp;gt;[:raw, :erb, :html, :builder, :ruby, :jbuilder]}. Searched in:\n  * \"/app/app/views\"\n  * \"/usr/local/bundle/gems/kaminari-core-1.2.1/app/views\"\n  * \"/usr/local/bundle/gems/actiontext-6.1.0/app/views\"\n  * \"/usr/local/bundle/gems/actionmailbox-6.1.0/app/views\"\n&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>zhugexinxin</author>
      <pubDate>Sun, 27 Dec 2020 23:34:02 +0800</pubDate>
      <link>https://ruby-china.org/topics/40754</link>
      <guid>https://ruby-china.org/topics/40754</guid>
    </item>
    <item>
      <title>测试驱动开发在项目中的实践</title>
      <description>&lt;p&gt;好久没有动笔写文章了，今天来写点什么。这篇文章主要简单谈谈最近把测试驱动开发应用在公司项目中的心得体会。原文链接： &lt;a href="https://www.lanzhiheng.com/posts/tdd-in-our-project" rel="nofollow" target="_blank"&gt;https://www.lanzhiheng.com/posts/tdd-in-our-project&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;好久没有动笔写文章了，今天来写点什么。这篇文章主要简单谈谈最近把测试驱动开发应用在公司项目中的心得体会。虽说主要技术栈是 Ruby 一系，但相信对其他的领域也有一定的参考价值吧。&lt;/p&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;近期一直都在给公司的新产品添砖加瓦，眼看着第一版即将发布，也稍微能喘口气写点儿什么。第一次独自用 Ruby On Rails 编写项目代码，内心多少有一点忐忑。与在豆厂时期不同，周边没有许多已成规范的东西，技术选型上也没那么多理所当然，这一切得从 0 开始。&lt;/p&gt;

&lt;p&gt;这其中各有各的好，有大量前辈的经验可以借鉴，以及可复用的代码，在某种程度上能够让自己少走些弯路，降低技术决策的成本。但如果没能站在巨人的肩膀上，那就只能自己去做些尝试，看看什么才是适合自己当前项目的，自由度会稍微高一些。多少让我想起《解忧杂货店》中的剧情，&lt;strong&gt;一张白纸，也意味着可以随意涂鸦&lt;/strong&gt;。为了保证代码质量，也降低组员们介入测试时的工作量，我决定给项目引入测试驱动的开发方式。&lt;/p&gt;

&lt;p&gt;个把月过去，不知不觉已经累计了大概有 500 多个测试用例，总体效果还是比较满意的：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://lanzhiheng.s3-ca-central-1.amazonaws.com/500-test.png" title="" alt="500-test.png"&gt;&lt;/p&gt;
&lt;h2 id="如何写测试"&gt;如何写测试&lt;/h2&gt;
&lt;p&gt;编写测试大概有以下两个比较极端的场景：&lt;/p&gt;
&lt;h2 id="1. 自顶向下"&gt;1. 自顶向下&lt;/h2&gt;
&lt;p&gt;从开发者的层面指的其实就是端对端的测试。我们可以通过代码来模拟浏览器的行为，借此测试已有的业务逻辑。这种方式的好处在于能够模拟真实用户，一些繁琐的点击操作完全由机器来取代，降低日后回归测试的成本。然而这类测试往往编写难度较大，“杀鸡用牛刀” - 大概就是这种感觉。想象一下假设你要用这种方法来测试接口，那么你可能需要：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;（用代码的方式）打开浏览器 --&amp;gt; 在浏览器输入接口的URL --&amp;gt; 点击确认 --&amp;gt; 等待页面加载 --&amp;gt; 检查页面内容（或者返回的数据）是否符合预期
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到这里也许有人会问：“等等，为什么我测试个接口要打开浏览器？”。对啊，&lt;strong&gt;为什么我们要打开浏览器？&lt;/strong&gt;简单点不好吗？&lt;/p&gt;
&lt;h2 id="2. 自底向上"&gt;2. 自底向上&lt;/h2&gt;
&lt;p&gt;直接从终端视角来编写测试“性价比”不高，很多时候你的网站还没活到能见到“终端”的阶段就已经夭折，而在这之前测试几乎不能给开发过程带来任何收益。或许可以换个角度，从最底层写起。也就是我们常说的单元测试。这种测试编写起来十分简便，熟练编写不仅能加快开发的速度，还能保证交付代码的质量。不过许多人刚开始写这种测试很容易就会迷失，因为自己也抓不准哪一个模块才是最有测试价值的，于是他们就想朝着测试覆盖率百分百的目标前进。最终的结果就是身心俱疲。&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;A&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;end_method&lt;/span&gt;
    &lt;span class="n"&gt;return_a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;return_b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;return_c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;return_d&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;return_a&lt;/span&gt;
    &lt;span class="s1"&gt;'a'&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;return_b&lt;/span&gt;
    &lt;span class="s1"&gt;'b'&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;return_c&lt;/span&gt;
    &lt;span class="s1"&gt;'c'&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;return_d&lt;/span&gt;
    &lt;span class="s1"&gt;'d'&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;/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="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'method'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;A&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;it&lt;/span&gt; &lt;span class="s1"&gt;'test size private methods'&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="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:return_a&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;'a'&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;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:return_b&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;'b'&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;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:return_c&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;'c'&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;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:return_d&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;'d'&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;'test public method'&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="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_method&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="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;/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="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="s1"&gt;'method'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;A&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;it&lt;/span&gt; &lt;span class="s1"&gt;'test public method'&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="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_method&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="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;把公有的方法测试好足矣，有些时候甚至公有方法都不需要写测试（尺度还需要开发者自己把控）。测试覆盖率 100% 除了能够带来围观群众们的惊叹，并不能带来任何实质的收益。相反，不必要的测试太多，还会带来更高的维护成本，试想如果你给一个&lt;strong&gt;简单的&lt;/strong&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;日常运营所需的后台管理系统。&lt;/li&gt;
&lt;li&gt;小程序应用所需要的接口。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;技术栈为 Ruby On Rails。以下方略大多以 Ruby 为背景，不过大体策略应该都是通用的。&lt;/p&gt;
&lt;h2 id="1. 有选择地去测试"&gt;1. 有选择地去测试&lt;/h2&gt;
&lt;p&gt;当一个项目日趋庞大之后，构建项目所需要的类会慢慢增多，相应的，他们所包含的方法也越来越多。如果不加分辨地对所有方法加以测试覆盖，那么不但会给自己造成心理负担，还会占用掉编写更有价值测试的时间。&lt;strong&gt;减少对不必要测试的编写，也就是为那些真正重要的事情腾出时间。&lt;/strong&gt;从长远来看那个执行复杂运算的方法，出错的几率更大，带来的损失可能也更直接。反之，像下面这种测试又有多大必要呢？&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;return_true&lt;/span&gt;
  &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&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="k"&gt;do&lt;/span&gt;
 &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'test 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="n"&gt;return_true&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="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="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;/p&gt;

&lt;p&gt;如果在许多场景下公有方法都能够得到正确的结果，那么服务于他的私有方法一般也不会错哪里去。哪怕真的错了，到时候再给那个出错的方法补测试即可，保证下次不犯同样的错误。也就是我接下来要说的，错误导向测试。&lt;/p&gt;
&lt;h2 id="2. 错误导向测试"&gt;2. 错误导向测试&lt;/h2&gt;
&lt;p&gt;第一次写测试，很容易会陷入到“不知道应该测试什么的窘境”。接下来，为了让自己看起来很充实，我们会不加分辨地给所有方法加上单元测试，以求测试覆盖率 100%，这就是所谓的&lt;strong&gt;“战略上的懒惰，战术上的勤奋”&lt;/strong&gt;吧？如果你是在一个通过代码行数来定工资的公司工作，这种做法无疑是明智的。然而如果项目在快速迭中，公司吃了上顿不知道有没有下顿，那咱们还是别这样做的好。&lt;/p&gt;

&lt;p&gt;我推荐错误导向的测试。也就是说对一些自己有把握的方法，在一开始可以不用给他们加测试。然而这个方法一旦暴雷，那么修 bug 的时候就要给它加上测试，避免它下一次再暴雷（可参考《程序员修炼之道》一书对测试的阐述）。笔者也用这种策略补了很多测试代码。比如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;前端跟我说接口漏了几个字段？马上编写测试保证接口会返回相应的字段。&lt;/li&gt;
&lt;li&gt;接口接收参数的时候没有做空判断，导致传入空值的时候会报 500 的异常？立马编写参数为空值的测试用例，然后修改代码让这个测试通过。&lt;/li&gt;
&lt;li&gt;数据库出现了跟预期不一致的数据？在测试中构造这种奇葩的数据，尝试入库，预期入库失败。然后调整校验代码，让测试通过。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这种针对系统薄弱的地方来编写测试的策略，编写起来更省力，所带来的效益也更高，最起码能做到孔子说的“不贰过”吧？同样，那些选择了测试后行的同学我也建议采用这种方式去补测试，这样更有针对性，毕竟每天都有 bug 要修，修的 bug 多了，有价值的测试也越来越多。&lt;/p&gt;
&lt;h2 id="3. 尽可能少针对页面布局写测试"&gt;3. 尽可能少针对页面布局写测试&lt;/h2&gt;
&lt;p&gt;相对于后端的业务逻辑，其实前端的页面结构更改的的几率会大很多。假设你针对一个页面写了大量的断言（&lt;a href="https://github.com/kucaahbe/rspec-html-matchers" rel="nofollow" target="_blank" title=""&gt;rspec-html-matchers&lt;/a&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="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="s1"&gt;'&amp;lt;p class="qwe rty" id="qwerty"&amp;gt;Paragraph&amp;lt;/p&amp;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;have_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'p'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:with&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'qwe rty'&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="s1"&gt;'&amp;lt;p class="qwe rty" id="qwerty"&amp;gt;Paragraph&amp;lt;/p&amp;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;have_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'p'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:with&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'rty qwe'&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="s1"&gt;'&amp;lt;p class="qwe rty" id="qwerty"&amp;gt;Paragraph&amp;lt;/p&amp;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;have_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'p'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:with&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rty'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'qwe'&lt;/span&gt;&lt;span class="p"&gt;]&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="s1"&gt;'&amp;lt;p class="qwe rty" id="qwerty"&amp;gt;Paragraph&amp;lt;/p&amp;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;have_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'p'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:with&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'qwe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rty'&lt;/span&gt;&lt;span class="p"&gt;]&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;rspec-html-matchers&lt;/code&gt;的维护者用这些代码来做例子可能只是为了展示这个库的能力，应该不会希望使用者在项目里面这样去写测试。不然的话要是刚好要改个类名，我得调整多少测试？这种代码在笔者看来也是不健壮的。想想我要删掉一个按钮都要去改一下测试是多难受的事情。&lt;/p&gt;

&lt;p&gt;页面布局，按钮有无这种场景，除非自己真的很闲，否则就别给他们写测试了。多一个少一个按钮对系统来说并不会造成太大的影响，要去维护这堆测试却特别累人。还不如参考 2 的建议，等到出了真正的 bug 的时候再针对他们来写一个测试？比如在订单的某个状态下应该显示 XX 按钮，这次我漏了，那么修复 bug 的时候就给这种场景加上测试，保证下次不出错即可。&lt;/p&gt;
&lt;h2 id="4. 给所有接口都加上测试用例"&gt;4. 给所有接口都加上测试用例&lt;/h2&gt;
&lt;p&gt;接口的种类有很多，但目前最基础的无非就是 CRUD。我们要么就是通过接口来获取后台的信息，要么就是通过接口来修改后台的数据。那么接口的测试过程或许可以简化成：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;组装请求链接。&lt;/li&gt;
&lt;li&gt;利用客户端发送相关的请求。&lt;/li&gt;
&lt;li&gt;检查返回的状态码，如果有必要再检查返回的数据正确性。&lt;/li&gt;
&lt;li&gt;检查请求所造成的副作用。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;对接口的测试，我采用的框架是&lt;a href="https://github.com/rswag/rswag" rel="nofollow" target="_blank" title=""&gt;rswag&lt;/a&gt;。它很好的贯彻了，&lt;strong&gt;测试即文档&lt;/strong&gt;这一理念。用它来写测试还能顺便生成 Swagger 接口文档，真的不能更棒了。比如我要测试获取博客详情数据的接口，那么可以这样去写&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;'Blogs API'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="s1"&gt;'/blogs/{id}'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'Retrieves a blog'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="s1"&gt;'Blogs'&lt;/span&gt;
      &lt;span class="n"&gt;produces&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;
      &lt;span class="n"&gt;parameter&lt;/span&gt; &lt;span class="ss"&gt;name: :id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;in: :path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :string&lt;/span&gt;

      &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="s1"&gt;'200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'blog found'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Blog&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;title: &lt;/span&gt;&lt;span class="s1"&gt;'foo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="s1"&gt;'bar'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;run_test!&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="c1"&gt;# Check the field of result&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;response&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="s1"&gt;' do
        let(:id) { '&lt;/span&gt;&lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;run_test!&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;/blogs/{id}&lt;/code&gt;这个模式的 URL，发送 GET 请求的场景。可以在请求发送之前通过&lt;code&gt;RSpec&lt;/code&gt;的语法设置资源的&lt;code&gt;id&lt;/code&gt;，当运行&lt;code&gt;run_test!&lt;/code&gt;的时候框架会自动帮你发送请求。如上面所期待的，我们期望找到资源的时候得到状态码&lt;code&gt;200&lt;/code&gt;，而找不到资源的时候接收&lt;code&gt;404&lt;/code&gt;状态码。&lt;/p&gt;

&lt;p&gt;如果需要进一步检测请求的响应情况，则可以在&lt;code&gt;run_test!&lt;/code&gt;后面加上代码块，并解构&lt;code&gt;response&lt;/code&gt;。这种测试的组装方式一开始看起来是很奇葩，我第一感觉就是嵌套层数太多了。后来写习惯了就好，也慢慢理解它为何这样做。不过这个框架给我的感觉就是文档用例有点少，不少资源/参数的定义方式都要去参考&lt;a href="https://swagger.io/specification/" rel="nofollow" target="_blank" title=""&gt;Swagger&lt;/a&gt;。这些大家用到的时候再慢慢去探索吧。&lt;/p&gt;

&lt;p&gt;目前我用这种方式给我司的项目提供了大概 55 个接口，bug 率比想象中少。而且关键的是，在开发接口的过程中，几乎没有打开过浏览器，都是直接写测试，写业务，跑测试。不得不说其实工作效率要比开浏览器逐个场景去校验高效得多。&lt;/p&gt;
&lt;h2 id="5. 测试最好不要后行"&gt;5. 测试最好不要后行&lt;/h2&gt;
&lt;p&gt;我们经常会说：“等有空再去补测试吧。”然而，多年的工作经验告诉我，补测试这个事情，其实是下下策。下面对比以下两个流程&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;测试先行：想想怎么写测试 -&amp;gt; 写测试 -&amp;gt; 写代码 -&amp;gt; 跑测试 -&amp;gt; 完工
测试后行：研究已有代码 -&amp;gt; 想想怎么写测试 -&amp;gt; 写测试 -&amp;gt; 跑测试 -&amp;gt; 调整测试以适应代码 -&amp;gt; 完工&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;研究已有的代码，意味着你要重新去审阅过去的代码，不管这段代码是不是自己写的，要重新理解并补上测试都是需要时间的。而且往往如果测试不通过，我们并不敢第一时间去改代码，因为我们害怕弄坏原来已经跑着正常的业务代码，多数情况下就是不断改测试，来“适应”已有的代码。不折腾几番之后根本就不知道，到底是原来的代码有问题，还是自己写的测试有问题。然而这些都是测试后行的时间成本。&lt;/p&gt;

&lt;p&gt;个人感觉，除非是客户要求，或者钱给够，否则测试后行真的很难。而且过程极其痛苦。如果你真的有写测试的打算，还不如一开始就把用例给加上。&lt;/p&gt;
&lt;h2 id="推荐技术"&gt;推荐技术&lt;/h2&gt;&lt;h2 id="1. 数据构造"&gt;1. 数据构造&lt;/h2&gt;
&lt;p&gt;一开始项目比较简单的时候我是直接采用了 Rails 自带的&lt;code&gt;fixture&lt;/code&gt;功能，也就是测试开始之前先把必要的数据入库，然后依赖这些数据来写测试。但后面发现，这种方式虽然测试运行的速度很可观，但是依赖性太强，写起来不是特别方便。笔者更倾向于跑测试的时候针对特定用例去构造数据，个人推荐用&lt;a href="https://github.com/thoughtbot/factory_bot" rel="nofollow" target="_blank" title=""&gt;factory_bot&lt;/a&gt;。虽然这样测试运行起来要慢一些，但是写起来就很舒服了（似乎有点像动态语言跟静态语言之争）。这一个月大概写了 550 个测试用例，全面跑完花费时间大概是 5-7 分钟，还有很大的优化空间。&lt;/p&gt;
&lt;h2 id="2. 测试框架"&gt;2. 测试框架&lt;/h2&gt;
&lt;p&gt;单纯地测试 API 的话其实 Rails 自带的测试套件就能够满足要求。本质上我们只需要&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;组装 api&lt;/li&gt;
&lt;li&gt;发送请求&lt;/li&gt;
&lt;li&gt;检测响应状态码，还有响应体。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;不需要太多花里胡哨的功能，我之所以后面用&lt;a href="https://rspec.info/" rel="nofollow" target="_blank" title=""&gt;RSpec&lt;/a&gt;，主要是因为我还要给前端小伙子提供 Swagger 的 API 文档，而刚好它的仓库&lt;a href="https://github.com/rswag/rswag" rel="nofollow" target="_blank" title=""&gt;rswag&lt;/a&gt;又是依赖了 RSpec，所以就干脆整个项目都换成 RSpec 了。然而它并非什么必需品，RubyChina 的&lt;a href="http://homeland.ruby-china.org/" rel="nofollow" target="_blank" title=""&gt;Homeland&lt;/a&gt;项目至今都没有引入 RSpec，测试写起来也没什么毛病。&lt;/p&gt;
&lt;h2 id="3. CI服务"&gt;3. CI 服务&lt;/h2&gt;
&lt;p&gt;个人觉得更好的开发流程肯定还是&lt;strong&gt;有人相互 Review 代码。代码推送到托管平台之后有 CI 服务来帮忙跑测试，测试不过的一律不予合并。合并之后自动部署到测试环境。&lt;/strong&gt;写了测试之后才觉得 CI 是如此重要，起码它能帮你把测试跟部署的工作都放到云端。你自己的机子就专门用于开发，并测试那些关键的模块就好，并不需要跑全局测试。&lt;/p&gt;

&lt;p&gt;我们的项目托管在 Github 上，是采用了&lt;a href="https://circleci.com/" rel="nofollow" target="_blank" title=""&gt;CircleCI&lt;/a&gt;来跑测试，你也可以跟社区一样采用&lt;a href="https://travis-ci.org/" rel="nofollow" target="_blank" title=""&gt;Travis&lt;/a&gt;来跑。不过个人觉得有时间的话尝试自建或许更好一些。笔者本来也以为 CircleCI 的免费版都够我用了，无奈随着用例越来越多，CircleCI 所提供的每周信用额度在周三就消耗殆尽（周日清 0），导致后面几天只能在本地去跑测试了，后面会尝试使用自建的 CI 服务。&lt;/p&gt;
&lt;h2 id="结语"&gt;结语&lt;/h2&gt;
&lt;p&gt;以上就是近期尝试测试驱动开发的心得体会。反正目前看来还是觉得挺不错的，希望能够坚持下去。很多人会担心加了测试之后会降低开发的速度，其实从个人的使用角度来看倒还好。特别是编写接口的时候，不用打开浏览器就几乎能完成所有接口的开发，而且测试用例就能帮我检测一些字段的正确性（规避了肉眼检测的疏忽），许多重构代码时的暴雷都能很好地帮我检测出来。&lt;/p&gt;

&lt;p&gt;虽说前期学习成本有点大，不过坚持一段时间之后好处渐渐显现出来了，Bug 率也明显降低。建议有条件的同学可以在项目中试试，我觉得它的坏处大概就是熟悉了这一套流程之后，会养成习惯，一旦不写测试都不知道怎么写代码了。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Sun, 06 Dec 2020 10:56:30 +0800</pubDate>
      <link>https://ruby-china.org/topics/40653</link>
      <guid>https://ruby-china.org/topics/40653</guid>
    </item>
    <item>
      <title>TestProf II —— Ruby 测试的 “工厂疗法”（翻译）</title>
      <description>&lt;p&gt;&lt;em&gt;本文已获得原作者（Vladimir Dementyev）和 Evil Martians 授权许可进行翻译。原文是 TestProf 这个 Evil Martians 出品的 Gem 介绍文章系列的第二篇。作者介绍了造成 Ruby 慢测试的一个主要元凶——Factory Cascade，以及如何使用 TestProf 来消灭这个元凶。文中也提到了众所周知的 Factories vs. Fixtures 问题，而 TestProf 可以做到让你鱼和熊掌兼得。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;我的翻译 Blog 链接在这里：&lt;a href="https://xfyuan.github.io/2020/07/testprof-factory-therapy-for-ruby-tests/" rel="nofollow" target="_blank"&gt;https://xfyuan.github.io/2020/07/testprof-factory-therapy-for-ruby-tests/&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原文链接：&lt;a href="https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest" rel="nofollow" target="_blank" title=""&gt;TestProf II: Factory therapy for your Ruby tests — Martian Chronicles, Evil Martians’ team blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;作者：&lt;a href="https://twitter.com/palkan_tula" rel="nofollow" target="_blank" title=""&gt;Vladimir Dementyev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;站点：Evil Martians ——位于纽约和俄罗斯的 Ruby on Rails 开发人员博客。它发布了许多优秀的文章，并且是不少 gem 的赞助商。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;【下面是正文】&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="概述"&gt;概述&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;学习如何把你的 Ruby 测试套件带回到健康满满、速度满满的路上，通过使用 &lt;a href="https://test-prof.evilmartians.io/" rel="nofollow" target="_blank" title=""&gt;TestProf&lt;/a&gt;——一个强大的工具包来诊断所有跟测试有关的问题。这一次，我们来聊聊 factories：它们如何拖慢你的测试，如何来估量那些负面影响，如何避免，以及如何让你的 factories 跟 fixtures 一样快。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://test-prof.evilmartians.io/" rel="nofollow" target="_blank" title=""&gt;TestProf&lt;/a&gt;，用于很多 Evil Martians 的项目，以缩短 &lt;a href="https://en.wikipedia.org/wiki/Test-driven_development" rel="nofollow" target="_blank" title=""&gt;TDD&lt;/a&gt; 的反馈环，对任何其测试运行时间超过一分钟的 Rails（或其他基于 Ruby 的）应用而言都是一个必备工具。它通过扩展有关功能而对 &lt;a href="http://rspec.info/" rel="nofollow" target="_blank" title=""&gt;RSpec&lt;/a&gt; 和 &lt;a href="https://github.com/seattlerb/minitest" rel="nofollow" target="_blank" title=""&gt;minitest&lt;/a&gt; 均可适用。&lt;/p&gt;

&lt;p&gt;在我们展示该开源项目的&lt;a href="https://evilmartians.com/chronicles/testprof-a-good-doctor-for-slow-ruby-tests" rel="nofollow" target="_blank" title=""&gt;介绍文章&lt;/a&gt;里，当时承诺会有一篇专门文章来讲述在测试 Ruby Web 应用时被经常忽视的一个问题：&lt;em&gt;factory cascades&lt;/em&gt;。本文就是我们所承诺的东西。&lt;/p&gt;

&lt;p&gt;通过在你的实际测试中运行一下 &lt;a href="https://test-prof.evilmartians.io/" rel="nofollow" target="_blank" title=""&gt;TestProf&lt;/a&gt; 有个概览是比较好的。所以如果你手边恰好有一个 RSpec 覆盖的 Rails 项目，且使用了 factory_bot（之前闻名的名字叫 factory_girl）的 factories——我们建议你阅读下去之前安装该 gem，那么这将会是一个互动式演练！&lt;/p&gt;

&lt;p&gt;&lt;a href="https://test-prof.evilmartians.io/#/getting_started" rel="nofollow" target="_blank" title=""&gt;安装 TestProf&lt;/a&gt; 很简单，把下面这行添加到你&lt;code&gt;Gemfile&lt;/code&gt;的&lt;code&gt;:test&lt;/code&gt;组：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'test-prof'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="Crumbling factories"&gt;Crumbling factories&lt;/h2&gt;
&lt;p&gt;无论何时要测试自己的应用时，我们都需要生成测试数据——两种常见的方式就是 &lt;em&gt;factories&lt;/em&gt; 和 &lt;em&gt;fixtures&lt;/em&gt;。&lt;/p&gt;

&lt;p&gt;factory 是一个可以根据预定义 schema 生成其他对象（可以被持久化，也可以不）并&lt;em&gt;动态&lt;/em&gt;实现的对象。&lt;/p&gt;

&lt;p&gt;Fixtures 相当于一个不同的方案：它们声明数据的静态状态，其会被立即加载到测试数据库中，且通常在测试运行之间保持不变。&lt;/p&gt;

&lt;p&gt;在 Rails 世界中，我们有&lt;a href="http://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures" rel="nofollow" target="_blank" title=""&gt;内置 fixtures&lt;/a&gt; 和广受欢迎的第三方 factory 工具（比如 &lt;a href="https://github.com/thoughtbot/factory_bot" rel="nofollow" target="_blank" title=""&gt;factory_bot&lt;/a&gt;, &lt;a href="http://www.fabricationgem.org/" rel="nofollow" target="_blank" title=""&gt;Fabrication&lt;/a&gt;, 及&lt;a href="https://www.ruby-toolbox.com/categories/rails_fixture_replacement" rel="nofollow" target="_blank" title=""&gt;其他&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;尽管关于 &lt;a href="https://evilmartians.com/chronicles/factories-or-fixtures" rel="nofollow" target="_blank" title=""&gt;“factories vs. fixtures”&lt;/a&gt; 的辩论看起来永远不会停止，不过我们仍然把 factories 视为一种更加灵活也更易于维护的方式来处理测试数据。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;然而，能力越大责任越大：factories 更容易让你搬起石头砸自己的脚，让你的测试陷入困境。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;那么，我们如何判断自己是否滥用了这种能力并且该怎么办？首先，让我们来看看测试套件耗费在 factories 上的时间有多少。&lt;/p&gt;

&lt;p&gt;对此，我们应该使用这个“良医”：&lt;a href="https://test-prof.evilmartians.io/" rel="nofollow" target="_blank" title=""&gt;TestProf&lt;/a&gt;。该 gem 是个完整的诊断工具包，&lt;a href="https://test-prof.evilmartians.io/#/event_prof" rel="nofollow" target="_blank" title=""&gt;EventProf&lt;/a&gt; 为其中工具之一。顾名思义：它是一个事件分析器，可以让它追踪&lt;code&gt;factory.create&lt;/code&gt;&lt;a href="https://test-prof.evilmartians.io/#/event_prof?id=quotfactorycreatequot" rel="nofollow" target="_blank" title=""&gt;事件&lt;/a&gt;，它会在你每次调用&lt;code&gt;FactoryBot.create()&lt;/code&gt;时被触发，同时一个 factory 生成的对象被保存到数据库中。&lt;/p&gt;

&lt;p&gt;EventProf 对 &lt;a href="http://rspec.info/" rel="nofollow" target="_blank" title=""&gt;RSpec&lt;/a&gt; 和 &lt;a href="https://github.com/seattlerb/minitest" rel="nofollow" target="_blank" title=""&gt;minitest&lt;/a&gt; 都支持，有一个命令行界面，所以在任何 Rails 项目目录下启动终端（当然，它必须得有测试和 factories，且本文中所有测试用例都假设用的是 RSpec）并运行如下命令：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ EVENT_PROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"factory.create"&lt;/span&gt; bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在输出中，你可以看到从 factories 创建数据所花费的总时间，以及前五个最慢的测试：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;TEST PROF INFO] EventProf results &lt;span class="k"&gt;for &lt;/span&gt;factory.create

Total &lt;span class="nb"&gt;time&lt;/span&gt;: 03:07.353
Total events: 7459

Top 5 slowest suites &lt;span class="o"&gt;(&lt;/span&gt;by &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;:

UsersController &lt;span class="o"&gt;(&lt;/span&gt;users_controller_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt; – 00:10.119 &lt;span class="o"&gt;(&lt;/span&gt;581 / 248&lt;span class="o"&gt;)&lt;/span&gt;
DocumentsController &lt;span class="o"&gt;(&lt;/span&gt;documents_controller_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt; – 00:07.494 &lt;span class="o"&gt;(&lt;/span&gt;71 / 24&lt;span class="o"&gt;)&lt;/span&gt;
RolesController &lt;span class="o"&gt;(&lt;/span&gt;roles_controller_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt; – 00:04.972 &lt;span class="o"&gt;(&lt;/span&gt;181 / 76&lt;span class="o"&gt;)&lt;/span&gt;

Finished &lt;span class="k"&gt;in &lt;/span&gt;6 minutes 36 seconds &lt;span class="o"&gt;(&lt;/span&gt;files took 32.79 seconds to load&lt;span class="o"&gt;)&lt;/span&gt;
3158 examples, 0 failures, 7 pending
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从我们项目之一取出的一个真实范例中（重构之前），在六分半的测试运行时间里，超过三分钟是用于生成测试数据，其几乎占据了 50%。这并不令人惊讶：我曾经工作过的一些代码库上，从 factories 生成数据花费了多达 80% 的测试时间。&lt;/p&gt;

&lt;p&gt;平静一下，请继续阅读，我们明白如何修复这个问题。&lt;/p&gt;
&lt;h2 id="The name of the game is “cascade”"&gt;The name of the game is “cascade”&lt;/h2&gt;
&lt;p&gt;根据多年以来的观测和对 &lt;a href="https://test-prof.evilmartians.io/" rel="nofollow" target="_blank" title=""&gt;TestProf&lt;/a&gt; 的开发，以及对所有与测试有关东西的分析，慢测试的原因里最大的一个是——&lt;em&gt;factory cascade&lt;/em&gt;。&lt;/p&gt;

&lt;p&gt;让我们来做一个演示：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"Awesome comment #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;author&lt;/span&gt;
  &lt;span class="n"&gt;answer&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="ss"&gt;:answer&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"Awesome answer #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;author&lt;/span&gt;
  &lt;span class="n"&gt;question&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="ss"&gt;:question&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"Awesome question #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;author&lt;/span&gt;
  &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="c1"&gt;# suppose it's our tenant in SaaS application&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"Awesome author #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;account&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="ss"&gt;:account&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"Awesome account #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，试着猜一下，一旦你调用&lt;code&gt;create(:comment)&lt;/code&gt;，会有多少条记录被创建到数据库中？如果你已经有了答案，请继续往下看。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;首先，我们对于&lt;code&gt;comment&lt;/code&gt;生成了一个&lt;code&gt;body&lt;/code&gt;。但还没有记录被创建，所以这时总数是 0。&lt;/li&gt;
&lt;li&gt;接下来，对于&lt;code&gt;comment&lt;/code&gt;我们需要一个&lt;code&gt;author&lt;/code&gt;。&lt;code&gt;author&lt;/code&gt;应该属于&lt;code&gt;account&lt;/code&gt;，因此我们创建了两条记录。总数：2。&lt;/li&gt;
&lt;li&gt;每个 comment 需要一个可注释的对象，对吧？我们这里是&lt;code&gt;answer&lt;/code&gt;。&lt;code&gt;answer&lt;/code&gt;自身需要一个&lt;code&gt;author&lt;/code&gt;跟&lt;code&gt;account&lt;/code&gt;。这就多了三条记录。总数：2 + 2 = 4。【&lt;em&gt;译者注：原文这里有点描述不清晰。前面说“三条记录”，是把 answer、author、account 一起算上的，后面总数计算的数字又按 2 来加，应该没计入 answer。结合后文看，answer 是在最后一步才计入的，所以这里按 2 算是对的。&lt;/em&gt;】&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;answer&lt;/code&gt;也需要一个&lt;code&gt;question&lt;/code&gt;，其有自己的&lt;code&gt;author&lt;/code&gt;跟后者自身的&lt;code&gt;account&lt;/code&gt;。而且，我们的&lt;code&gt;:question&lt;/code&gt; factory 也包含一个&lt;code&gt;account&lt;/code&gt;关联。总数：4 + 4 = 8&lt;/li&gt;
&lt;li&gt;现在我们可以创建&lt;code&gt;answer&lt;/code&gt;以及&lt;code&gt;comment&lt;/code&gt;本身了。总数：8 + 2 = 10。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;就是如此！使用&lt;code&gt;create(:comment)&lt;/code&gt;创建一个 comment 产生了十条数据库记录。&lt;/p&gt;

&lt;p&gt;我们需要多个 account 和不同的 author 来测试一个单独的 comment 吗？不太可能吧。&lt;/p&gt;

&lt;p&gt;你可以想象出当我们创建多个 comment 时，比如&lt;code&gt;create_list(:comment, 10)&lt;/code&gt;，会发生什么。休斯敦，我们碰上麻烦了。【&lt;em&gt;译者注：电影《阿波罗 13》的经典台词&lt;/em&gt;】&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;遭遇 factory cascade——一个通过嵌套 factory 调用生成过量数据的无法控制的过程。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我们可以把 cascade 用一颗树来描绘：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;comment
|
|-- author
|    |
|    |-- account
|
|-- answer
     |
     |-- author
     |    |
     |    |-- account
     |
     |-- question
     |    |
     |    |-- author
     |    |    |
     |    |    |-- account
     |    |
     |    |--account
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们把这种呈现形式称为 &lt;em&gt;factory 树&lt;/em&gt;。稍后会用在我们的分析中。&lt;/p&gt;
&lt;h2 id="Fire walk with me"&gt;Fire walk with me&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://test-prof.evilmartians.io/#/event_prof" rel="nofollow" target="_blank" title=""&gt;EventProf&lt;/a&gt; 仅为我们展示了花费在 factories 上的总时间，也因此能够识别出某些东西不对。然而，我们仍然不知道它们在哪儿，除非去挖掘代码做猜谜游戏。使用 TestProf 医疗包中的另一个工具，就不用这么麻烦了。&lt;/p&gt;

&lt;p&gt;第二个分析器登场：&lt;a href="https://test-prof.evilmartians.io/#/factory_prof" rel="nofollow" target="_blank" title=""&gt;FactoryProf&lt;/a&gt;。你可以这样来运行它：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ FPROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报告结果列出了所有 factories 及其使用情况统计：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;TEST PROF INFO] Factories usage

 total      top-level                            name
  1298              2                         account
  1275             69                            city
   524            516                            room
   551            549                            user
   396            117                      membership

524 examples, 0 failures
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;total&lt;/code&gt;和&lt;code&gt;top-level&lt;/code&gt;结果有什么区别？&lt;code&gt;total&lt;/code&gt;值是一个 factory 被使用生成数据记录的次数，不管显式使用（通过&lt;code&gt;create&lt;/code&gt;调用），还是在另一个 factory 内的隐式使用（通过关联关系和回调）；而&lt;code&gt;top-level&lt;/code&gt;值仅考虑显式调用。&lt;/p&gt;

&lt;p&gt;因此，&lt;code&gt;top-level&lt;/code&gt;值和&lt;code&gt;total&lt;/code&gt;值之间的明显不同就可能指示了 factory cascade：告知我们一个 factory 更经常从其他 factories 被引用，而非直接调用其自身。&lt;/p&gt;

&lt;p&gt;如何准确找出这个“其他 factories”？就用前面讨论过的 factory 树来帮忙！我们来把这颗树平铺展开（使用 &lt;a href="https://en.wikipedia.org/wiki/Tree_traversal#Pre-order_(NLR)" rel="nofollow" target="_blank" title=""&gt;pre-order traversal&lt;/a&gt;）并把结果列表称为 &lt;em&gt;factory 堆栈&lt;/em&gt;：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;// factory stack built from the factory tree above
&lt;span class="o"&gt;[&lt;/span&gt;:comment, :author, :account, :answer, :author, :account, :question, :author, :account, :account]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是如何以编程方法构建 factory 堆栈的方式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;每次&lt;code&gt;FactoryBot.create(:thing)&lt;/code&gt;被调用时，一个新堆栈就被初始化（使用&lt;code&gt;:smth&lt;/code&gt;作为首元素）。&lt;/li&gt;
&lt;li&gt;每次另一个 factory 在一个&lt;code&gt;:thing&lt;/code&gt;内被使用时，我们把其压入堆栈。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为什么堆栈很棒？正如跟堆栈调用那样，我们可以绘制火焰图！而有什么比火焰图更酷的呢？&lt;/p&gt;

&lt;p&gt;&lt;a href="https://test-prof.evilmartians.io/#/factory_prof" rel="nofollow" target="_blank" title=""&gt;FactoryProf&lt;/a&gt; 了解如何生成交互的 HTML 火焰图报告，开箱即用。下面是另一个命令行调用：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ FPROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;flamegraph bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果包含一个 HTML 报告的路径：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;TEST PROF INFO] FactoryFlame report generated: tmp/test_prof/factory-flame.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在你浏览器中打开，可以看到类似这样：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.evilmartians.com/front/posts/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest/flame1-fe0a939.gif" title="" alt="https://cdn.evilmartians.com/front/posts/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest/flame1-fe0a939.gif"&gt;&lt;/p&gt;

&lt;p&gt;我们怎样来阅读它？&lt;/p&gt;

&lt;p&gt;每一栏代表一个 factory 堆栈。该栏越宽，该堆栈在测试中耗费的时间越多。&lt;code&gt;root&lt;/code&gt;那栏展示了 top-level &lt;code&gt;create&lt;/code&gt;调用的总数。&lt;/p&gt;

&lt;p&gt;如果你的 FactoryFlame 报告看起来象纽约的大厦轮廓线那样参差不齐，这就是有很多 factory cascade 了（每个“摩天大楼”代表一个 cascade）：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.evilmartians.com/front/posts/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest/flame2-7303761.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;尽管看起来景色很美，但这并非你理想的无 cascade 报告应有的模样。相反，你的目标应该是象荷兰乡村那样平坦的东西：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.evilmartians.com/front/posts/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest/flame3-7377b3b.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="Doctor, am I going to live?"&gt;Doctor, am I going to live?&lt;/h2&gt;
&lt;p&gt;知道如何发现 cascade 是不够的——我们还需要消灭它们。让我们来考虑有关的几种技术吧。&lt;/p&gt;
&lt;h3 id="Explicit associations"&gt;Explicit associations&lt;/h3&gt;
&lt;p&gt;涌上心头的第一个做法就是从我们的 factories 移除所有（或几乎所有）关联关系：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="ss"&gt;:comment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"Awesome comment #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;# do not declare associations&lt;/span&gt;
  &lt;span class="c1"&gt;# author&lt;/span&gt;
  &lt;span class="c1"&gt;# answer&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采用这个方案，你在使用一个 factory 时必须明确指定所有需要的关联关系：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;author: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;answer: &lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# But!&lt;/span&gt;
&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; raises ActiveRecord::InvalidRecord&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有人可能会问：我们使用 factories 不就是为了每次避免指定所需的参数么？对，没错。用这个方案，factories 变快了，但也更没用了。&lt;/p&gt;
&lt;h3 id="Association inference"&gt;Association inference&lt;/h3&gt;
&lt;p&gt;有时候（通常是在处理 &lt;a href="https://en.wikipedia.org/wiki/Denormalization" rel="nofollow" target="_blank" title=""&gt;denormalization&lt;/a&gt;时）是可能从其他关系推断出关联关系的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="ss"&gt;:question&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"Awesome question #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;author&lt;/span&gt;
  &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# infer account from author&lt;/span&gt;
    &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&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;create(:question)&lt;/code&gt;或&lt;code&gt;create(:question, author: user)&lt;/code&gt;而不创建一个单独的 account。&lt;/p&gt;

&lt;p&gt;我们也能够使用生命周期回调：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="ss"&gt;:question&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"Awesome question #&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;transient&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="ss"&gt;:undef&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="ss"&gt;:undef&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:build&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_evaluator&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# if only author is specified, set account to author's account&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:undef&lt;/span&gt;
    &lt;span class="c1"&gt;# if only account is specified, set author to account's owner&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;owner&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:undef&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;这个方案会很有效，但需要很多重构（而且，坦率地讲，让 factories 更难阅读）。&lt;/p&gt;
&lt;h3 id="Factory default"&gt;Factory default&lt;/h3&gt;
&lt;p&gt;而 TestProf 提供了另一种方式来消除 cascade——&lt;a href="https://test-prof.evilmartians.io/#/factory_default" rel="nofollow" target="_blank" title=""&gt;FactoryDefault&lt;/a&gt;。它是一个 &lt;a href="https://github.com/thoughtbot/factory_bot" rel="nofollow" target="_blank" title=""&gt;factory_bot&lt;/a&gt; 的扩展，通过允许你隐式重用 factory 内的记录，来启用更简洁和更不易出错的 DSL，以创建有关联关系的默认值。考虑如下示例：&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;'PATCH #update'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create_deafult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# implicitly uses account defined above&lt;/span&gt;
  &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# implicitly uses account and author defined above&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# implicitly uses question and author defined above&lt;/span&gt;

  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:another_question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# uses the same account and author&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:another_answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# uses the same question and author&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方案的主要优势在于你不必修改自己的 factories。你所有需要做的就是把测试中的一些&lt;code&gt;create(…)&lt;/code&gt;调用替换为&lt;code&gt;create_default(…)&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;另一方面，这个特性也为你的测试带来了一些魔法，所以请谨慎使用，因为测试应该尽可能保持可读性。仅针对 top-level 的实体（比如多租户 App 中的租户）使用是个不错的主意。&lt;/p&gt;
&lt;h2 id="Bonus: AnyFixture"&gt;Bonus: AnyFixture&lt;/h2&gt;
&lt;p&gt;到目前为止我们只讨论了 factory cascade。从 TestProf 报告中我们还能看出其他什么呢？&lt;/p&gt;

&lt;p&gt;让我们再来看一眼 FactoryProf 报告：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;TEST PROF INFO] Factories usage

 total      top-level                            name
  1298              2                         account
  1275             69                            city
   524            516                            room
   551            549                            user

524 examples, 0 failures
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到&lt;code&gt;room&lt;/code&gt;和&lt;code&gt;user&lt;/code&gt; factories 被使用了跟测试用例总数大约相同的次数。因此，在每个用例中两者可能都需要。对于所有用例一次性创建这些记录呢？那么，我们可以使用 fixtures。&lt;/p&gt;

&lt;p&gt;由于我们已经有了 factories，重用它们来生成 fixtures 会是很棒的。有请 &lt;a href="https://test-prof.evilmartians.io/#/any_fixture" rel="nofollow" target="_blank" title=""&gt;AnyFixture&lt;/a&gt; 登场。&lt;/p&gt;

&lt;p&gt;你可以为数据生成使用任何代码块，而 AnyFixture 会在运行结束时负责清理数据库。&lt;/p&gt;

&lt;p&gt;AnyFixture 可以很好地跟 RSpec 的 &lt;a href="https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-context" rel="nofollow" target="_blank" title=""&gt;shared contexts&lt;/a&gt; 一起工作：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Activate AnyFixture DSL (fixture) through refinements&lt;/span&gt;
&lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="no"&gt;TestProf&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AnyFixture&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DSL&lt;/span&gt;

&lt;span class="n"&gt;shared_context&lt;/span&gt; &lt;span class="s2"&gt;"shared user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# You should call AnyFixture outside of transaction to re-use the same&lt;/span&gt;
  &lt;span class="c1"&gt;# data between examples&lt;/span&gt;
  &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;room: &lt;/span&gt;&lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;)&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;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后激活该 shared context：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;CitiesController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sign_in&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随着 AnyFixture 的启用，FactoryProf 报告会看起来这样：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;total      top-level                            name
 1298              2                         account
 1275             69                            city
  8                1                            room
  2                1                            user

524 examples, 0 failures
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来完美，不是么？&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;小孩子才对 factories 和 fixtures 做选择，我们全都要！&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="附记"&gt;附记&lt;/h2&gt;
&lt;p&gt;感谢阅读！&lt;/p&gt;

&lt;p&gt;Factories 为你的测试数据生成带来了简单和灵活，但它们非常&lt;em&gt;脆弱&lt;/em&gt;——cascade 如幽灵般出现，而重复地数据创建会消费大量的时间。&lt;/p&gt;

&lt;p&gt;照顾好你的 factories 吧，定期带它们去看看医生（&lt;a href="https://test-prof.evilmartians.io/" rel="nofollow" target="_blank" title=""&gt;TestProf&lt;/a&gt;）。让测试更快速，开发者就更快乐！&lt;/p&gt;

&lt;p&gt;请阅读 &lt;a href="https://evilmartians.com/chronicles/testprof-a-good-doctor-for-slow-ruby-tests" rel="nofollow" target="_blank" title=""&gt;TestProf 介绍文章&lt;/a&gt; 以了解更多该项目和其他使用场景背后的初衷。&lt;/p&gt;</description>
      <author>apexy</author>
      <pubDate>Sat, 25 Jul 2020 11:17:31 +0800</pubDate>
      <link>https://ruby-china.org/topics/40197</link>
      <guid>https://ruby-china.org/topics/40197</guid>
    </item>
    <item>
      <title>TestProf I —— Ruby 慢测试的 “良医圣手”（翻译）</title>
      <description>&lt;p&gt;&lt;em&gt;本文已获得原作者（Vladimir Dementyev）和 Evil Martians 授权许可进行翻译。原文介绍了 TestProf 这个 Evil Martians 出品的强大 Gem。作者通过详细的范例场景和代码演示，说明了 TestProf 怎样对 Ruby 测试进行性能分析，找出慢测试的痛点，以及如何使用其提供的工具箱对慢测试改进，缩短测试运行时间，进行令人愉悦的 Ruby 开发。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;我的翻译 Blog 链接在这里：&lt;a href="https://xfyuan.github.io/2020/07/testprof-doctor-for-slow-ruby-tests/" rel="nofollow" target="_blank"&gt;https://xfyuan.github.io/2020/07/testprof-doctor-for-slow-ruby-tests/&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原文链接：&lt;a href="https://evilmartians.com/chronicles/testprof-a-good-doctor-for-slow-ruby-tests" rel="nofollow" target="_blank" title=""&gt;TestProf: a good doctor for slow Ruby tests — Martian Chronicles, Evil Martians’ team blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;作者：&lt;a href="https://twitter.com/palkan_tula" rel="nofollow" target="_blank" title=""&gt;Vladimir Dementyev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;站点：Evil Martians ——位于纽约和俄罗斯的 Ruby on Rails 开发人员博客。它发布了许多优秀的文章，并且是不少 gem 的赞助商。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;【下面是正文】&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="概述"&gt;概述&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;编写测试是开发过程的重要部分，尤其是在 Ruby 和 Rails 社区。我们平常不会关注测试用例套件的性能，直到发现自己在对测试“绿点”的等待中已经耗费了太多时间为止。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;我已经花费了许多时间用在分析测试套件的性能上，也开发了一些很有用的技术和工具来让测试跑得更快。我把所有这些集合到了一个称为 &lt;a href="https://github.com/palkan/test-prof" rel="nofollow" target="_blank" title=""&gt;TestProf&lt;/a&gt; 的 Gem 中，这是一个 Ruby 测试的分析工具箱。&lt;/p&gt;
&lt;h2 id="The Motivation"&gt;The Motivation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;慢测试浪费你的时间，降低你的效率。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;你可能在扪心自问：“为什么测试性能很重要？”。在给出任何建议之前，让我向你展示一些统计数据吧。&lt;/p&gt;

&lt;p&gt;今年早些时候我做过一个小调查，询问了 Ruby 开发者关于其测试偏好的内容。&lt;/p&gt;

&lt;p&gt;首先——各位，好消息是——事实证明大多数 Ruby 开发者都确实会编写测试（坦率地说，我并不感到惊讶）。顺便说一句，这就是我之所以如此喜欢 Ruby 社区的原因。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.evilmartians.com/front/posts/testprof-a-good-doctor-for-slow-ruby-tests/writetests-c0e3731.svg" title="" alt="https://cdn.evilmartians.com/front/posts/testprof-a-good-doctor-for-slow-ruby-tests/writetests-c0e3731.svg"&gt;&lt;/p&gt;

&lt;p&gt;根据这个调查，所有测试套件只有四分之一其运行时间超过 10 分钟——同时只有一半是运行少于 5 分钟。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.evilmartians.com/front/posts/testprof-a-good-doctor-for-slow-ruby-tests/howlong-e6897ac.svg" title="" alt="https://cdn.evilmartians.com/front/posts/testprof-a-good-doctor-for-slow-ruby-tests/howlong-e6897ac.svg"&gt;&lt;/p&gt;

&lt;p&gt;看起来情况还不坏，对吧？那让我们只来看拥有超过 1000 个测试用例的情况。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.evilmartians.com/front/posts/testprof-a-good-doctor-for-slow-ruby-tests/howlong1000-b37bf3e.svg" title="" alt="https://cdn.evilmartians.com/front/posts/testprof-a-good-doctor-for-slow-ruby-tests/howlong1000-b37bf3e.svg"&gt;&lt;/p&gt;

&lt;p&gt;现在看起来就差多了：将近一半的测试套件运行都超过了 10 分钟，而几乎 30%——更是超过了 20 分钟。&lt;/p&gt;

&lt;p&gt;顺便说一句，我一直工作着的一个典型 Rails 应用有着 6000 ～ 10000 个测试用例。&lt;/p&gt;

&lt;p&gt;当然，你不是每次进行一个改动后都必须运行整个测试套件。通常，当我做一个中等大小的功能时，每次提交之前会运行大约 100 个测试用例，而这只花费一分钟左右。但即使这样的“一分钟”也影响到了我的&lt;em&gt;反馈环&lt;/em&gt;（参看 Justin Searls 的&lt;a href="https://www.youtube.com/watch?v=VD51AkG8EZw" rel="nofollow" target="_blank" title=""&gt;演讲&lt;/a&gt;）从而浪费了我的时间。&lt;/p&gt;

&lt;p&gt;尽管如此，在部署周期之内，我们还是必须在使用 CI 服务时运行所有测试。你是否愿意等待好几十分钟（如果队列中有大量的构建，甚至要数小时）来部署一个 hotfix？我很怀疑。&lt;/p&gt;

&lt;p&gt;并发地构建会有所帮助，但它们是有代价的。看看下面的柱状图：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.evilmartians.com/front/posts/testprof-a-good-doctor-for-slow-ruby-tests/howmanyci-82f3d21.svg" title="" alt="https://cdn.evilmartians.com/front/posts/testprof-a-good-doctor-for-slow-ruby-tests/howmanyci-82f3d21.svg"&gt;&lt;/p&gt;

&lt;p&gt;例如，我&lt;a href="https://onboardiq.com/" rel="nofollow" target="_blank" title=""&gt;当前的项目&lt;/a&gt;上，我们有 5 个并行构建，而平均（每个 job）RSpec 耗时是 2 分 30 秒于 1250 个测试用例上。这意味着我们的 EPM（examples per minute）等于 500.&lt;/p&gt;

&lt;p&gt;在未优化之前，800 个测试用例耗时 4 分钟——这仅仅 200 EPM！现在每次构建我们节省了 3～4 分钟。&lt;/p&gt;

&lt;p&gt;所以，毫无疑问，慢测试浪费你的时间，拉低你的工作效率。&lt;/p&gt;
&lt;h2 id="The Toolbox"&gt;The Toolbox&lt;/h2&gt;
&lt;p&gt;好，你已经认识到了自己的测试套件很慢。如何找出它们慢的原因？&lt;/p&gt;

&lt;p&gt;让我略过&lt;a href="https://www.youtube.com/watch?v=q52n4p0wkIs" rel="nofollow" target="_blank" title=""&gt;这个介绍视频&lt;/a&gt;的所有说辞直接向你介绍 &lt;a href="https://github.com/palkan/test-prof" rel="nofollow" target="_blank" title=""&gt;TestProf&lt;/a&gt;——一个 Ruby 测试分析工具箱。&lt;/p&gt;

&lt;p&gt;TestProf 旨在帮你识别测试套件的瓶颈，并为你提供修复它们的“配方”。&lt;/p&gt;

&lt;p&gt;我来给你展示下自己是如何使用它来分析并且改进测试的。&lt;/p&gt;
&lt;h3 id="General Profiling"&gt;General Profiling&lt;/h3&gt;
&lt;p&gt;在深入挖掘整个测试套件之前，收集一些常规信息通常很有用。&lt;/p&gt;

&lt;p&gt;尝试回答如下问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在哪些地方你的测试花费了更多的时间：controllers、models、services 或者 jobs？&lt;/li&gt;
&lt;li&gt;最耗时间的 module/method 是什么？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;并非那么容易，对吧？&lt;/p&gt;

&lt;p&gt;要回答第一个问题，你可以使用 TestProf 的 &lt;a href="https://test-prof.evilmartians.io/#/tag_prof.md" rel="nofollow" target="_blank" title=""&gt;&lt;em&gt;Tag Profiler&lt;/em&gt;&lt;/a&gt;，它让你可以收集到根据特别的 RSpec tag 值进行分组的统计信息。RSpec 为测试用例&lt;a href="https://relishapp.com/rspec/rspec-rails/v/3-6/docs/directory-structure" rel="nofollow" target="_blank" title=""&gt;自动添加&lt;/a&gt;了&lt;code&gt;type&lt;/code&gt;的 tag，所以我们可以这样使用它：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TAG_PROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type &lt;/span&gt;rspec

&lt;span class="o"&gt;[&lt;/span&gt;TEST PROF INFO] TagProf report &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;type

          type          time   &lt;/span&gt;total  %total   %time           avg

    controller     08:02.721    2822   39.04   34.29     00:00.171
       service     05:56.686    1363   18.86   25.34     00:00.261
         model     04:26.176    1711   23.67   18.91     00:00.155
           job     01:58.766     327    4.52    8.44     00:00.363
       request     01:06.716     227    3.14    4.74     00:00.293
          form     00:37.212     218    3.02    2.64     00:00.170
         query     00:19.186      75    1.04    1.36     00:00.255
        facade     00:18.415      95    1.31    1.31     00:00.193
    serializer     00:10.201      19    0.26    0.72     00:00.536
        policy     00:06.023      65    0.90    0.43     00:00.092
     presenter     00:05.593      42    0.58    0.40     00:00.133
        mailer     00:04.974      41    0.57    0.35     00:00.121
        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在要查找瓶颈即可只关注某些测试类型就够了。&lt;/p&gt;

&lt;p&gt;你可能已经了解了常规的 Ruby 分析器，例如 &lt;a href="https://github.com/ruby-prof/ruby-prof" rel="nofollow" target="_blank" title=""&gt;RubyProf&lt;/a&gt; 和 &lt;a href="https://github.com/tmm1/stackprof" rel="nofollow" target="_blank" title=""&gt;StackProf&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;TestProf 帮助你在测试套件上无需任何调整就能运行它们：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TEST_RUBY_PROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rake &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# or&lt;/span&gt;

&lt;span class="nv"&gt;TEST_STACK_PROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 rspec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些分析器生成的报告可以帮你识别最热的堆栈路径，从而回答第二个问题。&lt;/p&gt;

&lt;p&gt;不幸的是，这种类型的分析需要大量资源，让你本来就不那么快的测试套件愈加迟缓。你不得不在测试的一小部分上来运行它，但如何选择是哪一小部分？好吧，只能随机了！&lt;/p&gt;

&lt;p&gt;TestProf 包含一个&lt;a href="https://test-prof.evilmartians.io/#/tests_sampling.md" rel="nofollow" target="_blank" title=""&gt;特别的补丁&lt;/a&gt;，让你可以运行随机的 RSpec 用例组（或 Minitest 的）：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SAMPLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在尝试在你 controller 测试的一个样例上运行 StackProf（因为根据上面 TestProf 它们是最慢的）看看输出结果。当我在自己的一个项目上这样做之后，看到了如下内容：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;%self     calls  name
20.85       721   &amp;lt;Class::BCrypt::Engine&amp;gt;#__bc_crypt
 2.31      4690  &lt;span class="k"&gt;*&lt;/span&gt;ActiveSupport::Notifications::Instrumenter#instrument
 1.12     47489   Arel::Visitors::Visitor#dispatch
 1.04    205208   String#to_s
 0.87    531377   Module#&lt;span class="o"&gt;===&lt;/span&gt;
 0.87    117109  &lt;span class="k"&gt;*&lt;/span&gt;Class#new
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事实证明我们 &lt;a href="https://github.com/Sorcery/sorcery" rel="nofollow" target="_blank" title=""&gt;Sorcery&lt;/a&gt; 的 encryption 配置在测试环境跟在生产环境中一样严格。&lt;/p&gt;

&lt;p&gt;一个典型的 Rails 应用中，关于时间你会在报告内看到类似这样的一些内容：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;TOTAL    &lt;span class="o"&gt;(&lt;/span&gt;pct&lt;span class="o"&gt;)&lt;/span&gt;     SAMPLES    &lt;span class="o"&gt;(&lt;/span&gt;pct&lt;span class="o"&gt;)&lt;/span&gt;     FRAME
   205  &lt;span class="o"&gt;(&lt;/span&gt;48.6%&lt;span class="o"&gt;)&lt;/span&gt;          96  &lt;span class="o"&gt;(&lt;/span&gt;22.7%&lt;span class="o"&gt;)&lt;/span&gt;     ActiveRecord::PostgreSQLAdapter#exec_no_cache
    41   &lt;span class="o"&gt;(&lt;/span&gt;9.7%&lt;span class="o"&gt;)&lt;/span&gt;          22   &lt;span class="o"&gt;(&lt;/span&gt;5.2%&lt;span class="o"&gt;)&lt;/span&gt;     ActiveModel::AttributeMethods::#define_proxy_call
    20   &lt;span class="o"&gt;(&lt;/span&gt;4.7%&lt;span class="o"&gt;)&lt;/span&gt;          14   &lt;span class="o"&gt;(&lt;/span&gt;3.3%&lt;span class="o"&gt;)&lt;/span&gt;     ActiveRecord::LazyAttributeHash#[]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大量&lt;code&gt;ActiveRecord&lt;/code&gt;的东西——意味着大量的数据库操作。想知道如何处理？继续往下看。&lt;/p&gt;
&lt;h3 id="Database Interactions"&gt;Database Interactions&lt;/h3&gt;
&lt;p&gt;知道你的测试套件在数据库上花费了多少时间吗？先猜猜看，然后使用 TestProf 来计算一下。&lt;/p&gt;

&lt;p&gt;我们&lt;a href="https://evilmartians.com/chronicles/fighting-the-hydra-of-n-plus-one-queries" rel="nofollow" target="_blank" title=""&gt;已经扩展了&lt;/a&gt; Rails 中的 instrumentation（ActiveSupport 的 &lt;a href="http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html" rel="nofollow" target="_blank" title=""&gt;Notification&lt;/a&gt; 和 &lt;a href="http://guides.rubyonrails.org/active_support_instrumentation.html" rel="nofollow" target="_blank" title=""&gt;Instrumentation&lt;/a&gt; 功能），所以让我们略过基础来介绍 &lt;a href="https://test-prof.evilmartians.io/#/event_prof.md" rel="nofollow" target="_blank" title=""&gt;&lt;em&gt;Event Profiler&lt;/em&gt;&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;EventProf 在你的测试套件运行中收集检测指标，并提供包含常规信息的报告以及与指定指标有关的前 N 个最慢的组和用例。目前，它自带的仅支持&lt;code&gt;ActiveSupport::Notifications&lt;/code&gt;，但其&lt;a href="https://test-prof.evilmartians.io/#/event_prof.md#custom-instrumentation" title=""&gt;很容易跟你自己的解决方案集成&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;要获取有关数据库使用情况的信息，我们可以用&lt;code&gt;sql.active_record&lt;/code&gt;事件。然后报告看起来会是这样（很类似于&lt;code&gt;rspec --profile&lt;/code&gt;）：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;EVENT_PROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sql.active_record rspec ...

&lt;span class="o"&gt;[&lt;/span&gt;TEST PROF INFO] EventProf results &lt;span class="k"&gt;for &lt;/span&gt;sql.active_record

Total &lt;span class="nb"&gt;time&lt;/span&gt;: 00:05.045
Total events: 6322

Top 5 slowest suites &lt;span class="o"&gt;(&lt;/span&gt;by &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;:

MessagesController &lt;span class="o"&gt;(&lt;/span&gt;./spec/controllers/messages_controller_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt;–00:03.253 &lt;span class="o"&gt;(&lt;/span&gt;4058 / 100&lt;span class="o"&gt;)&lt;/span&gt;
UsersController &lt;span class="o"&gt;(&lt;/span&gt;./spec/controllers/users_controller_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt;–00:01.508 &lt;span class="o"&gt;(&lt;/span&gt;1574 / 58&lt;span class="o"&gt;)&lt;/span&gt;
Confirm &lt;span class="o"&gt;(&lt;/span&gt;./spec/services/confirm_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt;–00:01.255 &lt;span class="o"&gt;(&lt;/span&gt;1530 / 8&lt;span class="o"&gt;)&lt;/span&gt;
RequestJob &lt;span class="o"&gt;(&lt;/span&gt;./spec/jobs/request_job_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt;–00:00.311 &lt;span class="o"&gt;(&lt;/span&gt;437 / 3&lt;span class="o"&gt;)&lt;/span&gt;
ApplyForm &lt;span class="o"&gt;(&lt;/span&gt;./spec/forms/apply_form_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt;–00:00.118 &lt;span class="o"&gt;(&lt;/span&gt;153 / 5&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于我目前的项目，耗费在 DB 上的时间量大约是 20%——而这已经是在对其进行了大量优化之后！起初，其耗费时间超过了 30%。&lt;/p&gt;

&lt;p&gt;这个指标对于每个项目没有一个单一的最优数字。它高度依赖于你的测试风格：编写更多的单元测试还是集成测试。&lt;/p&gt;

&lt;p&gt;我们主要编写集成测试，顺便说一句——20% 并不差（但还能更好）。&lt;/p&gt;

&lt;p&gt;什么是数据库耗时偏高的典型原因呢？一言难尽，我来捋一捋其中的部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;无用的数据生成&lt;/li&gt;
&lt;li&gt;过重的测试准备（&lt;code&gt;before&lt;/code&gt;/&lt;code&gt;setup&lt;/code&gt; hooks）&lt;/li&gt;
&lt;li&gt;Factory cascades&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;第一个是著名的&lt;code&gt;Model.new&lt;/code&gt; vs. &lt;code&gt;Model.create&lt;/code&gt;问题（或者&lt;code&gt;build_stubbed&lt;/code&gt; vs. &lt;code&gt;create&lt;/code&gt;在 &lt;a href="https://robots.thoughtbot.com/use-factory-girls-build-stubbed-for-a-faster-test" rel="nofollow" target="_blank" title=""&gt;FactoryBot 中的区别&lt;/a&gt;）——你在对 model 的单元测试中可能不需要写入数据库。所以别那样做，好吧？&lt;/p&gt;

&lt;p&gt;但如果已经那样做了怎么办？如何找出哪些测试不需要持久化数据？这就该 &lt;a href="https://test-prof.evilmartians.io/#/factory_doctor.md" rel="nofollow" target="_blank" title=""&gt;&lt;em&gt;Factory Doctor&lt;/em&gt;&lt;/a&gt; 登场了。&lt;/p&gt;

&lt;p&gt;当你创建不必要的数据时，FactoryDoctor 会通知你：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;FDOC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 rspec

&lt;span class="o"&gt;[&lt;/span&gt;TEST PROF INFO] FactoryDoctor report

Total &lt;span class="o"&gt;(&lt;/span&gt;potentially&lt;span class="o"&gt;)&lt;/span&gt; bad examples: 2
Total wasted &lt;span class="nb"&gt;time&lt;/span&gt;: 00:13.165

User &lt;span class="o"&gt;(&lt;/span&gt;./spec/models/user_spec.rb:3&lt;span class="o"&gt;)&lt;/span&gt;
  validates name &lt;span class="o"&gt;(&lt;/span&gt;./spec/user_spec.rb:8&lt;span class="o"&gt;)&lt;/span&gt;–1 record created, 00:00.114
  validates email &lt;span class="o"&gt;(&lt;/span&gt;./spec/user_spec.rb:8&lt;span class="o"&gt;)&lt;/span&gt;–2 records created, 00:00.514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不幸的是，FactoryDoctor 不是魔术师（它还在学习中），有时它也会发生“误诊”的事情。&lt;/p&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="no"&gt;BeatleSearchQuery&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# We want to test our searching feature,&lt;/span&gt;
  &lt;span class="c1"&gt;# so we need some data for every example&lt;/span&gt;
  &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:paul&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:beatle&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;'Paul'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ringo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:beatle&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;'Ringo'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:george&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:beatle&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;'George'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;let!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:john&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:beatle&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;'John'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# and about 15 examples here&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能会想：“这里用 fixture 就行了呗”。这貌似个好主意，不过当你正在做一个每天都要更改数十个 model 的大型项目时，就不是那么回事儿了。&lt;/p&gt;

&lt;p&gt;另外一个考虑是用&lt;code&gt;before(:all)&lt;/code&gt; hook 仅生成数据一次。但这里要提请注意——我们不得不手动清理数据库，因为&lt;code&gt;before(:all)&lt;/code&gt;是在事务外运行的。&lt;/p&gt;

&lt;p&gt;或者，我们可以把整个组都手动包在一个事务里！这正是 TestProf 的 &lt;a href="https://test-prof.evilmartians.io/#/before_all.md" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;before_all&lt;/code&gt;&lt;/a&gt; helper 所做的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;BeatleSearchQuery&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;before_all&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="vi"&gt;@paul&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:beatle&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;'Paul'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@ringo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:beatle&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;'Ringo'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@george&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:beatle&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;'George'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@john&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:beatle&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;'John'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# and about 15 examples here&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想要在不同的组（文件）之间共享 context，考虑使用 &lt;a href="https://test-prof.evilmartians.io/#/any_fixture.md" rel="nofollow" target="_blank" title=""&gt;&lt;em&gt;Any Fixture&lt;/em&gt;&lt;/a&gt;，其让你能从代码生成 fixture（比如，使用 factories）。&lt;/p&gt;
&lt;h3 id="Factory Cascades"&gt;Factory Cascades&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Factory cascade&lt;/em&gt; 是一个非常普遍但很少解决的问题，它会让你的整个测试套件陷入困境。&lt;/p&gt;

&lt;p&gt;简而言之，它是通过嵌套 factory 调用而生成过度数据的不可控进程。TestProf 知道如何处理它，我们已经写了一篇专栏文章来单独讨论这个主题——&lt;a href="https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest" rel="nofollow" target="_blank" title=""&gt;你值得看一下&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="Background Jobs"&gt;Background Jobs&lt;/h3&gt;
&lt;p&gt;除去数据库瓶颈之外，当然还有很多其他瓶颈。我们来说说其中一个。&lt;/p&gt;

&lt;p&gt;在测试中有一个 &lt;em&gt;inline&lt;/em&gt; 后台任务的普遍做法（例如，&lt;a href="https://github.com/mperham/sidekiq/issues/3495" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;Sidekiq::Testing.inline!&lt;/code&gt;&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;通常，我们把一些繁重的事情丢进后台任务中，因此无条件地运行所有任务会拖慢运行时间。&lt;/p&gt;

&lt;p&gt;TestProf 支持对后台任务耗费时间的分析（目前，仅对 Sidekiq）。只需告诉它要分析&lt;code&gt;sidekiq.inline&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;EVENT_PROF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sidekiq.inline bundle &lt;span class="nb"&gt;exec &lt;/span&gt;rspec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在当你知道了所耗费的准确时间之后，接下来要做什么？简单地关闭 inline 模式很可能会破坏很多测试用例——太多太多以至于无法快速修复。&lt;/p&gt;

&lt;p&gt;解决方案就是全局关闭 inline 模式，仅在必要时才使用它。如果你在用 RSpec，则可以这样做：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add a shared context&lt;/span&gt;
&lt;span class="n"&gt;shared_context&lt;/span&gt; &lt;span class="s2"&gt;"sidekiq:inline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;sidekiq: :inline&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inline!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# And use it when necessary&lt;/span&gt;
&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"do some bg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;sidekiq: :inline&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那还是得必须把这些 tag 添加到每个失败的用例上，不是么？抱歉，有 TestProf 在，你确实不必。&lt;/p&gt;

&lt;p&gt;在 TestProf 的工具箱中，有一个称为 &lt;a href="https://test-prof.evilmartians.io/#/rspec_stamp.md" rel="nofollow" target="_blank" title=""&gt;&lt;em&gt;RSpec Stamp&lt;/em&gt;&lt;/a&gt; 的特殊工具。它可以&lt;em&gt;自动地&lt;/em&gt;添加指定的 tag：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RSTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sidekiq:inline rspec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺便说一句，RSpec Stamp 在其之下是用了 &lt;a href="https://ruby-doc.org/stdlib-2.4.0/libdoc/ripper/rdoc/Ripper.html" rel="nofollow" target="_blank" title=""&gt;Ripper&lt;/a&gt; 来解析源文件并准确插入 tag 的。&lt;/p&gt;

&lt;p&gt;在&lt;a href="https://test-prof.evilmartians.io/#/rspec_stamp.md" rel="nofollow" target="_blank" title=""&gt;我们的指南&lt;/a&gt;里可以阅读到关于如何从&lt;code&gt;inline!&lt;/code&gt;迁移到&lt;code&gt;fake!&lt;/code&gt;的完整说明。&lt;/p&gt;
&lt;h2 id="附记"&gt;附记&lt;/h2&gt;
&lt;p&gt;TestProf 已经发布在 &lt;a href="https://github.com/palkan/test-prof" rel="nofollow" target="_blank" title=""&gt;GitHub&lt;/a&gt; 和 &lt;a href="https://rubygems.org/gems/test-prof" rel="nofollow" target="_blank" title=""&gt;rubygems.org&lt;/a&gt;，可随时用在你的应用中，帮助你提升测试套件的性能。&lt;/p&gt;

&lt;p&gt;本文只是一个 TestProf 的简介，并未涵盖其所有特性。可以跳转到该系列的下一篇：&lt;a href="https://evilmartians.com/chronicles/testprof-2-factory-therapy-for-your-ruby-tests-rspec-minitest" rel="nofollow" target="_blank" title=""&gt;TestProf II: Factory therapy for your Ruby tests&lt;/a&gt; 来学习更多关于 TestProf“医生”的工具包，它们能使你的测试更加漂亮优雅，你的 TDD 反馈环更短更快，从而让你成为一个&lt;em&gt;快乐的&lt;/em&gt; Ruby 开发者。&lt;/p&gt;

&lt;p&gt;这里是一些额外的资源列表：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TestProf &lt;a href="https://test-prof.evilmartians.io/" rel="nofollow" target="_blank" title=""&gt;文档&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;2017 年 &lt;a href="http://railsclub.ru/" rel="nofollow" target="_blank" title=""&gt;RailsClub Moscow&lt;/a&gt; 的“Faster Tests”演讲（&lt;a href="https://www.youtube.com/watch?v=8S7oHjEiVzs" rel="nofollow" target="_blank" title=""&gt;视频&lt;/a&gt;[俄语]，&lt;a href="https://speakerdeck.com/palkan/railsclub-moscow-2017-faster-tests" rel="nofollow" target="_blank" title=""&gt;slides&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;2017 年 &lt;a href="http://rubyconference.by/" rel="nofollow" target="_blank" title=""&gt;RubyConfBy&lt;/a&gt; 的“Run Test Run”演讲（&lt;a href="https://www.youtube.com/watch?v=q52n4p0wkIs" rel="nofollow" target="_blank" title=""&gt;视频&lt;/a&gt;，&lt;a href="https://speakerdeck.com/palkan/rubyconfby-minsk-2017-run-test-run" rel="nofollow" target="_blank" title=""&gt;slides&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/benoittgt" rel="nofollow" target="_blank" title=""&gt;Benoit Tigeot&lt;/a&gt; 发表的 &lt;a href="https://medium.com/appaloosa-store-engineering/tips-to-improve-speed-of-your-test-suite-8418b485205c" rel="nofollow" target="_blank" title=""&gt;“Tips to improve speed of your test suite”&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;</description>
      <author>apexy</author>
      <pubDate>Sat, 25 Jul 2020 11:13:55 +0800</pubDate>
      <link>https://ruby-china.org/topics/40196</link>
      <guid>https://ruby-china.org/topics/40196</guid>
    </item>
    <item>
      <title>2020 时代的 Rails 系统测试（翻译）</title>
      <description>&lt;p&gt;&lt;em&gt;本文已获得原作者（Vladimir Dementyev）和 Evil Martians 授权许可进行翻译。原文介绍了在新的 2020 时代，摒弃了基于 Java 的笨重 Selenium 之后，如何在 Rails 下构建基于浏览器的高效系统测试。作者对于系统测试概念进行了详细阐述，演示了具体配置的范例和运行效果，对 Docker 开发环境也有专业级别的涵盖。非常推荐。我的翻译 Blog 链接在这里：&lt;a href="https://xfyuan.github.io/2020/07/proper-browser-testing-in-rails/" rel="nofollow" target="_blank" title=""&gt;https://xfyuan.github.io/2020/07/proper-browser-testing-in-rails/&lt;/a&gt;&lt;/em&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原文链接：&lt;a href="https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing" rel="nofollow" target="_blank" title=""&gt;System of a test:
Proper browser testing in Ruby on Rails — Martian Chronicles, Evil Martians’ team blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;作者：&lt;a href="https://twitter.com/palkan_tula" rel="nofollow" target="_blank" title=""&gt;Vladimir Dementyev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;站点：Evil Martians ——位于纽约和俄罗斯的 Ruby on Rails 开发人员博客。它发布了许多优秀的文章，并且是不少 gem 的赞助商。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;【下面是正文】&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;发现 Ruby on Rails 应用中端到端浏览器测试的最佳实践集合，并在你的项目上采用它们。了解如何摒弃基于 Java 的 Selenium，转而使用更加精简的 Ferrum-Cuprite 组合，它们能直接通过纯 Ruby 方式来使用 Chrome DevTools 的协议。如果你在使用 Docker 开发环境——本文也有涵盖。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ruby 社区对于测试饱含激情。我们有数不胜数的&lt;a href="https://www.ruby-toolbox.com/search?q=testing" rel="nofollow" target="_blank" title=""&gt;测试库&lt;/a&gt;，有成千上万篇关于测试主题的博客文章，甚至为此有一个专门的&lt;a href="https://player.fm/series/2414463" rel="nofollow" target="_blank" title=""&gt;播客&lt;/a&gt;。更可怕的是，&lt;a href="https://rubygems.org/stats" rel="nofollow" target="_blank" title=""&gt;下载量排在前三名的 Gem&lt;/a&gt; 也都是有关 &lt;a href="http://rspec.info/" rel="nofollow" target="_blank" title=""&gt;RSpec&lt;/a&gt; 测试框架的！&lt;/p&gt;

&lt;p&gt;我相信，Rails，是 Ruby 测试兴盛背后的原因之一。这个框架让测试的编写尽可能地成为一种享受了。多数情况下，跟随着 &lt;a href="https://guides.rubyonrails.org/testing.html" rel="nofollow" target="_blank" title=""&gt;Rails 测试指南&lt;/a&gt;的教导就已足够（起码在刚开始的时候）。但事情总有例外，而在我们这里，就是&lt;em&gt;系统测试&lt;/em&gt;。&lt;/p&gt;

&lt;p&gt;对 Rail 应用而言，编写和维护系统测试很难被称为“惬意的”。我在处理这个问题上，自 2013 年的第一次 &lt;a href="https://cucumber.io/" rel="nofollow" target="_blank" title=""&gt;Cucumber&lt;/a&gt; 驱动测试算起，已经逐步改进了太多。而今天，到了 2020 年，我终于可以来跟大家分享一下自己当前（关于测试）的设置了。本文中，我将会讨论以下几点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;系统测试概述&lt;/li&gt;
&lt;li&gt;使用 Cuprite 进行现代系统测试&lt;/li&gt;
&lt;li&gt;配置范例&lt;/li&gt;
&lt;li&gt;Docker 化的系统测试&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="系统测试概述"&gt;系统测试概述&lt;/h2&gt;
&lt;p&gt;“系统测试”是 Rails 世界中对于自动化端到端测试的一种通常称谓。在 Rails 采用这个名称之前，我们使用诸如&lt;em&gt;功能测试&lt;/em&gt;、&lt;em&gt;浏览器测试&lt;/em&gt;、甚至&lt;em&gt;验收测试&lt;/em&gt;等各种叫法（尽管后者在意思上&lt;a href="https://en.wikipedia.org/wiki/Acceptance_testing" rel="nofollow" target="_blank" title=""&gt;有所不同&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;如果我们回忆下&lt;a href="https://martinfowler.com/bliki/TestPyramid.html" rel="nofollow" target="_blank" title=""&gt;测试金字塔&lt;/a&gt;（或者就&lt;a href="http://yakshave.fm/7" rel="nofollow" target="_blank" title=""&gt;金字塔&lt;/a&gt;），系统测试是处于非常靠上的位置：它们把整个程序视为一个黑盒，通常模拟终端用户的行为和预期。而这就是在 Web 应用程序情况下，为什么我们需要浏览器来运行这些测试的原因（或者至少是类似 &lt;a href="https://github.com/rack/rack-test" rel="nofollow" target="_blank" title=""&gt;Rack Test&lt;/a&gt; 的模拟器）。&lt;/p&gt;

&lt;p&gt;我们来看下典型的系统测试架构：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.evilmartians.com/front/posts/system-of-a-test-setting-up-end-to-end-rails-testing/diagram-25c8249.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;我们需要管理至少三个“进程”（它们有些可以是 Ruby 线程）：一个 运行我们应用的 Web 服务端，一个浏览器，和一个测试运行器。这是最低要求。实际情况下，我们通常还需要另一个工具提供 API 来控制浏览器（比如，ChromeDriver）。一些工具尝试通过构建特定浏览器（例如 capybara-webkit 和 PhantomJS）来简化此设置，并提供开箱即用的此类 API，但它们在对真正浏览器的兼容性“竞争”中都败下阵来，没能幸免于难。&lt;/p&gt;

&lt;p&gt;当然，我们需要添加少量 Ruby Gem 测试依赖库——来把所有碎片都粘合起来。更多的依赖库会带来更多的问题。例如，&lt;a href="https://github.com/DatabaseCleaner/database_cleaner" rel="nofollow" target="_blank" title=""&gt;Database Cleaner&lt;/a&gt; 在很长时间内都是一个必备的库：我们不能使用事务来自动回滚数据库状态，因为每个线程都是用自己独立的连接。我们不得不针对每张表使用&lt;code&gt;TRUNCATE ...&lt;/code&gt;或&lt;code&gt;DELETE FROM ...&lt;/code&gt;来代替，这会相当慢。我们通过在所有线程中使用一个共享连接解决了这个问题（使用 &lt;a href="https://test-prof.evilmartians.io/#/active_record_shared_connection" rel="nofollow" target="_blank" title=""&gt;TestProf extension&lt;/a&gt;）。&lt;a href="https://github.com/rails/rails/pull/28083" rel="nofollow" target="_blank" title=""&gt;Rails 5.1&lt;/a&gt; 也发布了一个现成的类似功能。&lt;/p&gt;

&lt;p&gt;这样，通过添加系统测试，我们增加了开发环境和 CI 环境的维护成本，以及引入了潜在的故障或不稳定因素：由于复杂的设置，&lt;a href="https://github.com/evilmartians/terraforming-rails/blob/master/guides/flaky.md" rel="nofollow" target="_blank" title=""&gt;&lt;em&gt;flakiness&lt;/em&gt;&lt;/a&gt; 是端到端测试中最常见的问题。而大多数这些 flakiness 来自于跟浏览器的通信。&lt;/p&gt;

&lt;p&gt;尽管通过在 Rails 5.1 中引入系统测试简化了浏览器测试，它们仍然需要一些配置才能平滑运行：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;你需要处理 Web drivers（Rails 假设你使用 Selenium）。&lt;/li&gt;
&lt;li&gt;你可以自己在容器化环境中配置系统测试（例如，象&lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development" rel="nofollow" target="_blank" title=""&gt;这篇文章中的做法&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;配置不够灵活（例如，屏幕快照的路径）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;让我们来转到代码层面吧，看看在 2020 时代如何让你的系统测试更有乐趣！&lt;/p&gt;
&lt;h2 id="使用 Cuprite 进行现代系统测试"&gt;使用 Cuprite 进行现代系统测试&lt;/h2&gt;
&lt;p&gt;默认情况下，Rails 假设你会使用 &lt;a href="https://www.selenium.dev/" rel="nofollow" target="_blank" title=""&gt;Selenium&lt;/a&gt; 来跑系统测试。Selenium 是一个实战验证过的 Web 浏览器自动化软件。它旨在为所有浏览器提供一个通用 API 以及最真实的体验。在模拟用户浏览器交互方面，唯有有血有肉的真人才比它做得更好些。&lt;/p&gt;

&lt;p&gt;不过，这种能力不是没有代价的：你需要安装特定浏览器的驱动，现实交互的开销在规模上是显而易见的（例如，Selenium 的测试通常都相当慢）。&lt;/p&gt;

&lt;p&gt;Selenium 已经是很久以前发布的了，那时浏览器还不提供任何内置自动化测试兼容性。这么多年过去了，现在的情况已经大不相同，Chrome 引入了 &lt;a href="https://chromedevtools.github.io/devtools-protocol/" rel="nofollow" target="_blank" title=""&gt;CDP 协议&lt;/a&gt;。使用 CDP，你可以直接操作浏览器的 Session，不再需要中间的抽象层和工具。&lt;/p&gt;

&lt;p&gt;从那以后，涌现了好多利用 CDP 的项目，包括最著名的——&lt;a href="https://github.com/puppeteer/puppeteer" rel="nofollow" target="_blank" title=""&gt;Puppeteer&lt;/a&gt;，一个 Node.js 的浏览器自动化库。那么 Ruby 世界呢？是 &lt;a href="https://github.com/rubycdp/ferrum" rel="nofollow" target="_blank" title=""&gt;Ferrum&lt;/a&gt;，一个 Ruby 的 CDP 库，尽管还相当年轻，也提供了不逊色于 Puppeteer 的体验。而对我们更重要的是，它带来了一个称为 &lt;a href="https://github.com/rubycdp/cuprite" rel="nofollow" target="_blank" title=""&gt;Cuprite&lt;/a&gt; 的伙伴项目——使用 CDP 的纯 Ruby Capybara 驱动。&lt;/p&gt;

&lt;p&gt;我从 2020 年初才开始积极使用 Cuprite 的（一年前我尝试过，但在 Docker 环境下有些问题），从未让我后悔过。设置系统测试变得异常简单（全部所需仅 Chrome 而已），而且执行是如此之快，以至于在从 Selenium 迁移过来之后我的一些测试都失败了：它们缺乏合适的异步期望断言，在 Selenium 中能通过仅仅是因为 Selenium 太慢。&lt;/p&gt;

&lt;p&gt;让我们来看下我最近工作上所用到 Cuprite 的系统测试配置。&lt;/p&gt;
&lt;h2 id="注释过的配置范例"&gt;注释过的配置范例&lt;/h2&gt;
&lt;p&gt;这个范例来自于我最近的开源 Ruby on Rails 项目——&lt;a href="https://github.com/anycable/anycable_rails_demo" rel="nofollow" target="_blank" title=""&gt;AnyCable Rails Demo&lt;/a&gt;。它旨在演示如何跟 Rails 应用一起使用&lt;a href="https://evilmartians.com/chronicles/anycable-1-0-four-years-of-real-time-web-with-ruby-and-go" rel="nofollow" target="_blank" title=""&gt;刚刚发布&lt;/a&gt;的 AnyCable 1.0，但我们也可用于本文——它有很好的系统测试覆盖着。&lt;/p&gt;

&lt;p&gt;这个项目使用 RSpec 及其系统测试封装器。其大部分也是可以用在 Minitest 上的。&lt;/p&gt;

&lt;p&gt;让我们从一个足以在本地机器上运行的最小示例开始。其代码放在 AnyCable Rails Demo 的 &lt;a href="https://github.com/anycable/anycable_rails_demo/tree/demo/dockerless" rel="nofollow" target="_blank" title=""&gt;demo/dockerless&lt;/a&gt; 分支上。&lt;/p&gt;

&lt;p&gt;首先来快速看一眼 Gemfile：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'capybara'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'selenium-webdriver'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'cuprite'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;什么？为什么需要&lt;code&gt;selenium-webdriver&lt;/code&gt;，如果我们根本不用 Selenium 的话？事实证明，Rails 要求这个 gem 独立于你所使用的驱动而存在。好消息是，这&lt;a href="https://github.com/rails/rails/pull/39179" rel="nofollow" target="_blank" title=""&gt;已经被修复了&lt;/a&gt;，我们有望在 Rails 6.1 中移除这个 gem。&lt;/p&gt;

&lt;p&gt;我把系统测试的配置放在多个文件内，位于&lt;code&gt;spec/system/support&lt;/code&gt;目录，使用专门的&lt;code&gt;system_helper.rb&lt;/code&gt;来加载它们：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;spec/
  system/
    support/
      better_rails_system_tests.rb
      capybara_setup.rb
      cuprite_setup.rb
      precompile_assets.rb
      ...
  system_helper.rb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来看下上面这个列表中的每个文件都是干嘛的。&lt;/p&gt;
&lt;h3 id="system_helper.rb"&gt;system_helper.rb&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;system_helper.rb&lt;/code&gt;文件包含一些针对系统测试的通用 RSpec 配置，不过，通常而言，都如下面这样简单：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Load general RSpec Rails configuration&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"rails_helper.rb"&lt;/span&gt;

&lt;span class="c1"&gt;# Load configuration files and helpers&lt;/span&gt;
&lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__dir__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"system/support/**/*.rb"&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，在你的系统测试中，使用&lt;code&gt;require "system_helper"&lt;/code&gt;来激活该配置。&lt;/p&gt;

&lt;p&gt;我们为系统测试使用了一个单独的 helper 文件和一个 support 文件夹，以避免在我们只需运行一个单元测试时加载所有多余的配置。&lt;/p&gt;
&lt;h3 id="capybara_setup.rb"&gt;capybara_setup.rb&lt;/h3&gt;
&lt;p&gt;这个文件包含针对 &lt;a href="https://github.com/teamcapybara/capybara" rel="nofollow" target="_blank" title=""&gt;Capybara&lt;/a&gt; 框架的配置：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/system/support/capybara_setup.rb&lt;/span&gt;

&lt;span class="c1"&gt;# Usually, especially when using Selenium, developers tend to increase the max wait time.&lt;/span&gt;
&lt;span class="c1"&gt;# With Cuprite, there is no need for that.&lt;/span&gt;
&lt;span class="c1"&gt;# We use a Capybara default value here explicitly.&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_max_wait_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;# Normalize whitespaces when using `has_text?` and similar matchers,&lt;/span&gt;
&lt;span class="c1"&gt;# i.e., ignore newlines, trailing spaces, etc.&lt;/span&gt;
&lt;span class="c1"&gt;# That makes tests less dependent on slightly UI changes.&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_normalize_ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# Where to store system tests artifacts (e.g. screenshots, downloaded files, etc.).&lt;/span&gt;
&lt;span class="c1"&gt;# It could be useful to be able to configure this path from the outside (e.g., on CI).&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CAPYBARA_ARTIFACTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"./tmp/capybara"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该文件也包含一个对于 Capybara 很有用的补丁，其目的我们稍后揭示：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/system/support/capybara_setup.rb&lt;/span&gt;

&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;singleton_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:last_used_session&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;using_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_used_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;
  &lt;span class="k"&gt;ensure&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_used_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Capybara.using_session&lt;/code&gt;让你能够操作不同的浏览器 session，从而在单个测试场景内操作多个独立 session。这在测试实时功能时尤其有用，例如使用 WebSocket 的功能。&lt;/p&gt;

&lt;p&gt;该补丁跟踪上次使用的 session 名称。我们会用这个信息来支持在多 session 测试中获取失败情况下的屏幕快照。&lt;/p&gt;
&lt;h3 id="cuprite_setup.rb"&gt;cuprite_setup.rb&lt;/h3&gt;
&lt;p&gt;这个文件负责配置 Cuprite：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/system/support/cuprite_setup.rb&lt;/span&gt;

&lt;span class="c1"&gt;# First, load Cuprite Capybara integration&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"capybara/cuprite"&lt;/span&gt;

&lt;span class="c1"&gt;# Then, we need to register our driver to be able to use it later&lt;/span&gt;
&lt;span class="c1"&gt;# with #driven_by method.&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:cuprite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cuprite&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Driver&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;app&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="ss"&gt;window_size: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="c1"&gt;# See additional options for Dockerized environment in the respective section of this article&lt;/span&gt;
      &lt;span class="ss"&gt;browser_options: &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="c1"&gt;# Increase Chrome startup wait time (required for stable CI builds)&lt;/span&gt;
      &lt;span class="ss"&gt;process_timeout: &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;# Enable debugging capabilities&lt;/span&gt;
      &lt;span class="ss"&gt;inspector: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;# Allow running Chrome in a headful mode by setting HEADLESS env&lt;/span&gt;
      &lt;span class="c1"&gt;# var to a falsey value&lt;/span&gt;
      &lt;span class="ss"&gt;headless: &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HEADLESS"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;in?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sx"&gt;%w[n 0 no false]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Configure Capybara to use :cuprite driver by default&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;javascript_driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:cuprite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们也为常用 Cuprite API 方法定义了一些快捷方式：&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;CupriteHelpers&lt;/span&gt;
  &lt;span class="c1"&gt;# Drop #pause anywhere in a test to stop the execution.&lt;/span&gt;
  &lt;span class="c1"&gt;# Useful when you want to checkout the contents of a web page in the middle of a test&lt;/span&gt;
  &lt;span class="c1"&gt;# running in a headful mode.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pause&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Drop #debug anywhere in a test to open a Chrome inspector and pause the execution&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&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="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CupriteHelpers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :system&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面你可以看到一个&lt;code&gt;#debug&lt;/code&gt;帮助方法如何工作的演示：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.jsdelivr.net/gh/xfyuan/ossimgs@master/20200716debug_local.av1-4989ddb.gif" title="" alt="Debugging system tests"&gt;&lt;/p&gt;
&lt;h3 id="better_rails_system_tests.rb"&gt;better_rails_system_tests.rb&lt;/h3&gt;
&lt;p&gt;这个文件包含一些有关 Rails 系统测试内部的补丁以及通用配置（代码注释有详细解释）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/system/support/better_rails_system_tests.rb&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;BetterRailsSystemTests&lt;/span&gt;
  &lt;span class="c1"&gt;# Use our `Capybara.save_path` to store screenshots with other capybara artifacts&lt;/span&gt;
  &lt;span class="c1"&gt;# (Rails screenshots path is not configurable https://github.com/rails/rails/blob/49baf092439fc74fc3377b12e3334c3dd9d0752f/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L79)&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;absolute_image_path&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/screenshots/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;image_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Make failure screenshots compatible with multi-session setup.&lt;/span&gt;
  &lt;span class="c1"&gt;# That's where we use Capybara.last_used_session introduced before.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;take_screenshot&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_used_session&lt;/span&gt;

    &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;using_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_used_session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;super&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="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt; &lt;span class="no"&gt;BetterRailsSystemTests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :system&lt;/span&gt;

  &lt;span class="c1"&gt;# Make urls in mailers contain the correct server host.&lt;/span&gt;
  &lt;span class="c1"&gt;# It's required for testing links in emails (e.g., via capybara-email).&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :system&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;was_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_url_options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_url_options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server_host&lt;/span&gt;
    &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_url_options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;was_host&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Make sure this hook runs before others&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend_before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :system&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Use JS driver always&lt;/span&gt;
    &lt;span class="n"&gt;driven_by&lt;/span&gt; &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;javascript_driver&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="precompile_assets.rb"&gt;precompile_assets.rb&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/anycable/anycable_rails_demo/blob/master/spec/system/support/precompile_assets.rb" rel="nofollow" target="_blank" title=""&gt;这个文件&lt;/a&gt;负责在运行系统测试之前预编译 assets（我不在这儿粘贴它的完整代码，只给出最有趣的部分）：&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;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# Skip assets precompilcation if we exclude system specs.&lt;/span&gt;
  &lt;span class="c1"&gt;# For example, you can run all non-system tests via the following command:&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;#    rspec --tag ~type:system&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# In this case, we don't need to precompile assets.&lt;/span&gt;
  &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;opposite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"system"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exclude_pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;%r{spec/system}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:suite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# We can use webpack-dev-server for tests, too!&lt;/span&gt;
    &lt;span class="c1"&gt;# Useful if you working on a frontend code fixes and want to verify them via system tests.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Webpacker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dev_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;running?&lt;/span&gt;
      &lt;span class="vg"&gt;$stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;⚙️  Webpack dev server is running! Skip assets compilation.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="k"&gt;next&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="vg"&gt;$stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;🐢  Precompiling assets.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

      &lt;span class="c1"&gt;# The code to run webpacker:compile Rake task&lt;/span&gt;
      &lt;span class="c1"&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;为什么要手动预编译 assets 呢，如果 Rails 能够自动为你做这事的话？问题在于 Rails 预编译 assets 是惰性的（比如，在你首次请求一个 asset 的时候），这会使你的第一个测试用例非常非常慢，甚至碰到随机超时的异常。&lt;/p&gt;

&lt;p&gt;另一个我想提请注意的是使用 Webpack dev server 进行系统测试的能力。这在当你进行艰苦的前端代码重构时相当有用：你可以暂停一个测试，打开浏览器，编辑前端代码并看到它被热加载了！&lt;/p&gt;
&lt;h2 id="Docker 化的系统测试"&gt;Docker 化的系统测试&lt;/h2&gt;
&lt;p&gt;让我们把自己的配置提升到更高的层次，使其兼容我们的 &lt;a href="https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development" rel="nofollow" target="_blank" title=""&gt;Docker 开发环境&lt;/a&gt;。Docker 化版本的测试设置在 AnyCable Rails Demo 代码库的&lt;a href="https://github.com/anycable/anycable_rails_demo" rel="nofollow" target="_blank" title=""&gt;默认分支&lt;/a&gt;上，可随意查看，不过下面我们打算涵盖那些有意思的内容。&lt;/p&gt;

&lt;p&gt;Docker 下设置的主要区别是我们在一个单独的容器中运行浏览器实例。可以把 Chrome 添加到你的基础 Rails 镜像上，或者可能的话，甚至从容器内使用主机的浏览器（这可以用 &lt;a href="https://avdi.codes/run-rails-6-system-tests-in-docker-using-a-host-browser/" rel="nofollow" target="_blank" title=""&gt;Selenium and ChromeDriver&lt;/a&gt; 做到）。但是，在我看来，为&lt;code&gt;docker-compose.yml&lt;/code&gt;定义一个专用的浏览器 service 是一种更正确的 &lt;em&gt;Docker 式方式&lt;/em&gt;来干这个。&lt;/p&gt;

&lt;p&gt;目前，我用的是来自 &lt;a href="https://github.com/browserless/chrome" rel="nofollow" target="_blank" title=""&gt;browserless.io&lt;/a&gt; 的 Chrome Docker 镜像。它带有一个好用的 Debug 查看器，让你能够调试 headless 的浏览器 session（本文最后有一个简短的视频演示）：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;chrome&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;browserless/chrome:1.31-chrome-stable&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3333:3333"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# By default, it uses 3000, which is typically used by Rails.&lt;/span&gt;
      &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3333&lt;/span&gt;
      &lt;span class="c1"&gt;# Set connection timeout to avoid timeout exception during debugging&lt;/span&gt;
      &lt;span class="c1"&gt;# https://docs.browserless.io/docs/docker.html#connection-timeout&lt;/span&gt;
      &lt;span class="na"&gt;CONNECTION_TIMEOUT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把&lt;code&gt;CHROME_URL: http://chrome:3333&lt;/code&gt;添加到你的 Rails service 环境变量，以后台方式运行 Chrome：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt; chrome
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，如果提供了 URL，我们就需要配置 Cuprite 以和远程浏览器一起工作：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cuprite_setup.rb&lt;/span&gt;

&lt;span class="c1"&gt;# Parse URL&lt;/span&gt;
&lt;span class="c1"&gt;# NOTE: REMOTE_CHROME_HOST should be added to Webmock/VCR allowlist if you use any of those.&lt;/span&gt;
&lt;span class="no"&gt;REMOTE_CHROME_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CHROME_URL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;REMOTE_CHROME_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;REMOTE_CHROME_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;REMOTE_CHROME_URL&lt;/span&gt;
    &lt;span class="no"&gt;URI&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="no"&gt;REMOTE_CHROME_URL&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;yield_self&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;port&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="c1"&gt;# Check whether the remote chrome is running.&lt;/span&gt;
&lt;span class="n"&gt;remote_chrome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;REMOTE_CHROME_URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
      &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="no"&gt;Socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;REMOTE_CHROME_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;REMOTE_CHROME_PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;connect_timeout: &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;close&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;rescue&lt;/span&gt; &lt;span class="no"&gt;Errno&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ECONNREFUSED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Errno&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EHOSTUNREACH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SocketError&lt;/span&gt;
    &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;remote_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remote_chrome&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="no"&gt;REMOTE_CHROME_URL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的配置假设当&lt;code&gt;CHROME_URL&lt;/code&gt;未被设置或者浏览器未响应时，使用者想使用本地安装的 Chrome。&lt;/p&gt;

&lt;p&gt;我们这样做以便让配置向下兼容本地的配置（我们一般不强迫每个人都使用 Docker 作为开发环境；让拒绝使用 Docker 者为其独特的本地设置而受苦吧😈）。&lt;/p&gt;

&lt;p&gt;我们的驱动注册现在看起来是这样的：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/system/support/cuprite_setup.rb&lt;/span&gt;

&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register_driver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:cuprite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cuprite&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Driver&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;app&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="ss"&gt;window_size: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;browser_options: &lt;/span&gt;&lt;span class="n"&gt;remote_chrome&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"no-sandbox"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
      &lt;span class="ss"&gt;inspector: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remote_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们也需要更新自己的&lt;code&gt;#debug&lt;/code&gt;帮助方法以打印 Debug 查看器的 URL，而不是尝试去打开浏览器：&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;CupriteHelpers&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;binding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vg"&gt;$stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"🔎 Open Chrome inspector at http://localhost:3333"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pry&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;binding&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&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;localhost&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;为此，我们需要配置 Capybara 服务端的 host：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/system/support/capybara_setup.rb&lt;/span&gt;

&lt;span class="c1"&gt;# Make server accessible from the outside world&lt;/span&gt;
&lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0"&lt;/span&gt;
&lt;span class="c1"&gt;# Use a hostname that could be resolved in the internal Docker network&lt;/span&gt;
&lt;span class="c1"&gt;# NOTE: Rails overrides Capybara.app_host in Rails &amp;lt;6.1, so we have&lt;/span&gt;
&lt;span class="c1"&gt;# to store it differently&lt;/span&gt;
&lt;span class="no"&gt;CAPYBARA_APP_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`hostname`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0"&lt;/span&gt;
&lt;span class="c1"&gt;# In Rails 6.1+ the following line should be enough&lt;/span&gt;
&lt;span class="c1"&gt;# Capybara.app_host = "http://#{`hostname`.strip&amp;amp;.downcase || "0.0.0.0"}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，让我们对&lt;code&gt;better_rails_system_tests.rb&lt;/code&gt;做一些调整。&lt;/p&gt;

&lt;p&gt;首先，我们来让 VS Code 中的屏幕快照通知变得可点击🙂（Docker 绝对路径与主机系统是不同的）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/system/support/better_rails_system_tests.rb&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;BetterRailsSystemTests&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="c1"&gt;# Use relative path in screenshot message&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;image_path&lt;/span&gt;
    &lt;span class="n"&gt;absolute_image_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative_path_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_s&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;其次，确保测试都使用了正确的服务端 host（这&lt;a href="https://github.com/rails/rails/commit/d415eb4f6d6bb24b78b968ae28c22bb7e1721285#diff-9de6fe0bff4847b77cba72441ee855c2" rel="nofollow" target="_blank" title=""&gt;在 Rails 6.1 中已经被修复了&lt;/a&gt;）：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spec/system/support/better_rails_system_tests.rb&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend_before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :system&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Rails sets host to `127.0.0.1` for every test by default.&lt;/span&gt;
  &lt;span class="c1"&gt;# That won't work with a remote browser.&lt;/span&gt;
  &lt;span class="n"&gt;host!&lt;/span&gt; &lt;span class="no"&gt;CAPYBARA_APP_HOST&lt;/span&gt;
  &lt;span class="c1"&gt;# Use JS driver always&lt;/span&gt;
  &lt;span class="n"&gt;driven_by&lt;/span&gt; &lt;span class="no"&gt;Capybara&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;javascript_driver&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="In too Dip"&gt;In too Dip&lt;/h3&gt;
&lt;p&gt;如果你使用 &lt;a href="https://github.com/bibendi/dip" rel="nofollow" target="_blank" title=""&gt;Dip&lt;/a&gt; 来管理 Docker 开发环境（我强烈建议你这么做，它使你获得容器的强大能力，而无需记忆所有 Docker CLI 命令的成本付出），那么你可以通过在&lt;code&gt;dip.yml&lt;/code&gt;中添加自定义命令和在&lt;code&gt;docker-compose.yml&lt;/code&gt;中添加一个额外 service 定义，来避免手动加载&lt;code&gt;chrome&lt;/code&gt; service：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;

&lt;span class="c1"&gt;# Separate definition for system tests to add Chrome as a dependency&lt;/span&gt;
&lt;span class="na"&gt;rspec_system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*backend&lt;/span&gt;
  &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*backend_depends_on&lt;/span&gt;
    &lt;span class="na"&gt;chrome&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;

&lt;span class="c1"&gt;# dip.yml&lt;/span&gt;
&lt;span class="na"&gt;rspec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Rails unit tests&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rspec --exclude-pattern spec/system/**/*_spec.rb&lt;/span&gt;
  &lt;span class="na"&gt;subcommands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Rails system tests&lt;/span&gt;
      &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rspec_system&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundle exec rspec --pattern spec/system/**/*_spec.rb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我使用如下命令来运行系统测试：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dip rspec system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这样了！&lt;/p&gt;

&lt;p&gt;最后，让我向你展示一下如何使用 Browserless.io 的 Docker 镜像的 Debug 查看器进行调试：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.jsdelivr.net/gh/xfyuan/ossimgs@master/20200716debug_docker.av1-c027cb7.gif" title="" alt="Debugging system tests running in Docker"&gt;&lt;/p&gt;</description>
      <author>apexy</author>
      <pubDate>Sun, 19 Jul 2020 16:08:32 +0800</pubDate>
      <link>https://ruby-china.org/topics/40177</link>
      <guid>https://ruby-china.org/topics/40177</guid>
    </item>
    <item>
      <title>Jepsen 测试框架在图数据库 Nebula Graph 中的实践</title>
      <description>&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/56643819/72118703-f6f65c00-338c-11ea-8fa2-1ecaa5b49bd5.png" title="" alt="产品细节"&gt;&lt;/p&gt;

&lt;p&gt;在本篇文章中主要介绍图数据库 Nebula Graph 在 Jepsen 这块的实践。&lt;/p&gt;
&lt;h2 id="Jepsen&amp;nbsp;简介"&gt;Jepsen&amp;nbsp;简介&lt;/h2&gt;
&lt;p&gt;Jepsen&amp;nbsp;是一款用于系统测试的开源软件库，致力于提高分布式数据库、队列、共识系统等的安全性。作者&amp;nbsp;Kyle Kingsbury&amp;nbsp;使用函数式编程语言&amp;nbsp;Clojure&amp;nbsp;编写了这款测试框架，并对多个著名的分布式系统和数据库进行了一致性测试。目前&amp;nbsp;Jepsen&amp;nbsp;仍在 GitHub&amp;nbsp;保持活跃，能否通过&amp;nbsp;Jepsen&amp;nbsp;的测试已经成为各个分布式数据库对自身检验的一个标杆。&lt;/p&gt;
&lt;h2 id="Jepsen&amp;nbsp;的测试流程"&gt;Jepsen&amp;nbsp;的测试流程&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/56643819/72118748-20af8300-338d-11ea-86f8-3bec328b5b60.png" title="" alt="流程图"&gt;&lt;/p&gt;

&lt;p&gt;Jepsen 测试推荐使用 Docker 搭建集群。默认情况下由 6 个 container 组成，其中一个是控制节点（control node），另外 5 个是数据库的节点（默认为 n1-n5）。控制节点在测试程序开始后会启用多个 worker 进程，并发地通过 SSH 登入数据库节点进行读写操作。&lt;/p&gt;

&lt;p&gt;测试开始后，控制节点会创建一组进程，进程包含了待测试分布式系统的客户端。另一个 Generator 进程产生每个客户端执行的操作，并将操作应用于待测试的分布式系统。每个操作的开始和结束以及操作结果记录在历史记录中。同时，一个特殊进程 Nemesis 将故障引入系统。&lt;/p&gt;

&lt;p&gt;测试结束后，Checker 分析历史记录是否正确，是否符合一致性。用户可以使用 Jepsen 的 &lt;a href="https://github.com/jepsen-io/knossos" rel="nofollow" target="_blank" title=""&gt;knossos&lt;/a&gt; 中提供的验证模型，也可以自己定义符合需求的模型对测试结果进行验证。同时，还可以在测试中注入错误对集群进行干扰测试。&lt;/p&gt;

&lt;p&gt;最后根据本次测试所规定的验证模型对结果进行分析。&lt;/p&gt;
&lt;h2 id="如何使用&amp;nbsp;Jepsen"&gt;如何使用&amp;nbsp;Jepsen&lt;/h2&gt;
&lt;p&gt;使用 Jepsen 过程中可能会遇到一些问题，可以参考一下使用 Tips：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在&amp;nbsp;Jepsen&amp;nbsp;框架中，用户需要在&amp;nbsp;DB&amp;nbsp;接口中对自己的数据库定义下载，安装，启动与终止操作。在终止后，可以将&amp;nbsp;log&amp;nbsp;文件清除，同时也可以指定&amp;nbsp;log&amp;nbsp;的存储位置，Jepsen&amp;nbsp;会将其拷贝至&amp;nbsp;Jepsen&amp;nbsp;的&amp;nbsp;log&amp;nbsp;文件夹中，以便连同&amp;nbsp;Jepsen&amp;nbsp;自身的&amp;nbsp;log&amp;nbsp;进行分析。&lt;/li&gt;
&lt;li&gt;用户还需要提供访问自己数据库的客户端，这个客户端可以是你用&amp;nbsp;Clojure&amp;nbsp;实现的，比如&amp;nbsp;etcd&amp;nbsp;的&lt;a href="https://github.com/aphyr/verschlimmbesserung" rel="nofollow" target="_blank" title=""&gt;verschlimmbesserung&lt;/a&gt;，也可以是&amp;nbsp;JDBC，等等。然后需要定义&amp;nbsp;Client&amp;nbsp;接口，告诉&amp;nbsp;Jepsen 如何对你的数据库进行操作。&lt;/li&gt;
&lt;li&gt;在&amp;nbsp;Checker&amp;nbsp;中，你可以选择需要的测试模型，比如，性能测试（checker/perf）将会生成&amp;nbsp;latency&amp;nbsp;和整个测试过程的图表，时间轴（timeline/html）会生成一个记录着所有操作时间轴的&amp;nbsp;html&amp;nbsp;页面。&lt;/li&gt;
&lt;li&gt;另外一个不可或缺的组件就是在&amp;nbsp;nemesis&amp;nbsp;中注入想要测试的错误了。网络分区（nemesis/partition-random-halves）和杀掉数据节点（kill-node）是比较常见的注入错误。&lt;/li&gt;
&lt;li&gt;在&amp;nbsp;Generator&amp;nbsp;中，用户可以告知&amp;nbsp;worker&amp;nbsp;进程需要生成哪些操作，每一次操作的时间间隔，每一次错误注入的时间间隔等等。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="用 Jepsen 测试图数据库&amp;nbsp;Nebula Graph"&gt;用 Jepsen 测试图数据库&amp;nbsp;Nebula Graph&lt;/h2&gt;
&lt;p&gt;分布式图数据库 Nebula Graph 主要由&amp;nbsp;3&amp;nbsp;部分组成，分别是&amp;nbsp;meta&amp;nbsp;层，graph&amp;nbsp;层和&amp;nbsp;storage&amp;nbsp;层。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/56643819/72118779-358c1680-338d-11ea-84ea-2ef07faec18d.png" title="" alt="architecture"&gt;&lt;/p&gt;

&lt;p&gt;我们在使用&amp;nbsp;Jepsen&amp;nbsp;对&amp;nbsp;kv&amp;nbsp;存储接口进行的测试中，搭建了一个由&amp;nbsp;8&amp;nbsp;个&amp;nbsp;container&amp;nbsp;组成的集群：一个&amp;nbsp;Jepsen&amp;nbsp;的控制节点，一个&amp;nbsp;meta&amp;nbsp;节点，一个&amp;nbsp;graph&amp;nbsp;节点，和&amp;nbsp;5&amp;nbsp;个&amp;nbsp;storage&amp;nbsp;节点，集群由&amp;nbsp;Docker-compose&amp;nbsp;启动。需要注意的是，要建立一个集群的&amp;nbsp;subnet&amp;nbsp;网络，使集群可以连通，另外要安装&amp;nbsp;ssh&amp;nbsp;服务，并为&amp;nbsp;control node&amp;nbsp;与&amp;nbsp;5&amp;nbsp;个&amp;nbsp;storage&amp;nbsp;节点配置免密登入。&lt;/p&gt;

&lt;p&gt;测试中使用了&amp;nbsp;Java&amp;nbsp;编写的客户端程序，生成&amp;nbsp;jar&amp;nbsp;包并加入到&amp;nbsp;Clojure&amp;nbsp;程序依赖，来对 DB 进行&amp;nbsp;put，get&amp;nbsp;和&amp;nbsp;cas (compare-and-set)&amp;nbsp;操作。另外 &lt;strong&gt;Nebula Graph 的客户端有自动重试逻辑&lt;/strong&gt;，当遇到错误导致操作失败时，客户端会启用适当的重试机制以尽力确保操作成功。&lt;/p&gt;

&lt;p&gt;Nebula-Jepsen&amp;nbsp;的测试程序目前分为三种常见的测试模型和三种常见的错误注入。&lt;/p&gt;
&lt;h3 id="Jepsen 测试模型"&gt;Jepsen 测试模型&lt;/h3&gt;&lt;h4 id="single-register"&gt;single-register&lt;/h4&gt;
&lt;p&gt;模拟一个寄存器，程序并发地对数据库进行读写操作，每次成功的写入操作都会使寄存器中存储的值发生变化，然后通过对比每次从数据库读出的值是否和寄存器中记录的值一致，来验证结果是否满足线性要求。由于寄存器是单一的，所以在此处我们生成唯一的 key，随机的 value 进行操作。&lt;/p&gt;
&lt;h4 id="multi-register"&gt;multi-register&lt;/h4&gt;
&lt;p&gt;一个可以存不同键的寄存器。和单一寄存器的效果一样，但此处我们可以使 key 也随机生成了。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;4       :invoke :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 9 1]]
4       :ok     :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 9 1]]
3       :invoke :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 5 nil]]
3       :ok     :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 5 3]]
0       :invoke :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 7 nil]]
0       :ok     :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 7 2]]
0       :invoke :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 7 1]]
0       :ok     :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 7 1]]
1       :invoke :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 1 nil]]
1       :ok     :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 1 4]]
0       :invoke :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 8 nil]]
0       :ok     :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 8 3]]
:nemesis        :info   :start  nil
:nemesis        :info   :start  &lt;span class="o"&gt;[&lt;/span&gt;:isolated &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"n5"&lt;/span&gt; &lt;span class="c"&gt;#{"n2" "n1" "n4" "n3"}, "n2" #{"n5"}, "n1" #{"n5"}, "n4" #{"n5"}, "n3" #{"n5"}}]&lt;/span&gt;
1       :invoke :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 4 2]]
1       :ok     :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 4 2]]
2       :invoke :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 5 nil]]
3       :invoke :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 1 2]]
2       :ok     :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 5 3]]
3       :ok     :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 1 2]]
0       :invoke :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 4 nil]]
0       :ok     :read   &lt;span class="o"&gt;[[&lt;/span&gt;:r 4 2]]
1       :invoke :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 6 4]]
1       :ok     :write  &lt;span class="o"&gt;[[&lt;/span&gt;:w 6 4]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上片段是截取的测试中一小部分不同的读写操作示例，&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/56643819/72118797-4b014080-338d-11ea-9cde-530fb2256775.png" title="" alt="format"&gt;&lt;/p&gt;

&lt;p&gt;其中&lt;strong&gt;最左边的数字是执行这次操作的 worker&lt;/strong&gt;，也就是&lt;strong&gt;进程号&lt;/strong&gt;。每发起一次操作，标志都是 invoke，接下来一列会指出是 write 还是 read 操作，而之后一列的中括号内，则显示了具体的操作，比如&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;:invoke :read &amp;nbsp; [[:r 1 nil]]&lt;/code&gt;就是读取 key 为 1 的值，因为是 invoke，操作刚刚开始，还不知道值是什么，所以后面是 nil。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:ok &amp;nbsp; &amp;nbsp; :read &amp;nbsp; [[:r 1 4]]&lt;/code&gt;&amp;nbsp;中的 ok 则表示操作成功，可以看到读取到键 1 对应的值是 4。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在这个片段中，还可以看到一次 nemesis 被注入的时刻。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;:nemesis &amp;nbsp; :info &amp;nbsp; :start &amp;nbsp;nil&lt;/code&gt;&amp;nbsp;标志着 nemesis 的开始，后面的的内容 &lt;code&gt;（:isolated ...）&lt;/code&gt;&amp;nbsp;表示了节点 n5 从整个集群中被隔离，无法与其他 DB 节点进行网络通信。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="cas-register"&gt;cas-register&lt;/h4&gt;
&lt;p&gt;这是一个验证&amp;nbsp;CAS&amp;nbsp;操作的寄存器。除了读写操作外，这次我们还加入了随机生成的&amp;nbsp;CAS&amp;nbsp;操作，cas-register&amp;nbsp;将会对结果进行线性分析。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0       :invoke :read       nil
0       :ok         :read       0
1       :invoke :cas        &lt;span class="o"&gt;[&lt;/span&gt;0 2]
1       :ok         :cas        &lt;span class="o"&gt;[&lt;/span&gt;0 2]
4       :invoke :read       nil
4       :ok         :read       2
0       :invoke :read       nil
0       :ok         :read       2
2       :invoke :write  0
2       :ok         :write  0
3       :invoke :cas        &lt;span class="o"&gt;[&lt;/span&gt;2 2]
:nemesis        :info       :start  nil
0       :invoke :read       nil
0       :ok         :read       0
1       :invoke :cas        &lt;span class="o"&gt;[&lt;/span&gt;1 3]
:nemesis        :info       :start  &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"n1"&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
3       :fail       :cas        &lt;span class="o"&gt;[&lt;/span&gt;2 2]
1       :fail       :cas        &lt;span class="o"&gt;[&lt;/span&gt;1 3]
4       :invoke :read       nil
4       :ok         :read       0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，在这次测试中，我们采用唯一的键值，比如所有写入和读取操作都是对键 "f" 执行，在显示上省略了中括号中的键，只显示是什么值。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;:invoke  :read &amp;nbsp;nil&lt;/code&gt;&amp;nbsp;表示开始一次读取“f”的值的操作，因为刚开始操作，所以结果是 nil（空）。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:ok &amp;nbsp;   :read &amp;nbsp;0&lt;/code&gt;&amp;nbsp;表示成功读取到了键“f”的值为 0。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:invoke  :cas &amp;nbsp;[1 2]&lt;/code&gt;&amp;nbsp;意思是进行 CAS 操作，当读到的值为 1 时，将值改为 2。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在第二行可以看到，当保存的 value 是 0 时，在第 4 行 &lt;code&gt;cas[0 2]&lt;/code&gt;&amp;nbsp;会将 value 变为 2。在第 14 行当值为 0 时，17 行的 cas[2 2] 就失败了。&lt;/p&gt;

&lt;p&gt;第 16 行显示了 n1 节点被杀掉的操作，第 17、18 行会有两个 cas 失败（fail）&lt;/p&gt;
&lt;h3 id="Jepsen 错误注入"&gt;Jepsen 错误注入&lt;/h3&gt;&lt;h4 id="kill-node"&gt;kill-node&lt;/h4&gt;
&lt;p&gt;Jepsen 的控制节点会在整个测试过程中，多次随机 kill 某一节点中的数据库服务而使服务停止。此时集群中就少了一个节点。然后在一定时间后再将该节点的数据库服务启动，使之重新加入集群。&lt;/p&gt;
&lt;h4 id="partition-random-node"&gt;partition-random-node&lt;/h4&gt;
&lt;p&gt;Jepsen 会在测试过程中，多次随机将某一节点与其他节点网络隔离，使该节点无法与其他节点通信，其他节点也无法和它通信。然后在一定时间后再恢复这一网络隔离，使集群恢复原状。&lt;/p&gt;
&lt;h4 id="partition-random-halves"&gt;partition-random-halves&lt;/h4&gt;
&lt;p&gt;在这种常见的网络分区情景下，Jepsen 控制节点会将 5 个 DB 节点随机分成两部分，一部分为两个节点，另一部分为三个。一定时间后恢复通信。如下图所示。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/56643819/72118867-869c0a80-338d-11ea-878d-15ea79811a8b.png" title="" alt="partition"&gt;&lt;/p&gt;
&lt;h2 id="测试结束后"&gt;测试结束后&lt;/h2&gt;
&lt;p&gt;Jepsen&amp;nbsp;会根据需求对测试结果进行分析，并得出本次测试的结果，可以看到控制台的输出，本次测试是通过的。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;2020-01-08 03:24:51,742&lt;span class="o"&gt;{&lt;/span&gt;GMT&lt;span class="o"&gt;}&lt;/span&gt;    INFO    &lt;span class="o"&gt;[&lt;/span&gt;jepsen &lt;span class="nb"&gt;test &lt;/span&gt;runner] jepsen.core: &lt;span class="o"&gt;{&lt;/span&gt;:timeline &lt;span class="o"&gt;{&lt;/span&gt;:valid? &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
 :linear
 &lt;span class="o"&gt;{&lt;/span&gt;:valid? &lt;span class="nb"&gt;true&lt;/span&gt;,
  :configs
  &lt;span class="o"&gt;({&lt;/span&gt;:model &lt;span class="o"&gt;{&lt;/span&gt;:value 0&lt;span class="o"&gt;}&lt;/span&gt;,
    :last-op
    &lt;span class="o"&gt;{&lt;/span&gt;:process 0,
     :type :ok,
     :f :write,
     :value 0,
     :index 597,
     :time 60143184600&lt;span class="o"&gt;}&lt;/span&gt;,
    :pending &lt;span class="o"&gt;[]})&lt;/span&gt;,
  :analyzer :linear,
  :final-paths &lt;span class="o"&gt;()}&lt;/span&gt;,
 :valid? &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;


Everything looks good! ヽ&lt;span class="o"&gt;(&lt;/span&gt;‘ー&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;ノ
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="自动生成的 timeline.html 文件"&gt;自动生成的 timeline.html 文件&lt;/h3&gt;
&lt;p&gt;Jepsen 在测试执行过程中会自动生成一个名为 timeline.html 文件，以下为本次实践生成的 timeline.html 文件部分截图&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/56643819/72118901-a9c6ba00-338d-11ea-9efa-045ed1914608.png" title="" alt="timeline"&gt;&lt;/p&gt;

&lt;p&gt;上面的图片展示了测试中执行操作的时间轴片段，每个执行块有对应的执行信息，Jepsen 会将整个时间轴生成一个 HTML 文件。&lt;/p&gt;

&lt;p&gt;Jepsen 就是这样按照顺序的历史操作记录进行&amp;nbsp;Linearizability&amp;nbsp;一致性验证，这也是&amp;nbsp;Jepsen&amp;nbsp;的核心。我们也可以通过这个 HTML 文件来帮助我们溯源错误。&lt;/p&gt;
&lt;h3 id="Jepsen 生成的性能分析图"&gt;Jepsen 生成的性能分析图&lt;/h3&gt;
&lt;p&gt;下面是一些&amp;nbsp;Jepsen&amp;nbsp;生成的性能分析图表，本次实践项目名为「basic-test」各位读者阅读时请自行脑补为你项目名。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/56643819/72118934-c5ca5b80-338d-11ea-9603-ca65a36baf1b.png" title="" alt="latency"&gt;&lt;/p&gt;

&lt;p&gt;可以看到，这一张图表展示了 Nebula Graph 的读写操作延时。其中上方灰色的区域是错误注入的时段，在本次测试我们注入了随机 kill node。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/56643819/72118989-e5fa1a80-338d-11ea-8f81-af98870621ca.png" title="" alt="rate"&gt;&lt;/p&gt;

&lt;p&gt;而在这一张图展示了读写操作的成功率，我们可以看出，最下方红色集中突出的地方为出现失败的地方，这是因为 control node 在杀死节点时终止了某个 partition 的 leader 中的 nebula 服务。集群此时需要重新选举，在选举出新的 leader 之后，读写操作也恢复到正常了。&lt;/p&gt;

&lt;p&gt;通过观察测试程序运行结果和分析图表，可以看到 Nebula Graph 完成了本次在单寄存器模型中注入 kill-node 错误的测试，读写操作延时也均处于正常范围。&lt;/p&gt;
&lt;h2 id="小结"&gt;小结&lt;/h2&gt;
&lt;p&gt;Jepsen 本身也存在一些不足，比如测试无法长时间运行，因为大量数据在校验阶段会造成 Out of Memory。&lt;/p&gt;

&lt;p&gt;但在实际场景中，许多 bug 需要长时间的压力测试、故障模拟才能发现，同时系统的稳定性也需要长时间的运行才能被验证。但与此同时，在使用 Jepsen 对 Nebula Graph 进行测试的过程中，我们也发现了一些之前没有遇到过的 Bug，甚至其中一些在使用中可能永远也不会出现。&lt;/p&gt;

&lt;p&gt;目前，我们已经在日常开发过程中使用 Jepsen 对 Nebula Graph 进行测试。Nebula Graph 有代码更新后，每晚都将编译好的项目发布在 Docker Hub 中，Nebula-Jepsen 将自动下拉最新的镜像进行持续测试。&lt;/p&gt;

&lt;p&gt;最后是 Nebula 的 GitHub 地址，欢迎大家试用，有什么问题可以向我们提 issue。GitHub 地址：&lt;a href="https://0x7.me/ruby2github" rel="nofollow" target="_blank" title=""&gt;https://github.com/vesoft-inc/nebula&lt;/a&gt;，加入 Nebula Graph 交流群，请联系 Nebula Graph 官方小助手微信号：NebulaGraphbot&lt;/p&gt;
&lt;h2 id="参考文献"&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Jepsen 主页：&lt;a href="https://jepsen.io/" rel="nofollow" target="_blank" title=""&gt;https://jepsen.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Jepsen GitHub:&lt;a href="https://github.com/jepsen-io/jepsen" rel="nofollow" target="_blank" title=""&gt;https://github.com/jepsen-io/jepsen&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="http://thesecretlivesofdata.com/raft/" rel="nofollow" target="_blank" title=""&gt;Raft Understandable Distributed Consensus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://raft.github.io/" rel="nofollow" target="_blank" title=""&gt;The Raft Consensus Algorithm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Nebula Graph GitHub:&lt;a href="https://0x7.me/ruby2github" rel="nofollow" target="_blank" title=""&gt;https://github.com/vesoft-inc/nebula&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>NebulaGraph</author>
      <pubDate>Fri, 10 Jan 2020 10:38:14 +0800</pubDate>
      <link>https://ruby-china.org/topics/39429</link>
      <guid>https://ruby-china.org/topics/39429</guid>
    </item>
    <item>
      <title>2019年 度中国测试行业问卷调研 (有奖问卷)</title>
      <description>&lt;h2 id="2019年度中国测试行业问卷调研（有奖问卷）"&gt;2019 年度中国测试行业问卷调研（有奖问卷）&lt;/h2&gt;&lt;h3 id="开始"&gt;开始&lt;/h3&gt;
&lt;p&gt;TesterHome 在 2018 年的时候，发起了一次全中国的软件测试行业的问卷调查，当时反响很不错，收集到了 2000 多的用户数据，通过这些数据我们看到了其实软件测试行业并不像我们想象的那样。我们突然很有必要把这个事情坚持下去，所以今年我们继续发起，希望能扩散出去。发在开发论坛是希望开发同学能帮忙转发给你们的测试同学。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jinshuju.net/f/1mVOLZ?background=white&amp;amp;banner=show&amp;amp;embedded=true" rel="nofollow" target="_blank" title=""&gt;2019 年度中国测试行业问卷调研（有奖问卷）&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="结束语"&gt;结束语&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;活动奖品会有变化，今年会有惊喜&lt;/li&gt;
&lt;li&gt;参与调研并留下联系邮箱的，会提前拿到问卷结果&lt;/li&gt;
&lt;li&gt;本次调研报告数据会严格保密&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;附： &lt;a href="https://testerhome.com/topics/18175" rel="nofollow" target="_blank" title=""&gt;&amp;gt; 去年的问卷调查报告&lt;/a&gt; 看看真实的软件测试人员的处境，比如薪水：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2019/4feb2da5-3630-449a-935f-671a1278a519.png!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>lihuazhang</author>
      <pubDate>Sat, 07 Dec 2019 08:40:48 +0800</pubDate>
      <link>https://ruby-china.org/topics/39307</link>
      <guid>https://ruby-china.org/topics/39307</guid>
    </item>
    <item>
      <title>RSpec 在 Rails 6 上有坑</title>
      <description>&lt;p&gt;我在 Rails6 上用 rspec 测试一个基础的 controller action 遇到如下错误：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Failures&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="no"&gt;HomeController&lt;/span&gt; &lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;
     &lt;span class="no"&gt;Failure&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="ss"&gt;:index&lt;/span&gt;

     &lt;span class="no"&gt;ActionView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Template&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="n"&gt;wrong&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="c1"&gt;# ./spec/controllers/home_controller_spec.rb:8:in `block (3 levels) in &amp;lt;top (required)&amp;gt;'&lt;/span&gt;
     &lt;span class="c1"&gt;# ------------------&lt;/span&gt;
     &lt;span class="c1"&gt;# --- Caused by: ---&lt;/span&gt;
     &lt;span class="c1"&gt;# ArgumentError:&lt;/span&gt;
     &lt;span class="c1"&gt;#   wrong number of arguments (given 2, expected 1)&lt;/span&gt;
     &lt;span class="c1"&gt;#   ./spec/controllers/home_controller_spec.rb:8:in `block (3 levels) in &amp;lt;top (required)&amp;gt;'&lt;/span&gt;

&lt;span class="no"&gt;Finished&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mf"&gt;0.04047&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="n"&gt;took&lt;/span&gt; &lt;span class="mf"&gt;7.62&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/home_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'home#index'&lt;/span&gt;

&lt;span class="c1"&gt;# spec/controllers/home_controller_spec.rb&lt;/span&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;HomeController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :controller&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;"GET index"&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;"response successfully"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="ss"&gt;:index&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&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;200&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;废了半天劲最后找到这篇文章：&lt;a href="https://cloud.tencent.com/developer/ask/211674" rel="nofollow" target="_blank" title=""&gt;https://cloud.tencent.com/developer/ask/211674&lt;/a&gt;
然后把 Gemfile 改成 &lt;code&gt;gem 'rspec-rails', github: 'rspec/rspec-rails', branch: '4-0-dev'&lt;/code&gt; 解决了问题。&lt;/p&gt;

&lt;p&gt;坑！&lt;/p&gt;</description>
      <author>SpiderEvgn</author>
      <pubDate>Tue, 27 Aug 2019 11:44:06 +0800</pubDate>
      <link>https://ruby-china.org/topics/38981</link>
      <guid>https://ruby-china.org/topics/38981</guid>
    </item>
    <item>
      <title>请问有人对接过讯飞的转写接口吗？</title>
      <description>&lt;p&gt;请问有人对接过讯飞的转写接口吗？有的能否发上参考一下
根据文档提交参数，都报&lt;code&gt;转写业务通用错误&lt;/code&gt;&lt;/p&gt;</description>
      <author>stephen</author>
      <pubDate>Fri, 02 Aug 2019 11:15:36 +0800</pubDate>
      <link>https://ruby-china.org/topics/38897</link>
      <guid>https://ruby-china.org/topics/38897</guid>
    </item>
    <item>
      <title>RSpec + FactoryBot 出现奇怪的问题</title>
      <description>&lt;p&gt;application.rb:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generators&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test_framework&lt;/span&gt; &lt;span class="ss"&gt;:rspec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="ss"&gt;fixtures: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="ss"&gt;view_specs: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="ss"&gt;helper_specs: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="ss"&gt;routing_specs: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="ss"&gt;controller_specs: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="ss"&gt;request_specs: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;factory_bot&lt;/span&gt; &lt;span class="ss"&gt;dir: &lt;/span&gt;&lt;span class="s1"&gt;'spec/factories'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gemfile:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;group :test, :development do
  gem "rspec-rails"
  gem 'factory_bot_rails'
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 spec 下文件的样式:
&lt;img title="" alt="cb6568225b10a02718a645dff7631da2.png"&gt;&lt;/p&gt;

&lt;p&gt;spec/factories/users.rb&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FactoryBot.define do
  100.times do |i|
    factory :user, class: "User" do
      login "test001"
      uid i
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;spec_helper.rb&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This file was generated by the `rails generate rspec:install` command. Conventionally, all&lt;/span&gt;
&lt;span class="c1"&gt;# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.&lt;/span&gt;
&lt;span class="c1"&gt;# The generated `.rspec` file contains `--require spec_helper` which will cause&lt;/span&gt;
&lt;span class="c1"&gt;# this file to always be loaded, without a need to explicitly require it in any&lt;/span&gt;
&lt;span class="c1"&gt;# files.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Given that it is always loaded, you are encouraged to keep this file as&lt;/span&gt;
&lt;span class="c1"&gt;# light-weight as possible. Requiring heavyweight dependencies from this file&lt;/span&gt;
&lt;span class="c1"&gt;# will add to the boot time of your test suite on EVERY test run, even for an&lt;/span&gt;
&lt;span class="c1"&gt;# individual file that may not need all of that loaded. Instead, consider making&lt;/span&gt;
&lt;span class="c1"&gt;# a separate helper file that requires the additional dependencies and performs&lt;/span&gt;
&lt;span class="c1"&gt;# the additional setup, and require it from the spec files that actually need&lt;/span&gt;
&lt;span class="c1"&gt;# it.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration&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;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# rspec-expectations config goes here. You can use an alternate&lt;/span&gt;
  &lt;span class="c1"&gt;# assertion/expectation library such as wrong or the stdlib/minitest&lt;/span&gt;
  &lt;span class="c1"&gt;# assertions if you prefer.&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect_with&lt;/span&gt; &lt;span class="ss"&gt;:rspec&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;expectations&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# This option will default to `true` in RSpec 4. It makes the `description`&lt;/span&gt;
    &lt;span class="c1"&gt;# and `failure_message` of custom matchers include text for helper methods&lt;/span&gt;
    &lt;span class="c1"&gt;# defined using `chain`, e.g.:&lt;/span&gt;
    &lt;span class="c1"&gt;#     be_bigger_than(2).and_smaller_than(4).description&lt;/span&gt;
    &lt;span class="c1"&gt;#     # =&amp;gt; "be bigger than 2 and smaller than 4"&lt;/span&gt;
    &lt;span class="c1"&gt;# ...rather than:&lt;/span&gt;
    &lt;span class="c1"&gt;#     # =&amp;gt; "be bigger than 2"&lt;/span&gt;
    &lt;span class="n"&gt;expectations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include_chain_clauses_in_custom_matcher_descriptions&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;end&lt;/span&gt;

  &lt;span class="c1"&gt;# rspec-mocks config goes here. You can use an alternate test double&lt;/span&gt;
  &lt;span class="c1"&gt;# library (such as bogus or mocha) by changing the `mock_with` option here.&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock_with&lt;/span&gt; &lt;span class="ss"&gt;:rspec&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;mocks&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# Prevents you from mocking or stubbing a method that does not exist on&lt;/span&gt;
    &lt;span class="c1"&gt;# a real object. This is generally recommended, and will default to&lt;/span&gt;
    &lt;span class="c1"&gt;# `true` in RSpec 4.&lt;/span&gt;
    &lt;span class="n"&gt;mocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify_partial_doubles&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;end&lt;/span&gt;

  &lt;span class="c1"&gt;# This option will default to `:apply_to_host_groups` in RSpec 4 (and will&lt;/span&gt;
  &lt;span class="c1"&gt;# have no way to turn it off -- the option exists only for backwards&lt;/span&gt;
  &lt;span class="c1"&gt;# compatibility in RSpec 3). It causes shared context metadata to be&lt;/span&gt;
  &lt;span class="c1"&gt;# inherited by the metadata hash of host groups and examples, rather than&lt;/span&gt;
  &lt;span class="c1"&gt;# triggering implicit auto-inclusion in groups with matching metadata.&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shared_context_metadata_behavior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:apply_to_host_groups&lt;/span&gt;

&lt;span class="c1"&gt;# The settings below are suggested to provide a good initial experience&lt;/span&gt;
&lt;span class="c1"&gt;# with RSpec, but feel free to customize to your heart's content.&lt;/span&gt;
&lt;span class="cm"&gt;=begin
  # This allows you to limit a spec run to individual examples or groups
  # you care about by tagging them with `:focus` metadata. When nothing
  # is tagged with `:focus`, all examples get run. RSpec also provides
  # aliases for `it`, `describe`, and `context` that include `:focus`
  # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
  config.filter_run_when_matching :focus

  # Allows RSpec to persist some state between runs in order to support
  # the `--only-failures` and `--next-failure` CLI options. We recommend
  # you configure your source control system to ignore this file.
  config.example_status_persistence_file_path = "spec/examples.txt"

  # Limits the available syntax to the non-monkey patched syntax that is
  # recommended. For more details, see:
  #   - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
  #   - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
  #   - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
  config.disable_monkey_patching!

  # Many RSpec users commonly either run the entire suite or an individual
  # file, and it's useful to allow more verbose output when running an
  # individual spec file.
  if config.files_to_run.one?
    # Use the documentation formatter for detailed output,
    # unless a formatter has already been configured
    # (e.g. via a command-line flag).
    config.default_formatter = "doc"
  end

  # Print the 10 slowest examples and example groups at the
  # end of the spec run, to help surface which specs are running
  # particularly slow.
  config.profile_examples = 10

  # Run specs in random order to surface order dependencies. If you find an
  # order dependency and want to debug it, you can fix the order by providing
  # the seed, which is printed after each run.
  #     --seed 1234
  config.order = :random

  # Seed global randomization in this process using the `--seed` CLI option.
  # Setting this allows you to use `--seed` to deterministically reproduce
  # test failures related to randomization by passing the same `--seed` value
  # as the one that triggered the failure.
  Kernel.srand config.seed
=end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rails_helper:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This file is copied to spec/ when you run 'rails generate rspec:install'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'spec_helper'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'support/factory'&lt;/span&gt;
&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'RAILS_ENV'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'../../config/environment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Prevent database truncation if the environment is production&lt;/span&gt;
&lt;span class="nb"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"The Rails environment is running in production mode!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;production?&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rspec/rails'&lt;/span&gt;
&lt;span class="c1"&gt;# Add additional requires below this line. Rails is not loaded until this point!&lt;/span&gt;

&lt;span class="c1"&gt;# Requires supporting ruby files with custom matchers and macros, etc, in&lt;/span&gt;
&lt;span class="c1"&gt;# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are&lt;/span&gt;
&lt;span class="c1"&gt;# run as spec files by default. This means that files in spec/support that end&lt;/span&gt;
&lt;span class="c1"&gt;# in _spec.rb will both be required and run as specs, causing the specs to be&lt;/span&gt;
&lt;span class="c1"&gt;# run twice. It is recommended that you do not name files matching this glob to&lt;/span&gt;
&lt;span class="c1"&gt;# end with _spec.rb. You can configure this pattern with the --pattern&lt;/span&gt;
&lt;span class="c1"&gt;# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# The following line is provided for convenience purposes. It has the downside&lt;/span&gt;
&lt;span class="c1"&gt;# of increasing the boot-up time by auto-requiring all files in the support&lt;/span&gt;
&lt;span class="c1"&gt;# directory. Alternatively, in the individual `*_spec.rb` files, manually&lt;/span&gt;
&lt;span class="c1"&gt;# require only the support files necessary.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }&lt;/span&gt;

&lt;span class="c1"&gt;# Checks for pending migrations and applies them before tests are run.&lt;/span&gt;
&lt;span class="c1"&gt;# If you are not using ActiveRecord, you can remove these lines.&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
  &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maintain_test_schema!&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PendingMigrationError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
  &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&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;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fixture_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/spec/fixtures"&lt;/span&gt;

  &lt;span class="c1"&gt;# If you're not using ActiveRecord, or you'd prefer not to run each of your&lt;/span&gt;
  &lt;span class="c1"&gt;# examples within a transaction, remove the following line or assign false&lt;/span&gt;
  &lt;span class="c1"&gt;# instead of true.&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use_transactional_fixtures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# RSpec Rails can automatically mix in different behaviours to your tests&lt;/span&gt;
  &lt;span class="c1"&gt;# based on their file location, for example enabling you to call `get` and&lt;/span&gt;
  &lt;span class="c1"&gt;# `post` in specs under `spec/controllers`.&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# You can disable this behaviour by removing the line below, and instead&lt;/span&gt;
  &lt;span class="c1"&gt;# explicitly tag your specs with their type, e.g.:&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;#     RSpec.describe UsersController, :type =&amp;gt; :controller do&lt;/span&gt;
  &lt;span class="c1"&gt;#       # ...&lt;/span&gt;
  &lt;span class="c1"&gt;#     end&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# The different available types are documented in the features, such as in&lt;/span&gt;
  &lt;span class="c1"&gt;# https://relishapp.com/rspec/rspec-rails/docs&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;infer_spec_type_from_file_location!&lt;/span&gt;

  &lt;span class="c1"&gt;# Filter lines from Rails gems in backtraces.&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_rails_from_backtrace!&lt;/span&gt;
  &lt;span class="c1"&gt;# arbitrary gems may also be filtered via:&lt;/span&gt;
  &lt;span class="c1"&gt;# config.filter_gems_from_backtrace("gem name")&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切看起来没什么问题啊，但是就是会一个奇怪的错误，在运行 rspec 命令之后：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An error occurred while loading ./spec/models/apply_spec.rb.
Failure/Error: login 'test001'

NoMethodError:
  undefined method 'login' in 'user' factory
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/factory_bot-5.0.2/lib/factory_bot/definition_proxy.rb:97:in `method_missing'
# ./spec/factories/users.rb:3:in `block (2 levels) in &amp;lt;top (required)&amp;gt;'
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:18:in `instance_eval'
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:18:in `factory'
# ./spec/factories/users.rb:2:in `block in &amp;lt;top (required)&amp;gt;'
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:49:in `instance_eval'
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:49:in `run'
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/factory_bot-5.0.2/lib/factory_bot/syntax/default.rb:7:in `define'
# ./spec/factories/users.rb:1:in `&amp;lt;top (required)&amp;gt;'
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/activesupport-5.1.7/lib/active_support/dependencies.rb:286:in `load'
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/activesupport-5.1.7/lib/active_support/dependencies.rb:286:in `block in load'
# /Users/a123/.rvm/gems/ruby-2.4.1@oa_saas/gems/activesupport-5.1.7/lib/active_support/dependencies.rb:258:in `load_dependen
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试很多办法去修改配置了还是不行，实在没办法了&lt;/p&gt;</description>
      <author>LongLonghaoran</author>
      <pubDate>Wed, 22 May 2019 22:57:10 +0800</pubDate>
      <link>https://ruby-china.org/topics/38564</link>
      <guid>https://ruby-china.org/topics/38564</guid>
    </item>
    <item>
      <title>关于 RSpec：betterspecs</title>
      <description>&lt;blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;RSpec 是一个非常棒的工具。它在 BDD 流程的开发中被用来写高可读性的测试，引导并验证你开发的应用程序。网上有很多资源告诉你 RSpec 能“做什么”，但却很少有人讨论如何用它编写出高质量的测试用例。Better Specs 通过收集其他开发者经年累月积攒的绝大部分“最佳实践”来尝试着来填补这之间的鸿沟。&lt;/p&gt;

&lt;p&gt;&lt;a href="http://www.betterspecs.org/zh_cn/" rel="nofollow" target="_blank"&gt;http://www.betterspecs.org/zh_cn/&lt;/a&gt;&lt;/p&gt;</description>
      <author>chenge</author>
      <pubDate>Mon, 29 Apr 2019 04:59:47 +0800</pubDate>
      <link>https://ruby-china.org/topics/38462</link>
      <guid>https://ruby-china.org/topics/38462</guid>
    </item>
    <item>
      <title>想学 Cucumber, 付费求个老师。</title>
      <description>&lt;p&gt;想用 Cucumber 来给项目做集成测试，但是目前团队里面没有人对这个特别熟悉。
有没有对 Cucumber 比较擅长的同学，希望能够跟着学一下，可以远程。
不需要特别详细的上课那种教学，主要是遇到一些疑问和不太懂的地方能够有人可以问。
目前团队的主要开发语言就是 Ruby，所以不用太担心基础问题。
看有没有熟悉 Cucumber 的同学能聊一下？&lt;/p&gt;</description>
      <author>10000</author>
      <pubDate>Fri, 22 Mar 2019 15:37:50 +0800</pubDate>
      <link>https://ruby-china.org/topics/38274</link>
      <guid>https://ruby-china.org/topics/38274</guid>
    </item>
    <item>
      <title>单元测试之基本构成</title>
      <description>&lt;p&gt;在前后端分离大趋势的今天，通过模块的方式来管理代码似乎比以前任何时候都容易。组件都是由 JavaScript 编写，且组件本身就是一个状态机，这为我们编写测试带来了不少便利性。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2018/a2de8ae1-f784-4548-9a1b-4b32054f5be3.png!large" title="" alt="Unit testing"&gt;&lt;/p&gt;

&lt;p&gt;然而，在如此好的环境下&lt;strong&gt;测试&lt;/strong&gt;似乎依然得不到许多前端人员的重视（包括我）。说起来也是，即便组件化已经深入人心，但实现组件化的方式却多种多样。React, Vue, Angular 这些框架各有各的哲学思想，时间都花在折腾这些工具上了，好好写测试渐渐成了奢望。为此这篇文章我希望能从琳琅满目的前端工具中脱离出来，简单地阐述一些，关于单元测试最基础，或者说稍微本质性的东西。&lt;/p&gt;
&lt;h2 id="1. 关于单元测试"&gt;1. 关于单元测试&lt;/h2&gt;
&lt;p&gt;个人以为，无论一个单元测试有多么复杂，它本质上应该可以划分为下面这些部件&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;测试声明&lt;/li&gt;
&lt;li&gt;测试断言&lt;/li&gt;
&lt;li&gt;测试运行者&lt;/li&gt;
&lt;li&gt;仿真 (可能会有)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;每一个部件都有代表性的程序库，不同的社区可能有不同的选择 (不限于 JS 社区)，但个人觉得他们之间区别并不是很大。我们觉得测试复杂，很大一部分原因脚手架导致的，为了把测试集成到项目开发流程中除了需要安装相关的测试依赖库以外还需要搭配&lt;code&gt;Webpack&lt;/code&gt;，当然可能也会包括较为流行的前端框架&lt;code&gt;React&lt;/code&gt;, &lt;code&gt;Vue&lt;/code&gt;等等。&lt;/p&gt;

&lt;p&gt;很多时候会造成一种现象就是&lt;code&gt;package.json&lt;/code&gt;里面的依赖包，真正生产环境中会使用的只不过有 1-3 个，然而开发人员所要用到的单单用于测试的依赖包就有十几二十个，怎能不让人生畏？为了排除这些干扰，我只在 Node 平台上面来介绍这些单元测试的基本部件。&lt;/p&gt;
&lt;h2 id="2. 单元测试基本构成"&gt;2. 单元测试基本构成&lt;/h2&gt;&lt;h4 id="1) 测试框架Mocha"&gt;1) 测试框架 Mocha&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://mochajs.org" rel="nofollow" target="_blank" title=""&gt;Mocha&lt;/a&gt;是目前 JS 开源社区用得比较多的一个测试框架，在代码组织层面上它充当了我前面所说的&lt;code&gt;测试声明&lt;/code&gt;的角色，我们可以用它所提供的 DSL 组织测试代码。另外它也包含命令行工具，在 Node 平台上可以执行相关的命令来运行已经定义好的测试。下面我编写一个简单的函数并测试它 (原则上我应该先写测试再写函数)。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/handle.js&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleByCallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个很简单的函数，通过传入回调函数来处理相关的字符串参数，并返回结果。Mocha 如何安装我这里就不多说了，下面是我写的简单的测试文件&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// test/handle.spec.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../src/handle.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test handle module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handle string by callback method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callCount&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;called&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="nx"&gt;assert&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="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hellohello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;called&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callCount&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="nx"&gt;assert&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="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WorldWorld&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;called&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callCount&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这测试似乎有点长，试着运行一下 Mocha 的命令行工具并指定对应的测试文件，看看这杯摩卡好不好喝。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-gold-cdn.xitu.io/2018/12/2/1676ddec6e8ed701?w=1030&amp;amp;h=382&amp;amp;f=png&amp;amp;s=41719" title="" alt="One"&gt;&lt;/p&gt;

&lt;p&gt;看到绿了我就放心了。但是为了写个测试我们还得费心去定义一个函数，这使得我们的测试代码有点长了。耐心看下去，接下来你会知道怎么去优化它。&lt;/p&gt;
&lt;h4 id="2) 测试辅助工具Sinon"&gt;2) 测试辅助工具 Sinon&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://sinonjs.org/" rel="nofollow" target="_blank" title=""&gt;Sinon.js&lt;/a&gt;是我比较喜欢的一个测试辅助工具。我可以用它来创建仿真函数，或者 API 的请求，加快测试编写的进程，使得测试代码更为精炼且可读性更高。接下来我就用这个函数库来优化上面所编写的测试代码。&lt;/p&gt;

&lt;p&gt;上面的例子中，我自己创建了一个回调函数，并且为函数设定了相关属性。测试完结之后将会确认两个事情&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;配合回调函数所得到的结果是否符合预期。&lt;/li&gt;
&lt;li&gt;回调函数是否被调用，以及调用了多少次。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;细想一下如果每次我们都要手动地去定义回调函数及其相关属性的话代码将会越来越长，测试也将越发麻烦。这种时候我们可能会考虑把它封装成一个工厂函数，自动帮我们生成这类函数。毕竟比起内部逻辑我们更关心回调函数的返回值不是吗？这其实就是一种仿真的手段，Sinon 很好地协助我们做好了这个事情，下面是我利用 Sinon 优化过的测试代码&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sinon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sinon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test handle module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handle string by callback method using sinon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sinon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 仿真一个总是返回'Hello World'的函数&lt;/span&gt;

    &lt;span class="nx"&gt;assert&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="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;called&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callCount&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="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastArg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;assert&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="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;good job&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;called&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callCount&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="nx"&gt;assert&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="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastArg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;good job&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码最值得关注的地方在于，只需要&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sinon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就能够仿真出一个总会返回"Hello World"的回调函数，作为一个测试的辅助函数，足矣。&lt;/p&gt;

&lt;p&gt;除此之外，仿真函数里面会包含许多可用的属性，具体可参考&lt;a href="https://sinonjs.org/releases/v7.1.1/fakes/" rel="nofollow" target="_blank" title=""&gt;文档&lt;/a&gt;。我这里只列举了几个对于当前测试比较有意义的属性&lt;code&gt;called&lt;/code&gt;-记录函数是否被调用，&lt;code&gt;callCount&lt;/code&gt;-函数被调用的次数，&lt;code&gt;lastArg&lt;/code&gt;-调用函数的最后一个参数，测试效果如下&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-gold-cdn.xitu.io/2018/12/2/1676ddfd38345fa9?w=1028&amp;amp;h=418&amp;amp;f=png&amp;amp;s=49809" title="" alt="Two"&gt;&lt;/p&gt;

&lt;p&gt;当然这只是比较简单的场景，Sinon 的能力还远不止如此。我觉得仿真是测试里面的难点，毕竟并不是所有场景都如同上述例子那般简单粗暴，这方面我自己也在慢慢克服着，与君共勉。&lt;/p&gt;
&lt;h4 id="3) 更丰富的断言Chai"&gt;3) 更丰富的断言 Chai&lt;/h4&gt;
&lt;p&gt;Node.js 本身就有断言库，就是我上文引入的&lt;code&gt;assert&lt;/code&gt;。然而很多时候我们的测试代码并不是在 Node 端运行，而是要把相关的代码加载到对应的浏览器中，如 Chrome，Firefox 等等。这种时候就得借助第三方库了。这里我简单介绍一下&lt;a href="https://www.chaijs.com/" rel="nofollow" target="_blank" title=""&gt;Chai&lt;/a&gt;断言库，它的的断言语句十分丰富，下面我用简单的&lt;code&gt;expect&lt;/code&gt;语句来重写上面的逻辑&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chai&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test handle module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handle string by callback method using sinon and chai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sinon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;to&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;called&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastArg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleByCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;good job&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;to&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;called&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&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;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastArg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;good job&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行&lt;code&gt;node_modules/mocha/bin/mocha test/handle.spec.js&lt;/code&gt;命令，结果如下&lt;/p&gt;

&lt;p&gt;&lt;img src="https://user-gold-cdn.xitu.io/2018/12/2/1676de072dcdf99e?w=1024&amp;amp;h=430&amp;amp;f=png&amp;amp;s=57700" title="" alt="Three"&gt;&lt;/p&gt;

&lt;p&gt;测试效果跟之前一样。从语法上来看使用了 Chai 之后断言语句似乎有了点 Ruby 范儿。最后来我们聊聊测试 Runner。&lt;/p&gt;
&lt;h4 id="4) 测试Runner-Karma"&gt;4) 测试 Runner-Karma&lt;/h4&gt;
&lt;p&gt;测试 Runner 顾名思义就是测试的运行者，上面的例子中每次我们都是通过 Mocha 的命令行程序来运行相应的测试程序，其中 Mocha 就充当了测试 Runner 的角色，然而正式的业务中测试可能会分散到多个不同的目录下，我们可能会在测试或者开发文件中运用较新的 JS 语法，或者是相关的框架的 DSL，为了使这堆代码能够在浏览器端运行就少不了预编译。&lt;/p&gt;

&lt;p&gt;前面的例子都是用 Node 来写，相对比较简单且容易理解，但是前端测试注定要复杂的多，我所理解的前端测试对于 Runner 有如下要求&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;编译代码 (包括测试代码和源文件代码)。&lt;/li&gt;
&lt;li&gt;识别相关的测试目录。&lt;/li&gt;
&lt;li&gt;批量运行测试。&lt;/li&gt;
&lt;li&gt;可以根据需求安装相关的插件。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这看起来似乎有点难，但 JS 社区中有一个叫&lt;a href="https://karma-runner.github.io" rel="nofollow" target="_blank" title=""&gt;Karma&lt;/a&gt;的框架能够大大简化上述工作。它可以简单地与 Webpack 结合，并利用已有的 Webpack 配置来编译我们的代码，只需要简单的配置就可以识别并运行相关的测试。它还能以服务的方式运行，在开发过程中监测文件的改动并重新运行测试。由于篇幅有限就不对它的配置进行更多说明了，有具体需求再去查看文档即可。&lt;/p&gt;

&lt;p&gt;PS: 虽说 Angular 最近似乎不怎么受待见，但可别因为 Karma 是 Angular 团队出的就对它视而不见啊。&lt;/p&gt;
&lt;h2 id="3. Question &amp;amp; Answer"&gt;3. Question &amp;amp; Answer&lt;/h2&gt;
&lt;p&gt;Q: 为什么没有 Webpack？&lt;/p&gt;

&lt;p&gt;A: 说实话确实也计划过在文章里面添加这样一个东西，后来写着写着还是放弃了。Webpack 有丰富的插件系统，确实在某种程度上给予我们开发人员一定的便利性。但是个人觉得它是使得我们如今前端领域变得如此混乱的“罪魁祸首”。单从语法层面来说，Webpack 有点像是 Lisp 系语言中的宏，我们可以定制任何语法，但是在前端领域中这种“宏”却被无节制地使用着，不同的开发人员就能定制出不同的类 JS 语法，为了排除这种干扰，我决定直接采用了 Node 环境下最为“原生”的 JS 写法。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;Q: 为什么没有 Webpack 跟 Karma 的集成的相关代码示例子？&lt;/p&gt;

&lt;p&gt;A: Karma 本身的配置并不是很复杂，它只是一个测试的 Runner，预编译功能可以依赖 Webpack 来完成，加入一个叫做&lt;a href="https://github.com/webpack-contrib/karma-webpack" rel="nofollow" target="_blank" title=""&gt;karma-webpack&lt;/a&gt;作为他们之间的桥梁即可。贴相关的代码会导致篇幅过长，且本文重点并不是“配置”。&lt;/p&gt;
&lt;h2 id="4. 尾声"&gt;4. 尾声&lt;/h2&gt;
&lt;p&gt;这篇文章主要简单介绍了一些单元测试的基本部件，每个部件中我都列举了 JS 社区中较为常用的对应的软件库。或许他们会是比较好的选择但却并不是唯一的选择。比如测试框架我们还可以选择&lt;a href="https://jasmine.github.io/" rel="nofollow" target="_blank" title=""&gt;Jasmine&lt;/a&gt;，断言库我们可以选择&lt;a href="https://github.com/Automattic/expect.js/" rel="nofollow" target="_blank" title=""&gt;expect.js&lt;/a&gt;。至于选择什么纯粹是个人喜好的问题，在我看来区别并不是很大。&lt;/p&gt;
&lt;h2 id="Happy Coding and Writing!!"&gt;Happy Coding and Writing!!&lt;/h2&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Sun, 02 Dec 2018 16:04:26 +0800</pubDate>
      <link>https://ruby-china.org/topics/37846</link>
      <guid>https://ruby-china.org/topics/37846</guid>
    </item>
    <item>
      <title>Goreplay - 使用真实流量测试你的应用</title>
      <description>&lt;p&gt;场景描述：&lt;/p&gt;

&lt;p&gt;最近项目准备升级，其中一个步骤就是需要删除一些不再维护的 gem，这样就会涉及大量代码的修改，除了增加测试覆盖率以外，最好能使用线上真实的流量来访问测试环境，然后通过 newrelic 更加详尽的捕捉潜在的错误。&lt;/p&gt;

&lt;p&gt;那这里就涉及到流量分流或者流量复制的问题，而 goreplay 便是解决该问题的一个优秀的工具。
顾名思义，goreplay 是基于 go 语言实现的，要在生产服务器上安装 go 环境。&lt;/p&gt;

&lt;p&gt;安装参考：&lt;a href="https://golang.org/doc/install" rel="nofollow" target="_blank"&gt;https://golang.org/doc/install&lt;/a&gt;，&lt;/p&gt;

&lt;p&gt;准备好 go 语言环境后，goreplay 直接提供了编译好的版本，十分方便，直接解压即可，可参考以下步骤：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 请自行安装最新版本&lt;/span&gt;
wget https://github.com/buger/goreplay/releases/download/v0.16.1/gor_0.16.1_x64.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xvf gor_0.16.1_x64.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是 goreplay 官方的图例，简单来讲就是 goreplay 捕捉线上流量，并将捕捉到的流量释放到测试服务器上。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2018/ed1766d0-4311-490f-b2c9-2909bbb12fbe.png!large" title="" alt="image"&gt;&lt;/p&gt;

&lt;h5&gt;Goreplay 基本用法&lt;/h5&gt;

&lt;p&gt;注：本文中使用 sudo 权限执行，如需要权限配置，参考:
&lt;a href="https://github.com/buger/goreplay/wiki/Running-as-non-root-user" rel="nofollow" target="_blank"&gt;https://github.com/buger/goreplay/wiki/Running-as-non-root-user&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;捕捉流量并通过终端输出 (调试)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./goreplay &lt;span class="nt"&gt;--input-raw&lt;/span&gt; :8000 &lt;span class="nt"&gt;--output-stdout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述命令将监控 8000 端口上所有的流量，并通过终端 stdout 输出。你可以通过浏览器或者 curl 访问 8000 端口，然后在终端查看 gor 输出所有的 http 请求。&lt;/p&gt;

&lt;p&gt;2.捕捉流量并实时同步到另一台机器&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./goreplay &lt;span class="nt"&gt;--input-raw&lt;/span&gt; :8000 &lt;span class="nt"&gt;--output-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://example:8001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述命令将 8000 端口的流量实时同步访问&lt;a href="http://example:8001" rel="nofollow" target="_blank"&gt;http://example:8001&lt;/a&gt;服务器，你在访问第一台服务器时，将看到流量以相同的顺序请求到第二台。&lt;/p&gt;

&lt;p&gt;3.将捕捉流量保存到文件中，然后释放到其它机器
有时候实时同步流量是很难做到的，所以 Goreplay 提供了这种先保存后释放的模式：
第一步，通过--output-file 保存流量：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./goreplay &lt;span class="nt"&gt;--input-raw&lt;/span&gt; :8000 &lt;span class="nt"&gt;--output-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;requests.gor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述命令将 8000 端口的流量，保存到 requests.gor 文件中 (必须是.gor 后缀，其它后缀经测释放时有问题)。&lt;/p&gt;

&lt;p&gt;第二步，释放保存的流量：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./goreplay &lt;span class="nt"&gt;--input-file&lt;/span&gt; requests.gor &lt;span class="nt"&gt;--output-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述命令将释放所有保存在 requests.gor 中的请求通过相同的时间顺序释放到服务器&lt;a href="http://localhost:8001" rel="nofollow" target="_blank"&gt;http://localhost:8001&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;参数解释：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--input-raw&lt;/span&gt;   &lt;span class="c"&gt;#用来捕捉http流量，需要指定ip地址和端口&lt;/span&gt;
&lt;span class="nt"&gt;--input-file&lt;/span&gt;   &lt;span class="c"&gt;#接收通过--output-file保存流量的文件&lt;/span&gt;
&lt;span class="nt"&gt;--input-tcp&lt;/span&gt; &lt;span class="c"&gt;#将多个 Goreplay 实例获取的流量聚集到一个 Goreplay 实例&lt;/span&gt;
&lt;span class="nt"&gt;--output-stdout&lt;/span&gt;  &lt;span class="c"&gt;#终端输出&lt;/span&gt;
&lt;span class="nt"&gt;--output-tcp&lt;/span&gt; &lt;span class="c"&gt;#将获取的流量转移至另外的 Goreplay 实例&lt;/span&gt;
&lt;span class="nt"&gt;--output-http&lt;/span&gt;  &lt;span class="c"&gt;#流量释放的对象server，需要指定ip地址和端口&lt;/span&gt;
&lt;span class="nt"&gt;--output-file&lt;/span&gt;   &lt;span class="c"&gt;#录制流量时指定的存储文件&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Goreplay 的限速机制和请求过滤&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;限速机制:
由于生产服务器配置一般远高于测试服务器配置，所以直接将生产服务器全部流量同步到测试服务器是不可行的，goreplay 提供了两种策略：&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;a. 限制每秒的请求数&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./goreplay  &lt;span class="nt"&gt;--input-tcp&lt;/span&gt; :28020 &lt;span class="nt"&gt;--output-http&lt;/span&gt; &lt;span class="s2"&gt;"http://staging.com|10"&lt;/span&gt;&lt;span class="c"&gt;# (每秒请求数限制10个以内)&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./goreplay  &lt;span class="nt"&gt;--input-raw&lt;/span&gt; :80 &lt;span class="nt"&gt;--output-tcp&lt;/span&gt; &lt;span class="s2"&gt;"replay.local:28020|10%"&lt;/span&gt;  &lt;span class="c"&gt;# (每秒请求数限制10%以内)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;b. 基于 Header 或 Url 的参数限制一些请求，为指定的 header 或者 url 的请求设定限制的百分比。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./goreplay  &lt;span class="nt"&gt;--input-raw&lt;/span&gt; :80 &lt;span class="nt"&gt;--output-tcp&lt;/span&gt; &lt;span class="s2"&gt;"replay.local:28020|10%"&lt;/span&gt; &lt;span class="nt"&gt;--http-header-limiter&lt;/span&gt; &lt;span class="s2"&gt;"X-API-KEY: 10%"&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./goreplay  &lt;span class="nt"&gt;--input-raw&lt;/span&gt; :80 &lt;span class="nt"&gt;--output-tcp&lt;/span&gt; &lt;span class="s2"&gt;"replay.local:28020|10%"&lt;/span&gt; &lt;span class="nt"&gt;--http-param-limiter&lt;/span&gt; &lt;span class="s2"&gt;"api_key: 10%"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;请求过滤:
当你需要捕捉指定路径的请求流量时，可以使用该机制，如只同步/api 路径下的请求
&lt;code&gt;bash
sudo ./goreplay --input-raw :8080 --output-http staging.com --http-allow-url /api
&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;另外还有其它一些参数用法：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--http-disallow-url&lt;/span&gt;    &lt;span class="c"&gt;#不允许正则匹配的Url&lt;/span&gt;
&lt;span class="nt"&gt;--http-allow-header&lt;/span&gt; &lt;span class="c"&gt;#允许的 Header 头&lt;/span&gt;
&lt;span class="nt"&gt;--http-disallow-header&lt;/span&gt; &lt;span class="c"&gt;#不允许的 Header 头&lt;/span&gt;
&lt;span class="nt"&gt;--http-allow-method&lt;/span&gt; &lt;span class="c"&gt;#允许的请求方法，传入值为GET, POST, OPTIONS等&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更多参考官方文档：&lt;a href="https://github.com/buger/goreplay/wiki/Getting-Started" rel="nofollow" target="_blank"&gt;https://github.com/buger/goreplay/wiki/Getting-Started&lt;/a&gt;&lt;/p&gt;</description>
      <author>xiaocui</author>
      <pubDate>Tue, 13 Nov 2018 15:17:06 +0800</pubDate>
      <link>https://ruby-china.org/topics/37756</link>
      <guid>https://ruby-china.org/topics/37756</guid>
    </item>
    <item>
      <title>通过测试代码, 自动生成 postman 文件</title>
      <description>&lt;h2 id="0 最终成果预览"&gt;0 最终成果预览&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2018/0bc1af85-56e5-4d95-b0f9-1c6b9152241b.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="1 实现思路"&gt;1 实现思路&lt;/h2&gt;
&lt;p&gt;自动化接口测试的同时，通过打日志的形式构建 postman 格式的文本文件&lt;/p&gt;
&lt;h2 id="2. 编写rake任务"&gt;2. 编写 rake 任务&lt;/h2&gt;&lt;h3 id="lib/postman.rake"&gt;lib/postman.rake&lt;/h3&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:postman&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;admin: :environment&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;postman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'doc/admin_postman.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:admin_postman&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="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;postman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;
    {
      "variables": [],
      "info": {
        "name": "&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;",
        "_postman_id": "bce94da3-8aa9-c38e-bdb5-fe8835715887",
        "description": "",
        "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
      },
      "item": [
&lt;/span&gt;&lt;span class="no"&gt;  JSON&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;File&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="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;File&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="n"&gt;doc&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;def&lt;/span&gt; &lt;span class="nf"&gt;admin_postman&lt;/span&gt;
  &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'GENERATE_POSTMAN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;
  &lt;span class="n"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'test/controllers/admin/'&lt;/span&gt;
  &lt;span class="n"&gt;order_controllers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[
    sessions_controller_test.rb
    managers_controller_test.rb
  ]&lt;/span&gt;
  &lt;span class="n"&gt;order_controllers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="sb"&gt;`rails test &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="si"&gt;}#{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&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;h2 id="3.编写测试helper"&gt;3.编写测试 helper&lt;/h2&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'base_helper'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'timecop'&lt;/span&gt;

&lt;span class="no"&gt;Timecop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Time&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="mi"&gt;2018&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&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;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# 冻结测试时的时间&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;AdminTestHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;postman_logger&lt;/span&gt;
    &lt;span class="vi"&gt;@postman_logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Logger&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="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/doc/admin_postman.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@postman_logger.formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;proc&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_progname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@postman_logger&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Add more helper methods to be used by all tests here...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;generate_doc_flag: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;code: &lt;/span&gt;&lt;span class="s1"&gt;'200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;login_flag: &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;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'authorization'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_gw_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;auth_token: &lt;/span&gt;&lt;span class="s1"&gt;'m2TfHdaADecAEscTT4FzJ48sz'&lt;/span&gt;&lt;span class="p"&gt;)&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;login_flag&lt;/span&gt;
    &lt;span class="nb"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;headers: &lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="s1"&gt;'200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON&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="vi"&gt;@response.body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;generate_postman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'GENERATE_POSTMAN'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;generate_doc_flag&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;generate_postman&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@controller.action_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scope: &lt;/span&gt;&lt;span class="s1"&gt;'admin.action_name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locale: &lt;/span&gt;&lt;span class="s1"&gt;'zh-CN'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;postman_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pretty_generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;request: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;raw: &lt;/span&gt;&lt;span class="vi"&gt;@request.original_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'http://www.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{{WEBSITE}}'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'{{HOST}}'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="ss"&gt;port: &lt;/span&gt;&lt;span class="s1"&gt;'{{PORT}}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="vi"&gt;@request.path.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="ss"&gt;query: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;equals: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &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="p"&gt;},&lt;/span&gt;
        &lt;span class="ss"&gt;method: &lt;/span&gt;&lt;span class="nb"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;header: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s1"&gt;'AUTHORIZATION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="s1"&gt;'{{token}}'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="s1"&gt;'application/x-www-form-urlencoded'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;body:
        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt;
          &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="ss"&gt;mode: &lt;/span&gt;&lt;span class="s1"&gt;'urlencoded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;urlencoded: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;enabled: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;response: &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;postman_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s1"&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;class&lt;/span&gt; &lt;span class="nc"&gt;Admin::BaseControllerTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionDispatch&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IntegrationTest&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AdminTestHelper&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="4. 编写测试代码"&gt;4. 编写测试代码&lt;/h2&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'admin_test_helper'&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admin::SessionsControllerTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Admin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseControllerTest&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s1"&gt;'admin_login'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;send_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin_login_admin_sessions_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;username: &lt;/span&gt;&lt;span class="n"&gt;managers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:one&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'123456'&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;h2 id="5.运行rake任务, 在postman里导入生成的postman.json"&gt;5.运行 rake 任务，在 postman 里导入生成的 postman.json&lt;/h2&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;rake postman:admin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;p.s 最后把文档导入 Insomnia(更简洁的 api 工具，兼容 postman), 不是 postman&lt;/p&gt;</description>
      <author>ThxFly</author>
      <pubDate>Wed, 17 Oct 2018 19:45:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/37640</link>
      <guid>https://ruby-china.org/topics/37640</guid>
    </item>
    <item>
      <title>QA 不写测试用例，大家怎么看？</title>
      <description>&lt;p&gt;小团队十来个人，一个 QA，女。工作认真负责。就是不写测试用例，认为增量测试就可以了，写用例浪费时间，怎么破？&lt;/p&gt;</description>
      <author>lithium4010</author>
      <pubDate>Fri, 27 Jul 2018 22:51:58 +0800</pubDate>
      <link>https://ruby-china.org/topics/37240</link>
      <guid>https://ruby-china.org/topics/37240</guid>
    </item>
  </channel>
</rss>
