<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>apexy (apexy)</title>
    <link>https://ruby-china.org/apexy</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>深入 Rails 的 Zeitwerk 模式</title>
      <description>&lt;p&gt;原文请参看我的博客：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xfyuan.github.io/2022/12/deep-into-rails-zeitwerk-autoloader/" rel="nofollow" target="_blank"&gt;https://xfyuan.github.io/2022/12/deep-into-rails-zeitwerk-autoloader/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;=======================================================================&lt;/p&gt;

&lt;p&gt;&lt;em&gt;本文已获得原作者（&lt;/em&gt;&lt;em&gt;Simon Coffey&lt;/em&gt;&lt;em&gt;）授权许可进行翻译。原文深入讲述了 Rails 中新的 Zeitwerk 自动加载模式的实现原理，是对前一篇《&lt;a href="https://xfyuan.github.io/2022/11/rails7-zeitwerk-mode/" rel="nofollow" target="_blank" title=""&gt;Rails7 的 Zeitwerk 模式解惑&lt;/a&gt;》很好的补充&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原文链接：&lt;a href="https://www.urbanautomaton.com/blog/2020/11/04/rails-autoloading-heaven/" rel="nofollow" target="_blank" title=""&gt;Rails autoloading — now it works, and how!&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;作者：Simon Coffey（&lt;a href="http://twitter.com/urbanautomaton" rel="nofollow" target="_blank" title=""&gt;Twitter&lt;/a&gt;）&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Rails 从一开始就有自动加载。自动加载意味着当我们想要引用&lt;code&gt;User&lt;/code&gt; model 时，不必还要手写&lt;code&gt;require 'User'&lt;/code&gt;。没人有时间为每个需要用到&lt;code&gt;User&lt;/code&gt;的文件都来这么干，对吧？&lt;/p&gt;

&lt;p&gt;我已经写过一篇关于 Rails 最早的 autoloader（现在叫做“传统”模式的 autoloader）的文章【译者注：&lt;a href="https://www.urbanautomaton.com/blog/2013/08/27/rails-autoloading-hell/" rel="nofollow" target="_blank" title=""&gt;该文链接&lt;/a&gt;】：它是如何工作的，又是如何创造了诸多弊端的。那个时候，我对它很是气愤，为它可浪费了不少时间来调试它造成的问题。&lt;/p&gt;

&lt;p&gt;前一篇文章涵盖了很多细节，但传统的自动加载的诸多问题的根本在于其机制：它使用了&lt;code&gt;Module#const_missing&lt;/code&gt;来检测一个常量何时无法通过正常含义来解析，然后它尝试去查找并加载一个文件来定义它。&lt;/p&gt;

&lt;p&gt;有两个原因使得这种方案无法很可靠地工作：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Module#const_missing&lt;/code&gt;仅当一个常量无法通过正常含义来解析时才执行。因为 Ruby 中给定的常数引用&lt;a href="https://cirw.in/blog/constant-lookup.html" rel="nofollow" target="_blank" title=""&gt;可以潜在地解析为许多常数定义&lt;/a&gt;，这意味着在某些情况下，Ruby 可以在自动加载生效之前就为一个常量引用返回了错误的值。&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;Module#const_missing&lt;/code&gt;被执行时，它并未提供足够的信息来可靠地检测是哪个常量应该被返回。这也就意味着，自动加载，有时将会为一个常量引用返回错误的值得。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;经典的 autoloader 的大部分复杂性都涉及修补这两个无法克服的问题，这使得其很难理解或调试，也不可避免地会出错。&lt;/p&gt;

&lt;p&gt;然而对 Rails 6，有了一个新的 loader：&lt;a href="https://github.com/fxn/zeitwerk" rel="nofollow" target="_blank" title=""&gt;Zeitwerk&lt;/a&gt;。它&lt;a href="https://medium.com/@fxn/zeitwerk-a-new-code-loader-for-ruby-ae7895977e73" rel="nofollow" target="_blank" title=""&gt;声称已经解决了传统模式的所有问题&lt;/a&gt;，这真是个令人兴奋的消息！&lt;/p&gt;

&lt;p&gt;为了做到这点，它使用了三个关键机制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Module#autoload&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Kernel#require&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TracePoint&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;让我们来看看他如何把这些编织到一起的。&lt;/p&gt;
&lt;h2 id="Goodbye #const_missing, Hello #autoload"&gt;Goodbye &lt;code&gt;#const_missing&lt;/code&gt;, Hello &lt;code&gt;#autoload&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;Ruby 有一个内置的自动加载机制，&lt;code&gt;Module#autoload&lt;/code&gt;。这使我们可以提前告诉 Ruby 哪个文件将定义一个特定的常量，而无需付出立即加载该文件的成本。仅当我们第一次引用到那个常量时，Ruby 才会实际去加载它：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# a.rb&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Loading a.rb"&lt;/span&gt;
&lt;span class="no"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hi! I'm ::A"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;autoload&lt;/span&gt; &lt;span class="ss"&gt;:A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'a'&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;A&lt;/span&gt;
&lt;span class="c1"&gt;# Loading a.rb&lt;/span&gt;
&lt;span class="c1"&gt;# Hi! I'm ::A&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而这跟&lt;code&gt;Module#const_missing&lt;/code&gt;的最重要差别在于，我们告诉 Ruby 哪个文件定义了哪个常量，在该常量被使用之前，且在常量解析期间这个信息会被带入进去。&lt;/p&gt;

&lt;p&gt;这就潜在地排除了&lt;code&gt;#const_missing&lt;/code&gt;方案上述两个关键性的错误。我们将不再试图去检测并从错误中恢复，而只是用额外的信息来增强 Ruby 现有的常量解析机制。&lt;/p&gt;

&lt;p&gt;要使用&lt;code&gt;Module#autoload&lt;/code&gt;，你需要在常量被使用之前就知道哪个文件将定义给定的常量。Rails（以及 Ruby 约定）在常量名称和文件之间定义了可预测的映射，这在理论上让我们能够自动化如下变换：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;MyModule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MyClass&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; my_module/my_class.rb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而，传统的 autoloader 支持在初始化时从不存在的文件中来加载常量。如果我在&lt;code&gt;app/models/user.rb&lt;/code&gt;中创建了一个新的&lt;code&gt;User&lt;/code&gt; model，我可以直接在一个已经打开运行中的 rails console 里调用&lt;code&gt;User.create&lt;/code&gt;而无需做任何事情。&lt;/p&gt;

&lt;p&gt;除非有某些进程监控着文件系统的改变，否则我们是无法使用&lt;code&gt;Module#autoload&lt;/code&gt;在其初始化时来自动加载不存在的文件的。监控文件系统很麻烦，也不那么可靠，特别是你需要支持多个操作系统时。&lt;/p&gt;

&lt;p&gt;不过，这个功能会多有用呢？如果我们缩减 autoloader 的定义域，让其初始化时仅仅支持已存在的文件的话，&lt;code&gt;Module#autoload&lt;/code&gt;便成为了一个选择。事实上，这就是 Zeitwerk 所做的事。我们通过实例来看看。&lt;/p&gt;
&lt;h2 id="Loading a single file"&gt;Loading a single file&lt;/h2&gt;
&lt;p&gt;要使用 Zeitwerk，我们初始化一个 loader，并给它一个或多个根目录以从中加载。通过加入一个 logger，就能看到它的操作：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Zeitwerk&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Loader&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;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/ex'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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="no"&gt;STDOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们就可以把一些文件放入根目录中，启动 loader。在贯穿本文的示例代码片段中，我会在那些影响结果的行之后以注释形式展示打印的输出。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /ex/a.rb&lt;/span&gt;
&lt;span class="no"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hi! I'm ::A"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: autoload set for A, to be loaded from /ex/a.rb&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;A&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: constant A loaded from file /ex/a.rb&lt;/span&gt;
&lt;span class="c1"&gt;# Hi! I'm ::A&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们调用&lt;code&gt;loader.setup&lt;/code&gt;时能看到 Zeitwerk 检测并准备要自动加载的文件（这正是&lt;code&gt;Module#autoload&lt;/code&gt;执行的时候）。然后当我们第一次引用常量&lt;code&gt;A&lt;/code&gt;时，就看到它从之前被检测到的文件中被载入，最终我们看到了其打印出的值。&lt;/p&gt;

&lt;p&gt;有趣的是，Zeitwerk 可以检测实际发生的加载以记录下它！要看到它是如何做的，让我们来看看比单个文件更复杂的情况。&lt;/p&gt;
&lt;h2 id="Implicit namespaces"&gt;Implicit namespaces&lt;/h2&gt;
&lt;p&gt;在第一个示例中，我们看到了单个文件在 loader 的根路径内。几乎任何有一定体积的项目都会有一定深度的目录结构，以及一定深度的嵌套模块。&lt;/p&gt;

&lt;p&gt;如果我们创建一个文件&lt;code&gt;c/d.rb&lt;/code&gt;，则会想要加载一个常量&lt;code&gt;C::D&lt;/code&gt;。这意味着我们必须首先加载&lt;code&gt;C&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;然而，&lt;code&gt;C&lt;/code&gt;可能会是一个无趣的命名空间模块；如果对每个这样的命名空间我们都不得不为其创建如下的样板文件，那就很乏味了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /ex/c.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;C&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此 Zertwerk 允许这些命名空间是隐式的。无需那些样板文件，Zeitwerk 从目录名来“自动导入”命名空间模块；本质上，它不需要那样一个 ruby 文件就为我们声明了一个名为&lt;code&gt;C&lt;/code&gt;的模块。&lt;/p&gt;

&lt;p&gt;不过，这展示了一个问题：我们使用 ruby 默认的&lt;code&gt;Module#autoload&lt;/code&gt;来做实际的加载，而它对所要转换为模块的目录一无所知。那么，我们如何告诉 ruby 要怎样提前加载&lt;code&gt;C&lt;/code&gt;呢？&lt;/p&gt;

&lt;p&gt;来看看当目录中只有单个文件时会发生什么：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /ex/c/d.rb&lt;/span&gt;
&lt;span class="no"&gt;C&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hi! I'm C::D"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: autoload set for C, to be autovivified from /ex/c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在初始化的 setup 时，我们可以看到 Zeitwerk 只为自动加载准备了&lt;code&gt;C&lt;/code&gt;。因此，它一定只立刻查找根目录的文件。&lt;/p&gt;

&lt;p&gt;在根目录里，它只发现了一个目录，&lt;code&gt;/ex/c&lt;/code&gt;，所以它不会说从一个文件“&lt;code&gt;C&lt;/code&gt;...被自动加载”，而是会说从那个目录“&lt;code&gt;C&lt;/code&gt;...被自动导入”了。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;C&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: module C autovivified from directory /ex/c&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: autoload set for C::D, to be loaded from /ex/c/d.rb&lt;/span&gt;
&lt;span class="c1"&gt;# C&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后当我们引用&lt;code&gt;C&lt;/code&gt;时，会看到它被&lt;a href="https://github.com/fxn/zeitwerk/blob/034ae30d73247b8dda7df2992903ce560cd7f47f/lib/zeitwerk/loader/callbacks.rb#L39-L40" rel="nofollow" target="_blank" title=""&gt;自动导入&lt;/a&gt;了，然后&lt;code&gt;C::D&lt;/code&gt;被为自动加载而得到设置——Zeitwerk 必须向下深入&lt;code&gt;c&lt;/code&gt;目录以查找更多要自动加载的东西。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;C&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;D&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: constant C::D loaded from file /ex/c/d.rb&lt;/span&gt;
&lt;span class="c1"&gt;# Hi! I'm C::D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终我们引用到了&lt;code&gt;C::D&lt;/code&gt;，它从&lt;code&gt;c/d.rb&lt;/code&gt;这个常规的 ruby 文件得以被自动加载。&lt;/p&gt;

&lt;p&gt;如果没有 ruby 文件被读取，关于&lt;code&gt;C&lt;/code&gt; 的自动导入又是如何工作的呢？&lt;/p&gt;

&lt;p&gt;Zeitwerk 通过拦截&lt;code&gt;Module#autoload&lt;/code&gt;其中的加载那一部分代码来做到这点。当我们调用&lt;code&gt;autoload :C, '/ex/c'&lt;/code&gt;时，这意味着在&lt;code&gt;C&lt;/code&gt;被首次使用的时候，ruby 会自动调用&lt;code&gt;require '/ex/c'&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;默认情况下，如果我们试图&lt;code&gt;require&lt;/code&gt;一个目录时，ruby 会抛出一个&lt;code&gt;LoadError&lt;/code&gt;。但由于&lt;code&gt;Kernel#require&lt;/code&gt;是跟 ruby 任何其他方法一样的方法，Zeitwerk 就能够&lt;a href="https://github.com/fxn/zeitwerk/blob/034ae30d73247b8dda7df2992903ce560cd7f47f/lib/zeitwerk/kernel.rb#L24-L32" rel="nofollow" target="_blank" title=""&gt;拦截该&lt;code&gt;require&lt;/code&gt;调用&lt;/a&gt;，在其中加入一些“猴子补丁”：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lib/zeitwerk/kernel.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Kernel&lt;/span&gt;
  &lt;span class="kp"&gt;module_function&lt;/span&gt;

  &lt;span class="kp"&gt;alias_method&lt;/span&gt; &lt;span class="ss"&gt;:zeitwerk_original_require&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:require&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Zeitwerk&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loader_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;".rb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;zeitwerk_original_require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tap&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;required&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_file_autoloaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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;required&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_dir_autoloaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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;else&lt;/span&gt;
      &lt;span class="c1"&gt;# code to handle paths not managed by Zeitwerk&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;现在 Zeitwerk 就有机会在文件被读取之前去查找所要加载的路径了。通过使用绝对文件路径和&lt;code&gt;.rb&lt;/code&gt;扩展名（这是可选的）来声明其自动加载，它就能可靠地知道哪个&lt;code&gt;require&lt;/code&gt;的调用是在其所负责的目录内，以及哪些是针对目录或 ruby 文件的。&lt;/p&gt;

&lt;p&gt;对于应用中每个加载的文件，Zeitwerk 都做了如下一些事：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果它是一个由 loader 负责管理的 ruby 文件……

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;就让 ruby 加载它，并标记其常量为已加载&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果它是一个由 loader 负责管理的目录……

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;就自动导入模块，并设置其子目录用于自动加载&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;否则，loader 就不做管理，则……

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;让 ruby 加载它吧&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;目录处理的代码相当难懂，但这里我们能够看到命名空间模块被创建，被赋予给有关的常量名，然后加载操作记录下日志。&lt;/p&gt;

&lt;p&gt;到此为止，一切良好！我们加载了常规的文件，看到了隐式命名空间，已有的目录被用于推断命名空间模块。这已经涵盖了 Zeitwerk 三个主要基石技巧中的两个了。&lt;/p&gt;

&lt;p&gt;要看到 Zeitwerk 的行囊中最后那一个杀手锏，得来看看另一个场景。&lt;/p&gt;
&lt;h2 id="Explicit namespaces"&gt;Explicit namespaces&lt;/h2&gt;
&lt;p&gt;有时候我们确实想要显式定义命名空间模块，比如在那个模块上有一个方法时。这个场景下将会同时需要一个 ruby 文件来定义模块，和包含那些文件的目录来定义命名空间常量。&lt;/p&gt;

&lt;p&gt;这意味着当我们加载一个常规 ruby 文件时，有额外的工作要做。如果那个文件定义了一个 class 或一个 module，并且有一个匹配的子目录在其加载路径中，我们就需要确保为那个子目录设置了自动加载，就如同上面为隐式命名空间所做的那样。&lt;/p&gt;

&lt;p&gt;这就是&lt;a href="https://ruby-doc.org/core-2.7.2/TracePoint.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;TracePoint&lt;/code&gt;&lt;/a&gt; 的用武之地了。&lt;code&gt;TracePoint&lt;/code&gt;是 ruby 标准库的一部分，能让我们为发生在 ruby 解释器中的确定事件来定义回调，这些事件有：方法调用，module 或 class 的定义，等等。&lt;/p&gt;

&lt;p&gt;我们对&lt;code&gt;:class&lt;/code&gt;事件特别感兴趣，该事件会在一个 module 或 class 无论何时被定义时都告知我们：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;trace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;TracePoint&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="ss"&gt;:class&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;tp&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;self&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;A&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;# [:class, A]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过在这个事件上设置一个 trace，Zeitwerk 就能在任何新模块被定义时知道。类似于它查看&lt;code&gt;require&lt;/code&gt;的调用以检查它是否负责这些路径，它去查看 class 或 module 的名称以查看它是否是一个常量，其加载应该由 Zeitwerk 来负责。&lt;/p&gt;

&lt;p&gt;来看看 Zeitwerk 的做法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /ex/c.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;C&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hello&lt;/span&gt;
    &lt;span class="s2"&gt;"Hi! I'm ::C"&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;# /ex/c/d.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;C&lt;/span&gt;
  &lt;span class="no"&gt;D&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hi! I'm C::D"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: autoload set for C, to be loaded from /ex/c.rb&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hello&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: autoload set for C::D, to be loaded from /ex/c/d.rb&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: constant C loaded from file /ex/c.rb&lt;/span&gt;
&lt;span class="c1"&gt;# Hi! I'm ::C&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;C&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;D&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: constant C::D loaded from file /ex/c/d.rb&lt;/span&gt;
&lt;span class="c1"&gt;# Hi! I'm C::D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们能看到 Zeitwerk 在&lt;code&gt;c.rb&lt;/code&gt;还仍然在加载时就能够检测&lt;code&gt;C&lt;/code&gt;的定义。由于&lt;code&gt;C&lt;/code&gt;是一个它所负责的常量，并且由于有一个&lt;code&gt;c&lt;/code&gt;目录在 loader 的根目录内，它就向下深入到&lt;code&gt;c&lt;/code&gt;目录里并设置那个位置的自动加载，搜寻&lt;code&gt;d.rb&lt;/code&gt;，并设置&lt;code&gt;C::D&lt;/code&gt;的自动加载。&lt;/p&gt;

&lt;p&gt;事实上，这比它表现出来的还要灵活。我们能够从任何地方重新打开自动加载的常量，即使定位已经在 Zeitwerk 所管理路径之外，并且 loader 路径内的定义将依旧能生效。&lt;/p&gt;

&lt;p&gt;使用同样的文件来作为我们最后一个示例：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;
&lt;span class="c1"&gt;# Zeitwerk: autoload set for C, to be loaded from /ex/c.rb&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;C&lt;/span&gt;
  &lt;span class="c1"&gt;# Zeitwerk: autoload set for C::D, to be loaded from /ex/c/d.rb&lt;/span&gt;
  &lt;span class="c1"&gt;# Zeitwerk: constant C loaded from file /ex/c.rb&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;D&lt;/span&gt;
  &lt;span class="c1"&gt;# Zeitwerk: constant C::D loaded from file /ex/c/d.rb&lt;/span&gt;
  &lt;span class="c1"&gt;# Hi! I'm C::D&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;C&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hello&lt;/span&gt;
&lt;span class="c1"&gt;# Hi! I'm ::C&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个在传统自动加载模式下会失败的例子。当我们打开在加载路径之外的&lt;code&gt;C&lt;/code&gt;模块，且它还未被加载时，我们就定义了它；而&lt;code&gt;Module#const_missing&lt;/code&gt;根本就没被调用。因此，&lt;code&gt;c.rb&lt;/code&gt;将永远不会被加载，而方法&lt;code&gt;C.hello&lt;/code&gt;将永远不会被定义。&lt;/p&gt;

&lt;p&gt;然而，使用 TracePoint，我们就能发现 autoloader 所应负责的常量的重定义，并从 loader 路径（如果存在的话）预先加载相关文件。&lt;/p&gt;
&lt;h2 id="Conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;对于 Zeitwerk 还有更多内容（预加载，重加载，线程安全，等等），但那已经超出本文篇幅了。&lt;/p&gt;

&lt;p&gt;至此真是令人愉悦的旅程。这儿仍然有复杂的地方，但基石看起来确实非常牢固了。我还没有在新项目上使用新的 loader，但当我这样做时，我觉得会更有信心，可以或多或少地使用常量（特别是命名空间模块），而不必再太花心思了。&lt;/p&gt;

&lt;p&gt;非常感谢 Xavier Noria 以及&lt;a href="https://github.com/fxn/zeitwerk#thanks" rel="nofollow" target="_blank" title=""&gt;其他所有&lt;/a&gt;为 Zeitwerk 做出贡献的人！&lt;/p&gt;</description>
      <author>apexy</author>
      <pubDate>Thu, 08 Dec 2022 11:44:30 +0800</pubDate>
      <link>https://ruby-china.org/topics/42783</link>
      <guid>https://ruby-china.org/topics/42783</guid>
    </item>
    <item>
      <title>Rails7 的 Zeitwerk 模式解惑</title>
      <description>&lt;p&gt;原文请参看我的博客：&lt;a href="https://xfyuan.github.io/2022/11/rails7-zeitwerk-mode/" rel="nofollow" target="_blank"&gt;https://xfyuan.github.io/2022/11/rails7-zeitwerk-mode/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;=================================================================&lt;/p&gt;

&lt;p&gt;&lt;em&gt;本文已获得原作者（&lt;/em&gt;&lt;em&gt;Athira Kadampatta&lt;/em&gt;&lt;em&gt;、&lt;/em&gt;&lt;em&gt;Supriya Laxman Medankar&lt;/em&gt;&lt;em&gt;）和 Kiprosh 授权许可进行翻译。原文详细讲述了 Rails 7 中新的 Zeitwerk 自动加载模式。&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原文链接：&lt;a href="https://blog.kiprosh.com/autoloading-pitfalls-fixed-by-rails-7-s-default-zeitwerk-mode/" rel="nofollow" target="_blank" title=""&gt;Autoloading pitfalls fixed by Rails 7’s default Zeitwerk mode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;作者：&lt;strong&gt;Athira Kadampatta&lt;/strong&gt;、&lt;strong&gt;Supriya Laxman Medankar&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;站点：Kiprosh，一家印度的软件开发公司。&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Rails 中传统的 autoloader 很有帮助，但仍然有一些瑕疵造成自动加载偶尔会出毛病。为了解决这个问题， &lt;a href="https://github.com/fxn" rel="nofollow" target="_blank" title=""&gt;Xavier Noria&lt;/a&gt; 在 Rails 6 的&lt;a href="https://github.com/rails/rails/pull/35235" rel="nofollow" target="_blank" title=""&gt;这个 PR&lt;/a&gt; 中提出了 zeitwerk 模式并使其可配置使用。Rails 7 则更进一步，zeitwerk 完全替代了传统的 autoloader。&lt;/p&gt;

&lt;p&gt;本文中，我们会看看传统的自动加载会碰到的问题，以及 Zeitwerk 模式如何解决的。（你可以阅读&lt;a href="https://www.urbanautomaton.com/blog/2013/08/27/rails-autoloading-hell/" rel="nofollow" target="_blank" title=""&gt;这篇文章&lt;/a&gt; 来理解 Rails 的 autoloader 是怎样工作的）。&lt;/p&gt;
&lt;h2 id="How classic autoloading works?"&gt;How classic autoloading works?&lt;/h2&gt;
&lt;p&gt;起初，Rails 使用的是在 Active Support 中称作 &lt;a href="https://guides.rubyonrails.org/v6.0/autoloading_and_reloading_constants_classic_mode.html" rel="nofollow" target="_blank" title=""&gt;Classic Autoloading&lt;/a&gt; 的实现来作为 autoloader，一直持续到 Rails 6。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://guides.rubyonrails.org/v6.0/autoloading_and_reloading_constants_classic_mode.html" rel="nofollow" target="_blank" title=""&gt;Classic Autoloading&lt;/a&gt; 依赖的是 Ruby 的常量查找。要解析一个常量，会首先在所定义的类的词法域中查找，然后在其祖先链中查找。如果该常量未找到，&lt;code&gt;const_missing&lt;/code&gt;方法就会被 Ruby 调用。Rails 覆写了 Ruby 的&lt;code&gt;const_missing&lt;/code&gt; 方法，并使用&lt;code&gt;autoload_paths&lt;/code&gt; 根据惯例约定来解析常量。&lt;/p&gt;
&lt;h2 id="How zeitwerk autoloading works?"&gt;How zeitwerk autoloading works?&lt;/h2&gt;
&lt;p&gt;新引入的 &lt;a href="https://guides.rubyonrails.org/v6.0/autoloading_and_reloading_constants.html" rel="nofollow" target="_blank" title=""&gt;Zeitwerk Mode&lt;/a&gt; 则不依赖 Ruby 的常量查找。&lt;/p&gt;

&lt;p&gt;相反，它利用的是 Ruby 的 &lt;code&gt;Module#autoload&lt;/code&gt;  方法提前告知 Ruby 哪个文件将定义一个特定常量，而不需要立即加载该文件。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog.kiprosh.com/content/images/2022/09/Rails-autoload-how-it-works.png" title="" alt="https://blog.kiprosh.com/content/images/2022/09/Rails-autoload-how-it-works.png"&gt;&lt;/p&gt;
&lt;h2 id="Common Problems resolved by zeitwerk mode"&gt;Common Problems resolved by zeitwerk mode&lt;/h2&gt;
&lt;p&gt;传统模式存在许多&lt;a href="https://guides.rubyonrails.org/v6.0/autoloading_and_reloading_constants_classic_mode.html#common-gotchas" rel="nofollow" target="_blank" title=""&gt;问题&lt;/a&gt;，但都已被 zeitwerk 模式解决了。这其中，我们会看看三个不同的陷阱，每个都带有示例。&lt;/p&gt;
&lt;h3 id="1、When Constants aren't Missed"&gt;1、&lt;strong&gt;When Constants aren't Missed&lt;/strong&gt;
&lt;/h3&gt;
&lt;p&gt;假设我们有如下 model 结构：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# course.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Course&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"From Course"&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;# mit_university/course.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MitUniversity&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Course&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
      &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"From MitUniversity::Course"&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="c1"&gt;# mit_university/engineering.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MitUniversity&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Engineering&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;@course&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Course&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="With Classic Mode"&gt;With Classic Mode&lt;/h4&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Loading&lt;/span&gt; &lt;span class="n"&gt;development&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt; &lt;span class="mf"&gt;5.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;001&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="no"&gt;From&lt;/span&gt; &lt;span class="no"&gt;Course&lt;/span&gt;
 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;#&amp;lt;Course:0x0000563bfa029810&amp;gt;&lt;/span&gt;
&lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;002&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MitUniversity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engineering&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="no"&gt;From&lt;/span&gt; &lt;span class="no"&gt;Course&lt;/span&gt;
 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;#&amp;lt;MitUniversity::Engineering:0x0000563bf9e7ab40 @course=#&amp;lt;Course:0x0000563bf9e7aaf0&amp;gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里，由于我们在调用&lt;code&gt;MitUniversity::Course&lt;/code&gt;之前调用了&lt;code&gt;Course&lt;/code&gt;，Ruby 的常量查找就已经在内存中自动加载了&lt;code&gt;Course&lt;/code&gt;，所以如果我们想要为&lt;code&gt;MitUniversity::Engineering&lt;/code&gt;创建一个对象时，它就会引用到已在内存中被自动加载的&lt;code&gt;Course&lt;/code&gt;，而不去搜索&lt;code&gt;MitUniversity::Course&lt;/code&gt;了。这让自动加载依赖于常量被调用的顺序。&lt;/p&gt;
&lt;h4 id="With Zeitwerk Mode"&gt;With Zeitwerk Mode&lt;/h4&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Loading&lt;/span&gt; &lt;span class="n"&gt;development&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt; &lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;001&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Course&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="no"&gt;From&lt;/span&gt; &lt;span class="no"&gt;Course&lt;/span&gt;
 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;#&amp;lt;Course:0x00005615a9707fa0&amp;gt;&lt;/span&gt;
&lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;002&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MitUniversity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engineering&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="no"&gt;From&lt;/span&gt; &lt;span class="no"&gt;MitUniversity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Course&lt;/span&gt;
 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;#&amp;lt;MitUniversity::Engineering:0x00005615ae41d290 @course=#&amp;lt;MitUniversity::Course:0x00005615ae40b270&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;003&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 zeitwerk 模式为所有常量定义了&lt;code&gt;autoload_path&lt;/code&gt;，它已经知道了到哪里去查找哪个常量。所以尽管首先初始化&lt;code&gt;Course&lt;/code&gt;，但在&lt;code&gt;MitUniversity::Engineering&lt;/code&gt;类中调用时，它仍然如期望的那样加载了&lt;code&gt;MitUniversity::Course&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="2、Autoloading within Singleton Classes"&gt;2、&lt;strong&gt;Autoloading within Singleton Classes&lt;/strong&gt;
&lt;/h3&gt;
&lt;p&gt;这是一个关于 Singleton 类方法的类似问题，已经被 zeitwerk 模式所解决。例子如下所示：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mit_university/course.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MitUniversity&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Course&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;
      &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"From MitUniversity::Course"&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="c1"&gt;# mit_university/engineering.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;MitUniversity&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Engineering&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;details&lt;/span&gt;
        &lt;span class="no"&gt;Course&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;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="With classic mode"&gt;With classic mode&lt;/h4&gt;
&lt;p&gt;如果我们在调用&lt;code&gt;MitUniversity::Course&lt;/code&gt;之前调用 &lt;code&gt;MitUniversity::Engineering.details&lt;/code&gt;，它将会抛出&lt;code&gt;uninitialized constant Course&lt;/code&gt;的错误。这是由于，当自动加载被触发时，Rails 只去检查顶层命名空间，因为 singleton 类是匿名的，所以 Rails 不会知道嵌套的&lt;code&gt;MitUniversity&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Loading&lt;/span&gt; &lt;span class="n"&gt;development&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt; &lt;span class="mf"&gt;5.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;001&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MitUniversity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engineering&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;details&lt;/span&gt;
&lt;span class="no"&gt;Traceback&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&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;from&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="mi"&gt;3&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;from&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mit_university&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;engineering&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="ss"&gt;:in&lt;/span&gt; &lt;span class="sb"&gt;`details'
NameError (uninitialized constant Course)
2.7.5 :002 &amp;gt; MitUniversity::Course
 =&amp;gt; MitUniversity::Course
2.7.5 :003 &amp;gt; MitUniversity::Engineering.details
From MitUniversity::Course
 =&amp;gt; #&amp;lt;MitUniversity::Course:0x0000560cabb27c18&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="With zeitwerk mode"&gt;With zeitwerk mode&lt;/h4&gt;
&lt;p&gt;Zeitwerk 模式则不会抛出任何错误，并且即使之前没有自动加载它也能载入该常量。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Loading&lt;/span&gt; &lt;span class="n"&gt;development&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt; &lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;001&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;MitUniversity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engineering&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;details&lt;/span&gt;
&lt;span class="no"&gt;From&lt;/span&gt; &lt;span class="no"&gt;MitUniversity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Course&lt;/span&gt;
 &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;#&amp;lt;MitUniversity::Course:0x0000559ebba13dd0&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="3、Autoloading and Single-table Inheritance (STI)"&gt;3、&lt;strong&gt;Autoloading and Single-table Inheritance (STI)&lt;/strong&gt;
&lt;/h3&gt;
&lt;p&gt;假设我们有如下 Single-table Inheritance (STI) 的 model 已定义：&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;Polygon&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&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;Triangle&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Polygon&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;Rectangle&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Polygon&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;Square&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rectangle&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Square&lt;/code&gt;继承自 &lt;code&gt;Rectangle&lt;/code&gt;，所以当我们调用&lt;code&gt;Rectangle.all&lt;/code&gt;时，结果必须包含&lt;code&gt;Polygon&lt;/code&gt;类型的&lt;code&gt;Square&lt;/code&gt;以及&lt;code&gt;Rectangle&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id="With classic mode"&gt;With classic mode&lt;/h4&gt;
&lt;p&gt;然而，当我们调用&lt;code&gt;Rectangle.all&lt;/code&gt;时，并不能在结果中看到&lt;code&gt;Square&lt;/code&gt;记录。我们可以看到所生成的 SQL 查询中并未包含&lt;code&gt;Square&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Loading&lt;/span&gt; &lt;span class="n"&gt;development&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt; &lt;span class="mf"&gt;5.2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;7.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;001&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Rectangle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="no"&gt;Rectangle&lt;/span&gt; &lt;span class="no"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="sb"&gt;`polygons`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;*&lt;/span&gt; &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="sb"&gt;`polygons`&lt;/span&gt; &lt;span class="no"&gt;WHERE&lt;/span&gt; &lt;span class="sb"&gt;`polygons`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;`&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="sb"&gt;` = 'Rectangle' /* loading for inspect */ LIMIT 11
 =&amp;gt; #&amp;lt;ActiveRecord::Relation [#&amp;lt;Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000"&amp;gt;, #&amp;lt;Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000"&amp;gt;]&amp;gt;
 2.7.5 :002 &amp;gt; Square
 =&amp;gt; Square(id: integer, area: float, type: string, type_id: integer, created_at: datetime, updated_at: datetime)
 2.7.5 :003 &amp;gt; Rectangle.all
  Rectangle Load (0.9ms)  SELECT `&lt;/span&gt;&lt;span class="n"&gt;polygons&lt;/span&gt;&lt;span class="sb"&gt;`.* FROM `&lt;/span&gt;&lt;span class="n"&gt;polygons&lt;/span&gt;&lt;span class="sb"&gt;` WHERE `&lt;/span&gt;&lt;span class="n"&gt;polygons&lt;/span&gt;&lt;span class="sb"&gt;`.`&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="sb"&gt;` IN ('Rectangle', 'Square') /* loading for inspect */ LIMIT 11
 =&amp;gt; #&amp;lt;ActiveRecord::Relation [#&amp;lt;Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000"&amp;gt;, #&amp;lt;Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000"&amp;gt;, #&amp;lt;Square id: 5, area: 250.0, type: "Square", type_id: nil, created_at: "2022-08-28 14:01:52.165141000 +0000", updated_at: "2022-08-28 14:01:52.165141000 +0000"&amp;gt;]&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要解决这个问题，我们不得不在&lt;code&gt;rectangle.rb&lt;/code&gt;文件底部加上&lt;code&gt;require_dependency 'square'&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/rectangle.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rectangle&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Polygon&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;require_dependency&lt;/span&gt; &lt;span class="s1"&gt;'square'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="With zeitwerk mode"&gt;With zeitwerk mode&lt;/h4&gt;
&lt;p&gt;由于在 zeitwerk 模式中，&lt;code&gt;Square&lt;/code&gt;已被自动加载进来，我们就无需添加&lt;code&gt;require_dependency 'square'&lt;/code&gt;这一行了：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Loading&lt;/span&gt; &lt;span class="n"&gt;development&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt; &lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;2.7&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;001&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Rectangle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="no"&gt;Rectangle&lt;/span&gt; &lt;span class="no"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="sb"&gt;`polygons`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;*&lt;/span&gt; &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="sb"&gt;`polygons`&lt;/span&gt; &lt;span class="no"&gt;WHERE&lt;/span&gt; &lt;span class="sb"&gt;`polygons`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;`&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="sb"&gt;` IN ('Rectangle', 'Square') /* loading for inspect */ LIMIT 11
 =&amp;gt; #&amp;lt;ActiveRecord::Relation [#&amp;lt;Rectangle id: 1, area: 100.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:00:50.705523000 +0000", updated_at: "2022-08-28 14:00:50.705523000 +0000"&amp;gt;, #&amp;lt;Rectangle id: 2, area: 200.0, type: "Rectangle", type_id: nil, created_at: "2022-08-28 14:01:09.543825000 +0000", updated_at: "2022-08-28 14:01:09.543825000 +0000"&amp;gt;, #&amp;lt;Square id: 5, area: 250.0, type: "Square", type_id: nil, created_at: "2022-08-28 14:01:52.165141000 +0000", updated_at: "2022-08-28 14:01:52.165141000 +0000"&amp;gt;]&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="Conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;对于 Rails 7，Zeitwerk 已经成为默认模式，而传统模式已不可用了。这是一个很有影响的变化，改进了 Rails 中常量自动加载的方式，解决了诸多如上所述的传统模式带来的问题。&lt;/p&gt;
&lt;h3 id="References"&gt;References&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://guides.rubyonrails.org/v6.1/autoloading_and_reloading_constants_classic_mode.html#common-gotchas" rel="nofollow" target="_blank" title=""&gt;Common gotchas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.urbanautomaton.com/blog/2020/11/04/rails-autoloading-heaven/" rel="nofollow" target="_blank" title=""&gt;Rails autoloading — now it works, and how!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/cedarcode/understanding-zeitwerk-in-rails-6-f168a9f09a1f" rel="nofollow" target="_blank" title=""&gt;Understanding Zeitwerk in Rails 6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://guides.rubyonrails.org/classic_to_zeitwerk_howto.html" rel="nofollow" target="_blank" title=""&gt;Classic to Zeitwerk HOWTO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=DzyGdOd_6-Y&amp;amp;list=PLbHJudTY1K0f1WgIbKCc0_M-XMraWwCmk" rel="nofollow" target="_blank" title=""&gt;RailsConf 2022 - Opening Keynote: The Journey to Zeitwerk by Xavier Noria&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>apexy</author>
      <pubDate>Thu, 08 Dec 2022 11:42:44 +0800</pubDate>
      <link>https://ruby-china.org/topics/42782</link>
      <guid>https://ruby-china.org/topics/42782</guid>
    </item>
    <item>
      <title>Rails 6.1 正式发布了～！</title>
      <description>&lt;p&gt;新特性：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Horizontal Sharding, &lt;/li&gt;
&lt;li&gt;Multi-DB Improvements, &lt;/li&gt;
&lt;li&gt;Strict Loading, &lt;/li&gt;
&lt;li&gt;Destroy Associations in Background, &lt;/li&gt;
&lt;li&gt;Error Objects, &lt;/li&gt;
&lt;li&gt;and more!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://weblog.rubyonrails.org/2020/12/9/Rails-6-1-0-release/" rel="nofollow" target="_blank"&gt;https://weblog.rubyonrails.org/2020/12/9/Rails-6-1-0-release/&lt;/a&gt;&lt;/p&gt;</description>
      <author>apexy</author>
      <pubDate>Thu, 10 Dec 2020 09:57:06 +0800</pubDate>
      <link>https://ruby-china.org/topics/40661</link>
      <guid>https://ruby-china.org/topics/40661</guid>
    </item>
    <item>
      <title>TestProf 文档中文版上线（和跟作者 Vladimir 交流的过程及感受）</title>
      <description>&lt;p&gt;这是对 TestProf 中文文档翻译经过的一个简要记录，以及跟作者 Vladimir 交流的过程及感受。（本文在我的博客地址：&lt;a href="https://xfyuan.github.io/2020/08/testprof-chinese-doc-is-online/" rel="nofollow" target="_blank" title=""&gt;https://xfyuan.github.io/2020/08/testprof-chinese-doc-is-online/&lt;/a&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; 的一些使用方法和技巧，这个 Evil Martins 出品的 Ruby 测试工具 Gem 的强大和有趣，从中可窥一斑。要想了解和使用 TestProf 的全部功能，当然还是需要去看它的官方文档（地址：&lt;a href="https://test-prof.evilmartians.io" rel="nofollow" target="_blank" title=""&gt;https://test-prof.evilmartians.io&lt;/a&gt;）。顺便说一句，连这个 Gem 文档的网站都秉承了 Evil Martins 的一贯风格，同样的精致，同样的讲究设计感。&lt;/p&gt;

&lt;p&gt;为了更好地推荐给国内的 Ruby 开发者，于是我有了一个大胆的想法。我给 TestProf 作者 &lt;strong&gt;Vladimir Dementyev&lt;/strong&gt; 发了邮件，向他询问关于把 TestProf 的文档进行中文化的意向。Vladimir 技术很厉害，没想到人也非常好沟通。作为作者，他当然也是希望把文档翻译成更多的本地化语言，好推广给更多的 Ruby 开发者了。不过他同时回复说文档网站目前还不支持多语言，需要先调研一下，尽快告诉我结果。&lt;/p&gt;

&lt;p&gt;而他只用了两个晚上，就搞定了把原本只支持英文的文档网站，扩展升级为支持多语言的版本，并且添加了全部文档的俄语版本进行验证。既然文档的网站一切准备就绪，那么接下来就等我的中文化翻译了。&lt;/p&gt;

&lt;p&gt;翻译工作从 2020.07.24 开始，到 2020.08.12 完成最后一篇，共耗时 12 天。&lt;/p&gt;

&lt;p&gt;中文版本来可以那时就上线的，不巧的是，正好碰上是 Vladimir 的假期，只能等他回来再处理。&lt;/p&gt;

&lt;p&gt;今天，TestProf 文档的&lt;a href="https://test-prof.evilmartians.io/#/zh-cn/" rel="nofollow" target="_blank" title=""&gt;中文版&lt;/a&gt;终于正式放出，可供所有 Ruby 开发者查阅了。效果如下：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.jsdelivr.net/gh/xfyuan/ossimgs@master/20200818testprof-doc-cn-01.jpg" title="" alt="20200818testprof-doc-cn-01"&gt;&lt;/p&gt;

&lt;p&gt;Vladimir 也在 Twitter 上发布了中文版文档上线的消息:)&lt;/p&gt;

&lt;p&gt;&lt;img src="https://cdn.jsdelivr.net/gh/xfyuan/ossimgs@master/20200818testprof-doc-cn-02.jpg" title="" alt="20200818testprof-doc-cn-02"&gt;&lt;/p&gt;

&lt;p&gt;衷心希望 TestProf 的中文文档能够帮助所有国内的 Ruby 开发者把测试写得更加高效，也让编写测试成为一种乐趣，从而让 Ruby/Rails 程序更加完美！&lt;/p&gt;</description>
      <author>apexy</author>
      <pubDate>Tue, 18 Aug 2020 14:31:22 +0800</pubDate>
      <link>https://ruby-china.org/topics/40295</link>
      <guid>https://ruby-china.org/topics/40295</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>DHH 最新博客——“雄伟巨石”可以成为“城堡”</title>
      <description>&lt;p&gt;DHH 在 2020.04.08 发表了一篇最新博客“The Majestic Monolith can become The Citadel”，继续讨论对微服务的一点看法，提出了一种与微服务相对的“城堡”模式。在推上也引发了不少关注，搜关键字“The Majestic Monolith”就能看到很多。这是原文链接：&lt;a href="https://m.signalvnoise.com/the-majestic-monolith-can-become-the-citadel/" rel="nofollow" target="_blank"&gt;https://m.signalvnoise.com/the-majestic-monolith-can-become-the-citadel/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我按照个人理解，粗略翻译了一下。大家可以对照原文来看，如有翻译不准确的地方，请在回复中提出来。&lt;/p&gt;

&lt;p&gt;[翻译]&lt;/p&gt;
&lt;h2 id="“雄伟巨石”可以成为“城堡”"&gt;“雄伟巨石”可以成为“城堡”&lt;/h2&gt;
&lt;p&gt;大多数的 Web 应用应该都是从一块“雄伟巨石”开始其生涯的：一个单一代码库做其所需的所有事儿。与之相对的是一群 Service，不管这些 Service 是“微”还是“大一点”的，都试图把应用切成孤岛，每个仅做整体工作的一小片而已。&lt;/p&gt;

&lt;p&gt;大多数的 Web 应用都将以“雄伟巨石”形态在其一生中都能持续提供很好的服务。这种模式的上限很高，比大部分人幻想成为架构师时所能想象的要高得多。&lt;/p&gt;

&lt;p&gt;但是，尽管如此，“雄伟巨石”仍然会有需要一些帮助的那一天。也许你正在与庞大的团队打交道，其中的人们相互磕磕绊绊（即使这样，别忘了有很多非常大的组织依然在使用 monorepo 模式！）。或者你终究会遇到极端负载下的性能或可用性问题，这在“雄伟巨石”的技术选择范围内无法轻松解决。你的第一直觉将是改进“雄伟巨石”直到其能够应对问题，做了这一切而没成功时，你才会考虑下一步。&lt;/p&gt;

&lt;p&gt;下一步就是“城堡”，保持“雄伟巨石”在中心位置，但用一系列的“基地”对其支援，每个分离出应用程序职能一个小的子集。“基地”使得“雄伟巨石”得以卸下其不同行为的一个切片，（这些不同行为）要么是出于组织原因，要么是出于性能或实现的原因。&lt;/p&gt;

&lt;p&gt;一个在 Basecamp 的这种例子是我们的旧 chat 应用 Campfire。它是在 2005 年构建的，那时 Ajax 和其他 JavaScript 技术都还很新颖，所以它基于 polling 而不是现代 chat app 目前使用的长连接。这意味着每个客户端连接到系统都会每三秒触发一个请求来询问“是否有我的新消息？”。大多数这些请求都会回答“不，没有”，但为了获取这个答案，你仍然不得不对请求进行身份验证，查询数据库，等等。&lt;/p&gt;

&lt;p&gt;与应用程序的其他相比，这个服务的性能特征大不相同。在任何给定时间，它都将占所有请求的 99%。它也是一个确实很简单的系统。在 Ruby 中，它仅仅 20 行代码长度而已，如果我没记错的话。换句话说，这是一个极好的“基地”候选人！&lt;/p&gt;

&lt;p&gt;所以我们就（为它）构筑了一个“基地”。这么些年来，在日光下使用每种高性能编程语言来重写这种“基地”成为了我们的乐趣，因为通常只用短短几百行代码就能搞定，不管什么语言。所以我们使用了 C，C++，Go，Erlang，还有些我都忘记了。&lt;/p&gt;

&lt;p&gt;但这显然是一种“基地”！应用程序的其他部分继续作为 Ruby on Rails 构建的“雄伟巨石”。我们没有试图把整个 app 切成小的 Service，每个以不同语言来编写。不，我们只是分离出一个单独的“基地”。这是一个“城堡”的建设。&lt;/p&gt;

&lt;p&gt;随着越来越多的人们意识到对于微服务的追逐会走上一条死胡同，“钟摆将会再摆动回来“。“雄伟巨石”在这儿等待着微服务的“难民”。如果他们确实做到了大型应用程序的规模，那么“城堡”这种可以扩展的模式足以让你安心。&lt;/p&gt;</description>
      <author>apexy</author>
      <pubDate>Mon, 13 Apr 2020 14:37:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/39735</link>
      <guid>https://ruby-china.org/topics/39735</guid>
    </item>
    <item>
      <title>Travis CI 被收购了？</title>
      <description>&lt;p&gt;Travis CI joins the Idera family&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.travis-ci.com/2019-01-23-travis-ci-joins-idera-inc" rel="nofollow" target="_blank"&gt;https://blog.travis-ci.com/2019-01-23-travis-ci-joins-idera-inc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;但承诺对 open source project 的支持不变&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;What&lt;/span&gt; &lt;span class="n"&gt;impact&lt;/span&gt; &lt;span class="n"&gt;will&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;have&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt; &lt;span class="n"&gt;source?&lt;/span&gt;

&lt;span class="no"&gt;Open&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;heart&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="no"&gt;Travis&lt;/span&gt; &lt;span class="no"&gt;CI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="no"&gt;We&lt;/span&gt; &lt;span class="n"&gt;will&lt;/span&gt; &lt;span class="n"&gt;continue&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;maintain&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hosted&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;will&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt; &lt;span class="n"&gt;building&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="n"&gt;community&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="no"&gt;Additionally&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;our&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;will&lt;/span&gt; &lt;span class="n"&gt;stay&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;under&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="no"&gt;MIT&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="no"&gt;This&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;who&lt;/span&gt; &lt;span class="n"&gt;we&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="n"&gt;made&lt;/span&gt; &lt;span class="n"&gt;us&lt;/span&gt; &lt;span class="n"&gt;successful&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>apexy</author>
      <pubDate>Wed, 30 Jan 2019 11:10:24 +0800</pubDate>
      <link>https://ruby-china.org/topics/38067</link>
      <guid>https://ruby-china.org/topics/38067</guid>
    </item>
    <item>
      <title>为什么我偏爱 Ember.js 胜过 Angular 和 React.js</title>
      <description>&lt;h2 id="为什么我偏爱 Ember.js 胜过 Angular 和 React.js"&gt;为什么我偏爱 Ember.js 胜过 Angular 和 React.js&lt;/h2&gt;
&lt;p&gt;前几天看到了这篇文章：&lt;a href="http://voidcanvas.com/prefer-ember-js-angular-react-js/" rel="nofollow" target="_blank" title=""&gt;Why I prefer Ember.js over Angular &amp;amp; React.js&lt;/a&gt;，觉得对于国内期望了解 Ember.js 的开发者来说是一个不错的介绍。于是和该文的作者 &lt;a href="http://voidcanvas.com/author/paulshan/" rel="nofollow" target="_blank" title=""&gt;Paul Shan&lt;/a&gt; 联系取得翻译的授权，翻译了过来。&lt;/p&gt;

&lt;p&gt;文章也放在我的 Blog 上：&lt;a href="http://xfyuan.github.io/2017/03/prefer-ember-js-over-angular-and-react-js/" rel="nofollow" target="_blank" title=""&gt;为什么我偏爱 Ember.js 胜过 Angular 和 React.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;译文如下：&lt;/p&gt;

&lt;p&gt;从我开始写 JavaScript 已经有 5 年了。无论是开发项目，指导别人，还是发布文章，JavaScript 都给了我极大的满足感。感谢 JavaScript！&lt;/p&gt;

&lt;p&gt;在过去 5 年里我使用过很多 JavaScript 框架，不管做后端还是前端。但 Ember 从 2015 年中期就从我的开发世界中消失了。幸运的是 1 个月前机缘凑巧我又参加了一个用 Ember 做的前端项目。起初我并不太在意这点，只把它当作不过又一个日常开发而已。但随着项目的深入我开始思考自己关于这三个前端框架的体验，并在今天把它记录下来。&lt;/p&gt;
&lt;h2 id="声明"&gt;声明&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;该文并非是要抨击 Angular 或 React。&lt;/li&gt;
&lt;li&gt;该文并非要很深入探讨技术层面的部分，而是作为一个开发人员对其真实使用体验的吐露。&lt;/li&gt;
&lt;li&gt;标题中我使用了“偏爱”而非“推荐”的字眼，因为“推荐”往往要和具体项目具体场景相关，而“偏爱”则是一种更普适性的描述。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="Ember 令人满意之所在"&gt;Ember 令人满意之所在&lt;/h2&gt;&lt;h3 id="原生感受"&gt;原生感受&lt;/h3&gt;
&lt;p&gt;说实话，最近几年来我对 JavaScript 越来越不满。我不喜欢有人试图用工程的模式来对待 JavaScript。我喜爱 JavaScript 就仅仅是 JavaScript 而已。这也是我为什么不喜欢 TypeScript，以及 Angular 2。我看不到任何用处来多学习另一门语言，只为了试图引入一些丑陋的类（我知道是可选的）和语法。React 至少在这一点上做的比 Angular 2 好一些，但你依旧要面对 jsx 的问题。&lt;/p&gt;

&lt;p&gt;相反，Ember 使用了纯 JavaScript。你只用写 JavaScript。Ember 提供了很多 api，却没有额外的语法。这让我作为 JavaScript 开发者而言感觉很棒。&lt;/p&gt;
&lt;h3 id="约定大于配置"&gt;约定大于配置&lt;/h3&gt;
&lt;p&gt;相信我，尽可能地减少配置相关代码对你的项目将有巨大的好处。首先，代码会变得少而清爽。其次，约定将是通用的，任何新加入的开发者都能了解发生了什么。对于那些之前从没有使用过 Ember 的人来说，只要你遵循了约定来命名文件和变量，Ember 就能自己处理好剩下的事情。&lt;/p&gt;
&lt;h3 id="所见过的最好文档"&gt;所见过的最好文档&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://guides.emberjs.com/" rel="nofollow" target="_blank" title=""&gt;Ember guides&lt;/a&gt; 和 &lt;a href="http://emberjs.com/api/" rel="nofollow" target="_blank" title=""&gt;Ember API&lt;/a&gt; 的文档可能是我开发生涯中见过的最好技术文档。即使是一个初学者都能很容易理解并上手。Ember 的&lt;a href="https://discuss.emberjs.com/" rel="nofollow" target="_blank" title=""&gt;官方论坛&lt;/a&gt;对于解决疑问也很有帮助。&lt;/p&gt;
&lt;h3 id="最好的构建体验"&gt;最好的构建体验&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://ember-cli.com" rel="nofollow" target="_blank" title=""&gt;Ember-CLI&lt;/a&gt; 是 Ember 的一大杀器。即使 Angular 都试图借鉴 Ember 的这一工具来开发自己的 CLI。使用 ember-cli 你可以快速构建一个预定义好目录文件架构的项目，而这样的架构是经过社区的讨论和实践所验证过的。当项目开始构建时，无论团队是否对此有经验，都完全不用担心有没有遵循最佳实践的问题。对于 React 而言这里就可能存在风险，因为它&lt;a href="http://voidcanvas.com/framework-vs-library-one-better/" rel="nofollow" target="_blank" title=""&gt;只是一个库并非是一个框架&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id="强制性的最佳实践"&gt;强制性的最佳实践&lt;/h3&gt;
&lt;p&gt;即使你的团队中有很棒的开发者，有时候迫于上线的压力他们也会写出坏的代码。而 Ember 会至少在某些层面上强制性地让你采取最佳实践的方式。举个例子，你不应该把业务逻辑写到模版中。Ember 的模版里只可以使用迭代 iteration 和带布尔参数的 helper 帮助方法，这样你就根本别考虑在模版里写业务逻辑的事儿了。&lt;/p&gt;
&lt;h3 id="开发效率的提升"&gt;开发效率的提升&lt;/h3&gt;
&lt;p&gt;我知道，在三个框架里 Ember 可能是学习曲线最陡也最难学的那一个。但是一旦你掌握了它，你就能开发一个项目超快，远胜过 Angular 或 React。“约定大于配置”和 ember-cli 就是最主要的两个原因。&lt;/p&gt;
&lt;h3 id="团队工作"&gt;团队工作&lt;/h3&gt;
&lt;p&gt;如果你公司的所有团队（甚至即使有人不在公司办公）是在开发多个 Ember 项目，使用 ember-cli 构建工具，那么他们每个人的项目目录文件架构会非常相似，而且可以几乎不花费什么时间成本就进行项目的切换，并立刻投入开发和提交代码。这实际变相提高了公司实际上的开发效率。&lt;/p&gt;
&lt;h3 id="不属于公司只属于社区"&gt;不属于公司只属于社区&lt;/h3&gt;
&lt;p&gt;Angular 属于 Google，React 属于 Facebook。而 Ember 来自于社区，也只为了社区。&lt;strong&gt;Ember 核心开发团队的开发者们都来自于各自公司实际 Ember 项目的成员，这恰好是 Ember 的最大不同之处：他们不仅是框架的开发者，更是框架的使用者。这让他们能始终贴近现实，紧接地气。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="友好的版本发布"&gt;友好的版本发布&lt;/h3&gt;
&lt;p&gt;我幸运地在 Angular 2 发布时已经离开了之前的 Angular 项目，但我的朋友要为项目升级到 Angular 2 头疼和抓狂了。与此相反，Ember 2 发布时没有任何变化。是的，你没听错。它没有任何变化。Ember 1.13.0 和 2.0 在使用上完全相同，因为 Ember 是采取渐进式的策略来对 1.x 开放新功能，以便让使用 Ember 的实际项目能进行完全无痛地升级。这正是 Ember 核心团队成员“不仅是框架的开发者，更是框架的使用者”的最好体现。&lt;/p&gt;
&lt;h2 id="Ember 没有缺点了吗？"&gt;Ember 没有缺点了吗？&lt;/h2&gt;
&lt;p&gt;任何事物都有两面性，Ember 也不例外。Ember 也有自己的缺点，诸如陡峭的学习曲线，稍慢的页面渲染，框架体积较大等。已经有很多的技术文章对这些框架进行过比较了。但本文更多的是在开发实践体验上我个人的一些感受。我相信这些框架中不管选用哪一个，就技术而言都能帮你完成项目。但还有一些除此之外的因素影响到你项目的实施和进展。在考虑了各种实际情况、业务、场景之后，你就可以做出最佳的决策来启动项目了。&lt;/p&gt;
&lt;h2 id="相关文章"&gt;相关文章&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="http://voidcanvas.com/react-js-tougher-confusing-ember-angular/" rel="nofollow" target="_blank" title=""&gt;Is React.js tougher and confusing than Ember or Angular?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://voidcanvas.com/complete-guide-ember-ember-js-tutorial/" rel="nofollow" target="_blank" title=""&gt;A complete guide to Ember – Ember.js Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://voidcanvas.com/angular-2-introduction/" rel="nofollow" target="_blank" title=""&gt;What’s new in Angular 2.0? Why it’s rewritten – addressing few confusions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://voidcanvas.com/plus-minus-kind-calculation-comparison-ember-js-template-handlebars/" rel="nofollow" target="_blank" title=""&gt;Plus minus kind of calculation and comparison in Ember.js template, handlebars?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://voidcanvas.com/flux-vs-mvc/" rel="nofollow" target="_blank" title=""&gt;MVC vs Flux – which one is better?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://voidcanvas.com/scaffolding-understanding-ember-application-ember-js-tutorial-part-2/" rel="nofollow" target="_blank" title=""&gt;Scaffolding and understanding an ember application – Ember.js Tutorial part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>apexy</author>
      <pubDate>Sun, 26 Mar 2017 00:28:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/32634</link>
      <guid>https://ruby-china.org/topics/32634</guid>
    </item>
    <item>
      <title>抱怨 GitLab 安装麻烦的可看看这个。。</title>
      <description>&lt;p&gt;看到坛子里有人在抱怨 GitLab 不好安装。&lt;/p&gt;

&lt;p&gt;正好我最近刚写了篇 blog《CentOS6.4 下安装 GitLab5.2》，是自动脚本化安装。脚本也放在了 github 上（地址在 blog 文内），有需要的朋友应该可以借鉴一下。&lt;/p&gt;

&lt;p&gt;BTW，那个帖子里有人回复推荐了一个自动化安装脚本：github.com/mattias-ohlsson/gitlab-installer。正巧我当时也是参考了这个。但是要说一句，它是针对当时 GitLab 5.0 的，现在最新的 5.2 已经不支持了。我的脚本可以支持到 5.2。&lt;/p&gt;

&lt;p&gt;&lt;del&gt;blog url: yxfilm.us/blog/2013/06/centos6-4下安装gitlab5-2/&lt;/del&gt;&lt;/p&gt;

&lt;p&gt;坛子里 url 含中文名会识别错，改用这个：&lt;/p&gt;

&lt;p&gt;blog url: &lt;a href="http://yxfilm.us/blog/archives/2013/06/72" rel="nofollow" target="_blank"&gt;http://yxfilm.us/blog/archives/2013/06/72&lt;/a&gt;&lt;/p&gt;</description>
      <author>apexy</author>
      <pubDate>Sat, 22 Jun 2013 22:22:12 +0800</pubDate>
      <link>https://ruby-china.org/topics/11907</link>
      <guid>https://ruby-china.org/topics/11907</guid>
    </item>
  </channel>
</rss>
