<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Testing | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/testing/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:52 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/testing/index.xml" rel="self" type="application/rss+xml"/><item><title>Integration Tests in a Monorepo | Behind The M.Akita Chronicles</title><link>https://akitaonrails.github.io/en/2026/02/20/integration-tests-in-a-monorepo-behind-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/02/20/integration-tests-in-a-monorepo-behind-the-m-akita-chronicles/</guid><pubDate>Fri, 20 Feb 2026 12:48:45 +0000</pubDate><description>&lt;p&gt;This post is part of a series; follow along via the tag &lt;a href="https://akitaonrails.github.io/en/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. This is part 8.&lt;/p&gt;
&lt;p&gt;And make sure to subscribe to my new newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Unit tests are every developer&amp;rsquo;s comfort zone. You mock everything, isolate everything, run it in milliseconds, and the green in the terminal gives you a false sense of security. Until the day you deploy and discover that service A writes in a format service B can&amp;rsquo;t read. Or that the Hacker News RSS switched from Atom to RSS and your parser blew up. Or that Yahoo Finance started returning 429 after the third ticker because the rate limit that worked in tests doesn&amp;rsquo;t exist in real life.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is part of a series; follow along via the tag <a href="/en/tags/the-m-akita-chronicles/">/themakitachronicles</a>. This is part 8.</p>
<p>And make sure to subscribe to my new newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>Unit tests are every developer&rsquo;s comfort zone. You mock everything, isolate everything, run it in milliseconds, and the green in the terminal gives you a false sense of security. Until the day you deploy and discover that service A writes in a format service B can&rsquo;t read. Or that the Hacker News RSS switched from Atom to RSS and your parser blew up. Or that Yahoo Finance started returning 429 after the third ticker because the rate limit that worked in tests doesn&rsquo;t exist in real life.</p>
<p>Unit tests prove the pieces work in isolation. Integration tests prove the <strong>system</strong> works. And in a project with multiple applications sharing data via the filesystem, the difference between the two is the difference between &ldquo;the code compiles&rdquo; and &ldquo;the newsletter lands in the inbox&rdquo;.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-22-50.jpg" alt="unit tests"  loading="lazy" /></p>
<h2>The Problem: Three Apps, One Filesystem, Zero Guarantees<span class="hx:absolute hx:-mt-20" id="the-problem-three-apps-one-filesystem-zero-guarantees"></span>
    <a href="#the-problem-three-apps-one-filesystem-zero-guarantees" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The project has an unusual architecture: three Rails/Hugo applications in a monorepo, integrated through a shared <code>content/</code> directory:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>akitando-news/
├── marvin-bot/    # Gera conteúdo (Discord bot, AI, scraping)
├── newsletter/    # Monta e envia a newsletter
├── blog/          # Hugo &#43; Hextra → Netlify
├── podcast-tts/   # Servidor TTS (Python/FastAPI)
└── content/       # Markdown &#43; YAML, a cola entre tudo</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>marvin-bot writes Markdown with YAML frontmatter into <code>content/anime/</code>, <code>content/hacker_news/</code>, etc. The newsletter reads those files to assemble the newsletter. The blog serves the same content as static pages. The podcast reads the assembled newsletter to generate the dialogue script.</p>
<p>The contract between these systems is <strong>implicit</strong>. There&rsquo;s no schema. No versioned API. No protobuf. The contract is: &ldquo;the Markdown file has this frontmatter, with these fields, in this structure&rdquo;. If marvin-bot changes the format of a field — say, switches <code>image</code> from string to array — the newsletter breaks silently. No unit test in either app detects it.</p>
<p>That&rsquo;s the fundamental flaw of testing distributed systems with unit tests: <strong>you&rsquo;re testing each side of the contract separately, but never the contract itself</strong>.</p>
<h2>Layer 1: Unit Tests With Paranoid Isolation<span class="hx:absolute hx:-mt-20" id="layer-1-unit-tests-with-paranoid-isolation"></span>
    <a href="#layer-1-unit-tests-with-paranoid-isolation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before talking about integration, I need to explain what the unit tests do in this project — because without that foundation, the integration tests would have nothing to stand on.</p>
<p>The 977 marvin-bot tests run in parallel using every core on the machine. Each test worker has its own directory:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">parallelize</span><span class="p">(</span><span class="ss">workers</span><span class="p">:</span> <span class="ss">:number_of_processors</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">parallelize_setup</span> <span class="k">do</span> <span class="o">|</span><span class="n">worker</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">content_dir</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;tmp&#34;</span><span class="p">,</span> <span class="s2">&#34;test_content_w</span><span class="si">#{</span><span class="n">worker</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to_s</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>But worker-level isn&rsquo;t enough. Inside each worker, multiple tests run sequentially — and if one test leaves a file in <code>content/stories/</code>, the next test might find spurious data. The solution: <strong>one unique directory per test</strong>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">setup</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@base_content_dir</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">content_dir</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@test_content_dir</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="vi">@base_content_dir</span><span class="p">,</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">hex</span><span class="p">(</span><span class="mi">8</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">content_dir</span> <span class="o">=</span> <span class="vi">@test_content_dir</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="no">FileUtils</span><span class="o">.</span><span class="n">mkdir_p</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="vi">@test_content_dir</span><span class="p">,</span> <span class="s2">&#34;stories&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="no">FileUtils</span><span class="o">.</span><span class="n">mkdir_p</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="vi">@test_content_dir</span><span class="p">,</span> <span class="s2">&#34;newsletters&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="no">FileUtils</span><span class="o">.</span><span class="n">mkdir_p</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="vi">@test_content_dir</span><span class="p">,</span> <span class="s2">&#34;images&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">teardown</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">FileUtils</span><span class="o">.</span><span class="n">rm_rf</span><span class="p">(</span><span class="vi">@test_content_dir</span><span class="p">)</span> <span class="k">if</span> <span class="vi">@test_content_dir</span>
</span></span><span class="line"><span class="cl">  <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">content_dir</span> <span class="o">=</span> <span class="vi">@base_content_dir</span> <span class="k">if</span> <span class="vi">@base_content_dir</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pay attention to the teardown line that restores <code>@base_content_dir</code>. Without it, the next test would create its directory <strong>inside</strong> the previous test&rsquo;s directory — <code>tmp/test_content_w3/abc123/def456/</code> — and by test 50 you&rsquo;d have a path 50 levels deep. I learned this burning an entire afternoon debugging why the tests kept getting slower as the suite progressed.</p>
<p>This pattern — worker-level base + per-test subdirectory + restore on teardown — is identical across both apps. And it&rsquo;s what lets you run 977 tests in 7 seconds with 32 parallel workers.</p>
<p>The golden rule: <strong>the test <code>content_dir</code> must NEVER point to the real <code>../content</code></strong>. If it does, the teardown&rsquo;s <code>rm_rf</code> wipes out your production prompts. This isn&rsquo;t hypothetical — it happened during development.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-24-08.jpg" alt="unit tests newsletter"  loading="lazy" /></p>
<h2>Layer 2: The Integration Environment<span class="hx:absolute hx:-mt-20" id="layer-2-the-integration-environment"></span>
    <a href="#layer-2-the-integration-environment" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Unit tests mock everything: external APIs, AI, Discord, SES. That&rsquo;s correct — you don&rsquo;t want to burn $2 in GPT-5.2 tokens every time you run <code>bin/rails test</code>. But it means the tests never exercise the real pipeline.</p>
<p>The project has a third environment beyond development and test: <strong>integration</strong>. It&rsquo;s not test. It&rsquo;s not staging. It&rsquo;s a specific mode for running the full pipeline with real data, using real APIs, but without sending actual emails:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># config/environments/integration.rb</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Jobs rodam inline (perform_later vira perform_now)</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">active_job</span><span class="o">.</span><span class="n">queue_adapter</span> <span class="o">=</span> <span class="ss">:inline</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Emails salvos em arquivo ao invés de enviados via SES</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">action_mailer</span><span class="o">.</span><span class="n">delivery_method</span> <span class="o">=</span> <span class="ss">:file</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">action_mailer</span><span class="o">.</span><span class="n">file_settings</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">location</span><span class="p">:</span> <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;tmp/integration_emails&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The <code>queue_adapter = :inline</code> is crucial. In production, <code>perform_later</code> enqueues the job into SolidQueue and a separate worker executes it. In integration, it runs <strong>right there, in the same process</strong>. That turns the asynchronous production pipeline into a synchronous pipeline you can run, observe, and debug sequentially.</p>
<p>SES is swapped for writing to disk — every email becomes an HTML file in <code>tmp/integration_emails/</code>. After running the pipeline, you open those HTMLs in the browser to visually validate that the email renders correctly.</p>
<h2>Layer 3: DevCache — LLMs Aren&rsquo;t Idempotent<span class="hx:absolute hx:-mt-20" id="layer-3-devcache--llms-arent-idempotent"></span>
    <a href="#layer-3-devcache--llms-arent-idempotent" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s a problem anyone working with AI in production knows: calling GPT-5.2 with the same prompt twice <strong>doesn&rsquo;t return the same answer</strong>. And every call costs money. If you&rsquo;re iterating on the integration pipeline — running, tweaking a prompt, running again — spending $3 per iteration gets expensive fast.</p>
<p>The solution is <code>DevCache</code>: a file-based cache that only activates in the integration env:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">DevCache</span>
</span></span><span class="line"><span class="cl">  <span class="no">CACHE_TTL</span> <span class="o">=</span> <span class="mi">1</span><span class="o">.</span><span class="n">day</span>
</span></span><span class="line"><span class="cl">  <span class="no">CACHE_ENVS</span> <span class="o">=</span> <span class="sx">%w[integration]</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">namespace</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">yield</span> <span class="k">unless</span> <span class="n">enabled?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">path</span> <span class="o">=</span> <span class="n">cache_path</span><span class="p">(</span><span class="n">namespace</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">path</span><span class="o">.</span><span class="n">exist?</span> <span class="o">&amp;&amp;</span> <span class="no">File</span><span class="o">.</span><span class="n">mtime</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="o">&gt;</span> <span class="no">CACHE_TTL</span><span class="o">.</span><span class="n">ago</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="no">JSON</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">path</span><span class="o">.</span><span class="n">read</span><span class="p">,</span> <span class="ss">symbolize_names</span><span class="p">:</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="k">yield</span>
</span></span><span class="line"><span class="cl">    <span class="n">path</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="no">JSON</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">result</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In production and in test, <code>DevCache.fetch</code> runs the block directly. In integration, it saves the result as JSON and returns the cached value on the next run. TTL of 1 day — enough to iterate within the same day without recomputing, short enough to not serve stale data.</p>
<p>The generation code uses it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">summary</span> <span class="o">=</span> <span class="no">DevCache</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;summarize&#34;</span><span class="p">,</span> <span class="n">url_key</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">AiChat</span><span class="o">.</span><span class="n">summarize</span><span class="p">(</span><span class="n">article</span><span class="o">[</span><span class="ss">:content</span><span class="o">]</span><span class="p">,</span> <span class="ss">source_url</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>First run: calls GPT-5.2, pays $0.02, saves to <code>tmp/dev_cache/summarize/{md5}.json</code>. Second run: reads from the cache, zero cost. <code>FORCE=1</code> busts the cache when you want to force regeneration — as an explicit flag, not a default.</p>
<p>DevCache exists in both apps (marvin-bot and newsletter) with identical implementation. When you run <code>integration:clean</code>, both caches are wiped.</p>
<p>The point: <strong>LLM caching in a development environment is a requirement, not an optimization</strong>. Without it, iterating on a prompt that needs 8 API calls becomes financially unworkable.</p>
<h2>Layer 4: Real Data via Rsync<span class="hx:absolute hx:-mt-20" id="layer-4-real-data-via-rsync"></span>
    <a href="#layer-4-real-data-via-rsync" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is where most projects stop. They have unit tests, maybe a staging environment, and ship it. The problem: staging uses test data. Test data is <strong>too clean</strong>. It doesn&rsquo;t have the edge cases real data has:</p>
<ul>
<li>Articles with titles in Japanese (anime ranking)</li>
<li>URLs that redirect 3 times before reaching the destination</li>
<li>Images that return 200 with Content-Type <code>text/html</code> (CloudFlare error page)</li>
<li>Previous newsletters with slightly different frontmatter formats</li>
<li>Stories with <code>score: null</code> because they were created before the field existed</li>
</ul>
<p>The solution: <strong>rsync from production</strong>.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Limpa conteúdo gerado (preserva stories e imagens de produção)</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> marvin-bot <span class="o">&amp;&amp;</span> bin/rails integration:clean
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sincroniza content/ de produção pra local</span>
</span></span><span class="line"><span class="cl">rsync -avz --delete production:/path/to/content/ ../content/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Roda o pipeline completo com dados reais</span>
</span></span><span class="line"><span class="cl"><span class="nv">RAILS_ENV</span><span class="o">=</span>integration bin/rails integration:pipeline</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>integration:clean</code> is surgical: it removes generated directories (<code>anime/</code>, <code>hacker_news/</code>, <code>newsletters/</code>, <code>podcasts/</code>) but preserves <code>stories/</code> and <code>images/</code> — which are user-submitted data coming in via Discord. After the rsync, the local directory is an exact copy of production.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-25-37.jpg" alt="cleanup"  loading="lazy" /></p>
<p>The pipeline then generates every section from that real data. If a parser breaks on a Korean title, you find out here — not in production at 5pm on a Sunday when the 8 jobs run in parallel.</p>
<h2>Layer 5: The Full Pipeline<span class="hx:absolute hx:-mt-20" id="layer-5-the-full-pipeline"></span>
    <a href="#layer-5-the-full-pipeline" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The heart of the integration system is <code>integration:pipeline</code>. It simulates the entire production week in a single run:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">RAILS_ENV</span><span class="o">=</span>integration bin/rails integration:pipeline</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The pipeline runs in waves, honoring dependencies:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Wave 1 (paralelo, 8 jobs):
  Book, Holidays, History, Geek History,
  Anime, Hacker News, YouTube, World Events

Wave 2 (depende de World Events):
  Market Recap

Wave 3 (depende de tudo):
  Closing Remarks

Preflight → Newsletter Assembly → Podcast</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-26-13.jpg" alt="waves"  loading="lazy" /></p>
<p>Each wave runs its jobs in parallel threads, exactly like production does with SolidQueue. Parallel execution isn&rsquo;t an implementation detail here — it&rsquo;s a test feature. If two jobs try to write to the same file, or if one job reads a file another is in the middle of writing, the bug shows up here.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">threads</span> <span class="o">=</span> <span class="n">to_run</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">klass</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="no">Thread</span><span class="o">.</span><span class="n">new</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">connection_pool</span><span class="o">.</span><span class="n">with_connection</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">run_single_job</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">klass</span><span class="p">,</span> <span class="n">target_date</span><span class="p">,</span> <span class="n">results</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="n">threads</span><span class="o">.</span><span class="n">each</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:join</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>with_connection</code> is necessary because each thread needs its own SQLite connection — without it, connections leak and the pool gets exhausted.</p>
<p>At the end, the pipeline prints a status table and a billing summary:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-34-56.jpg" alt="billing"  loading="lazy" /></p>
<p>That billing summary isn&rsquo;t vanity — it&rsquo;s cost control. If a refactored prompt triples the output tokens, you see it here before going to production and burning 10x on a Sunday with 200 stories.</p>
<h2>Layer 6: Preflight — Automated Structural Validation<span class="hx:absolute hx:-mt-20" id="layer-6-preflight--automated-structural-validation"></span>
    <a href="#layer-6-preflight--automated-structural-validation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After generating every section, the pipeline runs <code>ContentPreflight</code>. It doesn&rsquo;t validate the <em>content</em> — it validates the <em>structure</em>. Each section gets checked:</p>
<ul>
<li><strong>Does the file exist?</strong> If <code>content/anime/2026-02-16.md</code> doesn&rsquo;t exist, something failed.</li>
<li><strong>Minimum item count?</strong> The anime section needs at least 3 items. If it has 1, the generator had a problem.</li>
<li><strong>Required markers?</strong> The Markdown must contain <code>[COMMENTARY]</code> and <code>[AKITA]</code> — without them, newsletter assembly doesn&rsquo;t know where to cut.</li>
<li><strong>Specific data?</strong> Market recap needs at least 5 ticker rows.</li>
</ul>
<p>The result is a status per section: <code>pass</code>, <code>degraded</code>, <code>fail</code>, or <code>skip</code>. The pipeline uses this to decide whether the newsletter can be assembled automatically or needs human intervention:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-36-19.jpg" alt="preflight"  loading="lazy" /></p>
<p>In production, this result goes to Discord as an embed. In the integration pipeline, it&rsquo;s printed to the terminal. The logic is the same — the notification channel changes.</p>
<h2>Layer 7: Cross-App — Newsletter Assembly<span class="hx:absolute hx:-mt-20" id="layer-7-cross-app--newsletter-assembly"></span>
    <a href="#layer-7-cross-app--newsletter-assembly" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s where the monorepo pays dividends. marvin-bot needs to invoke the newsletter to assemble the final newsletter. In production, these are different servers. In the integration pipeline, it&rsquo;s a <code>system()</code> call:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">env_vars</span> <span class="o">=</span> <span class="s2">&#34;RAILS_ENV=integration CONTENT_DIR=</span><span class="si">#{</span><span class="no">Shellwords</span><span class="o">.</span><span class="n">escape</span><span class="p">(</span><span class="n">content_dir</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">cmd</span> <span class="o">=</span> <span class="s2">&#34;cd </span><span class="si">#{</span><span class="n">newsletter_dir</span><span class="si">}</span><span class="s2"> &amp;&amp; </span><span class="si">#{</span><span class="n">env_vars</span><span class="si">}</span><span class="s2"> bin/rails integration:generate_all&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">system</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-38-17.jpg" alt="assembly"  loading="lazy" /></p>
<p>The newsletter reads the files marvin-bot just generated in <code>content/</code>, assembles the newsletter, and saves the result in <code>content/newsletters/</code>. CONTENT_DIR is passed explicitly — both apps point to the same directory.</p>
<p>If the frontmatter format changed and the newsletter can&rsquo;t parse it, the error shows up here. If the newsletter&rsquo;s <code>SectionParser</code> has diverged from marvin-bot&rsquo;s <code>SectionParser</code> (they&rsquo;re identical copies, but maintained by hand), here&rsquo;s where you find out.</p>
<p>After the newsletter, the podcast: the pipeline invokes <code>podcast:integration</code>, which generates the dialogue script from the assembled newsletter, synthesizes audio via TTS, and builds the final MP3. It&rsquo;s the longest pipeline — from Markdown newsletter to Spotify-ready MP3, passing through a two-pass LLM and per-turn voice synthesis.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-47-52.jpg" alt="tts"  loading="lazy" /></p>
<h2>The Value of Testing With Production Data<span class="hx:absolute hx:-mt-20" id="the-value-of-testing-with-production-data"></span>
    <a href="#the-value-of-testing-with-production-data" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let me give concrete examples of bugs that only showed up with real data:</p>
<p><strong>1. Japanese anime titles broke the slugify.</strong>
<code>ContentWriter.slugify</code> used Rails&rsquo; <code>parameterize</code>, which strips non-ASCII characters. Title: &ldquo;進撃の巨人 Season 4&rdquo; → slug: &ldquo;season-4&rdquo;. Two anime with the subtitle &ldquo;Season 4&rdquo; collided on the filesystem. It never showed up in tests because the fixtures used English titles.</p>
<p><strong>2. The HackerNews parser assumed RSS, but the feed switched to Atom.</strong>
<code>rss/channel/item</code> became <code>feed/entry</code>. Unit tests mocked the XML. The parser itself was never tested against the real feed. Running integration with the real rsynced feed, the parser returned zero items — caught by preflight as <code>fail</code>.</p>
<p><strong>3. Yahoo Finance returned 429 after the 5th ticker.</strong>
Rate limiting didn&rsquo;t exist in tests (HTTP was mocked). In the real integration, the first 5 tickers went through, then everything failed. The fix — sleeping between requests — would never have been implemented without real data exposing the problem.</p>
<p><strong>4. Images with a valid URL but the wrong Content-Type.</strong>
A CDN returned 200 for image URLs, but with an HTML body (Cloudflare error page). <code>UrlValidator.reachable?</code> returned <code>true</code>. <code>ImageProcessor.download_image</code> downloaded the HTML and tried to resize it. mini_magick crashed with an obscure error. The fix: validate Content-Type on the HEAD response — but this scenario would never surface with mocked URLs.</p>
<p><strong>5. Newsletter assembly failed on stories with <code>score: null</code>.</strong>
Old stories, created before the <code>score</code> field had a default, had <code>nil</code> in the frontmatter. Assembly tried to do <code>sort_by { |s| s[:frontmatter][&quot;score&quot;] }</code> — <code>nil &lt;=&gt; &quot;high&quot;</code> raises <code>ArgumentError</code>. It only appeared when I rsynced production data that had 3 months of accumulated stories.</p>
<p>None of these bugs would be caught by a unit test. All of them were caught by the integration pipeline with real data.</p>
<h2>Practical Tips<span class="hx:absolute hx:-mt-20" id="practical-tips"></span>
    <a href="#practical-tips" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>1. Separate Cost From Fidelity<span class="hx:absolute hx:-mt-20" id="1-separate-cost-from-fidelity"></span>
    <a href="#1-separate-cost-from-fidelity" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The full integration pipeline costs around $0.40 in API calls. Not much, but if you run it 10 times in a day while iterating, that&rsquo;s $4. DevCache solves this: the first run pays, the following runs are free.</p>
<p>But there are scenarios where you <strong>want</strong> to pay again — when you change a prompt and need to see the real result. <code>FORCE=1</code> busts the cache selectively. And <code>SKIP_EXISTING=1</code> skips sections that have already been generated, saving time when you only want to re-run a specific section.</p>
<h3>2. The Integration Environment Is Not Staging<span class="hx:absolute hx:-mt-20" id="2-the-integration-environment-is-not-staging"></span>
    <a href="#2-the-integration-environment-is-not-staging" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Staging is a server running 24/7 simulating production. The integration environment is a <strong>local execution mode</strong> you turn on when needed and turn off when you&rsquo;re done. No dedicated infrastructure required. No separate database required. Same code, same machine, different flags.</p>
<p>The difference is philosophical: staging tests &ldquo;does the system work on the staging server?&rdquo;. Integration tests &ldquo;does the system work <strong>with this specific data</strong>?&rdquo;. The first is environment-dependent. The second is data-dependent. And data bugs are more common than environment bugs.</p>
<h3>3. Clean Before Rsync, Not After<span class="hx:absolute hx:-mt-20" id="3-clean-before-rsync-not-after"></span>
    <a href="#3-clean-before-rsync-not-after" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Order matters: clean the generated content <strong>before</strong> syncing from production. If you do the opposite — rsync first, clean after — you lose the production data you just downloaded.</p>
<p><code>integration:clean</code> is surgical by design. It removes anime/, hacker_news/, newsletters/ (generated), but preserves stories/ and images/ (production). That enables an iterative workflow: rsync once, run the pipeline, tweak a prompt, run again — without re-downloading 500MB of content.</p>
<h3>4. Preflight Is Your Smoke Test<span class="hx:absolute hx:-mt-20" id="4-preflight-is-your-smoke-test"></span>
    <a href="#4-preflight-is-your-smoke-test" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Don&rsquo;t trust &ldquo;ran without exception = correct&rdquo;. A generation job can run, produce a Markdown with 2 items instead of 10, and return success. Preflight catches that by verifying minimum counts, required markers, and expected structure.</p>
<p>In practice, every LLM bug I&rsquo;ve found was caught by preflight: truncated responses, wrong format, missing sections. GPT-5.2 is good, but it isn&rsquo;t deterministic — and when it fails, it fails in creative ways no schema validation would catch.</p>
<h3>5. Billing Summary Is Not Optional<span class="hx:absolute hx:-mt-20" id="5-billing-summary-is-not-optional"></span>
    <a href="#5-billing-summary-is-not-optional" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you&rsquo;re using LLMs in production, <strong>every</strong> test pipeline needs to report cost. Not for accounting — for engineering. A prompt that works but costs 3x more than the previous one is a performance bug. Input and output tokens per provider, cost per job, total cost — all visible on every run.</p>
<h3>6. Monorepo Helps, But Doesn&rsquo;t Solve<span class="hx:absolute hx:-mt-20" id="6-monorepo-helps-but-doesnt-solve"></span>
    <a href="#6-monorepo-helps-but-doesnt-solve" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Having the three apps in the same repository helps: a <code>git bisect</code> covers changes on both sides of the contract. A PR that changes the frontmatter format in marvin-bot <strong>can</strong> also update the parser in newsletter — in the same commit.</p>
<p>But &ldquo;can&rdquo; isn&rsquo;t &ldquo;guarantees&rdquo;. The contract between the apps is file-based and implicit. There&rsquo;s no shared type, no common interface. <code>SectionParser</code> is copied manually between the two apps. If someone edits one and forgets the other, they diverge. The integration test is the <strong>only</strong> verification that both sides agree.</p>
<h3>7. Preview Before Sending<span class="hx:absolute hx:-mt-20" id="7-preview-before-sending"></span>
    <a href="#7-preview-before-sending" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The pipeline doesn&rsquo;t end at generation. The newsletter has a <code>newsletter:preview</code> that renders the final result as HTML in light and dark mode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> newsletter
</span></span><span class="line"><span class="cl"><span class="nv">FILE</span><span class="o">=</span>../content/newsletters/2026-02-16.md bin/rails newsletter:preview
</span></span><span class="line"><span class="cl"><span class="c1"># → tmp/preview/newsletter_light.html</span>
</span></span><span class="line"><span class="cl"><span class="c1"># → tmp/preview/newsletter_dark.html</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Opening those HTMLs in the browser is the visual integration test. The Markdown can be perfect, the frontmatter correct, every field present — but an anime image shows up stretched because the aspect ratio changed. Or Marvin&rsquo;s commentary block doesn&rsquo;t have enough contrast in dark mode. Those bugs are visual — no automated test catches them.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-48-32.jpg" alt="final assembly"  loading="lazy" /></p>
<h3>8. The CI Pipeline Is Different From the Integration Pipeline<span class="hx:absolute hx:-mt-20" id="8-the-ci-pipeline-is-different-from-the-integration-pipeline"></span>
    <a href="#8-the-ci-pipeline-is-different-from-the-integration-pipeline" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>These are different things. CI runs on every commit:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">bin/ci  <span class="c1"># rubocop + bundler-audit + brakeman + tests (~22s)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Integration runs when you want to validate the pipeline end-to-end — before a deploy, after changing prompts, or after rsyncing fresh data from production. CI is fast and cheap. Integration is slow and costs money. Both are necessary.</p>
<h2>The Cost of Not Having Integration<span class="hx:absolute hx:-mt-20" id="the-cost-of-not-having-integration"></span>
    <a href="#the-cost-of-not-having-integration" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let me be direct: if this project didn&rsquo;t have the integration pipeline, I would have shipped at least 3 broken newsletters.</p>
<p>One with the anime section missing because the XML parser assumed RSS and the feed was Atom. One with every YouTube image pointing to 404 because the thumbnail URL pattern changed. And one with the market recap showing last month&rsquo;s tickers because Yahoo Finance blocked the requests and the job failed silently.</p>
<p>Unit tests would have passed in all those scenarios. CI would have gone green. The deploy would have shipped. And at 7am on a Monday, 300 people would have received a newsletter full of holes.</p>
<p>The integration pipeline is expensive — in setup time, in API cost, in maintenance complexity. But it&rsquo;s orders of magnitude cheaper than sending broken content to real subscribers. Especially when you only get one shot a week.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The testing hierarchy in this project is:</p>
<ol>
<li><strong>Unit tests</strong> (1,330 across both apps, 7 seconds in parallel, $0): prove each piece works in isolation</li>
<li><strong>CI pipeline</strong> (rubocop + audit + brakeman + tests, ~22 seconds, $0): prove the code is healthy</li>
<li><strong>Integration pipeline</strong> (real data, real APIs, ~3 minutes, ~$0.40): proves the <strong>system</strong> works</li>
<li><strong>Visual preview</strong> (HTML in the browser, manual, $0): proves the final result is presentable</li>
</ol>
<p>Each layer catches a different type of bug. None of them is enough alone. And the one that surprises most — the one that catches the bugs you swore didn&rsquo;t exist — is the third: real data, real pipeline, no mocks.</p>
<p>The confidence you have in your system is proportional to how real the data in your tests is. Mock everything and you get 100% coverage and zero confidence. Test with production data and you get the truth — however inconvenient.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>testing</category><category>software-engineering</category></item><item><title>[SmallTips] Running GitLab CI Runner Locally</title><link>https://akitaonrails.github.io/en/2018/04/28/smalltips-running-gitlab-ci-runner-locally/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2018/04/28/smalltips-running-gitlab-ci-runner-locally/</guid><pubDate>Sat, 28 Apr 2018 18:35:00 -0300</pubDate><description>&lt;p&gt;If there is one thing you should always do, it&amp;rsquo;s maintain your test suite. Every bug you fix, every new feature, you should add new tests.&lt;/p&gt;
&lt;p&gt;I have been working very hard the past few weeks, quite literally coding non-stop, 7 days a week. Remember that &amp;ldquo;stop complaining you don&amp;rsquo;t have time, what do you do from midnight to 6 AM?&amp;rdquo; thing? Yeah, won&amp;rsquo;t apply to me.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s exactly when you&amp;rsquo;re sleep deprived and exhausted that you start to make mistakes. Even though your code runs, locally, you will shoot yourself in the foot in one of those 3 AM coding spree sessions.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If there is one thing you should always do, it&rsquo;s maintain your test suite. Every bug you fix, every new feature, you should add new tests.</p>
<p>I have been working very hard the past few weeks, quite literally coding non-stop, 7 days a week. Remember that &ldquo;stop complaining you don&rsquo;t have time, what do you do from midnight to 6 AM?&rdquo; thing? Yeah, won&rsquo;t apply to me.</p>
<p>And it&rsquo;s exactly when you&rsquo;re sleep deprived and exhausted that you start to make mistakes. Even though your code runs, locally, you will shoot yourself in the foot in one of those 3 AM coding spree sessions.</p>
<p>The test suite was really my sidekick, co-pilot, bringing me back to my senses whenever the night got too dark.</p>
<p>I maintain my own GitLab custom server for all of my company&rsquo;s client and internal projects. Just a bit of paranoia, to make sure my code is owned by me alone. And GitLab is a fantastic tool. Even more so because it has <a href="https://about.gitlab.com/features/gitlab-ci-cd/"target="_blank" rel="noopener">its own Continuous Integration companion</a>.</p>
<p>Just add your <code>.gitlab-ci.yml</code> file to your project, create a new branch, push it and create a Merge Request and the CI kicks in, with multiple <a href="https://docs.gitlab.com/ee/ci/yaml/#jobs"target="_blank" rel="noopener">parallel jobs</a>  if you want. Even when I was forgetting to run my tests, the CI would not let me fail.</p>
<p>Keeping a CI up and running does require you to stop and maintain your specs though. Sometimes you will wonder, why does a test run on your local machine but keeps failing in the CI server?</p>
<p>That&rsquo;s when it&rsquo;s super useful to run the CI docker image locally, to iron out remaining environment-dependent quirks.</p>
<p>And you can do just that by running the GitLab CI Runner itself. It will pick up your project&rsquo;s <code>.gitlab-ci.yml</code> and run it locally through docker. So whatever problem you&rsquo;re seeing on the server will quite definitely happen locally as well. With the added benefit that you don&rsquo;t need to wait in line in case there is a queue of jobs waiting to run (many developers working, not as many machines to chew the CI work immediately). And you won&rsquo;t be the one adding useless jobs to the same queue.</p>
<p>And, if you&rsquo;ve followed my recommendations and installed the awesome Arch Linux distro (or my personal favorite derivative, Manjaro Gnome) you can easily install the runner through AUR like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pacaur -S gitlab-runner</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Otherwise you will have to check your particular distro repos or download the binary from <a href="https://gitlab.com/gitlab-org/gitlab-runner/blob/master/docs/install/bleeding-edge.md#download-the-standalone-binaries"target="_blank" rel="noopener">here</a>. For example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>wget https://gitlab-runner-downloads.s3.amazonaws.com/master/binaries/gitlab-runner-linux-amd64
sudo mv gitlab-runner-linux-amd64 /usr/local/bin
sudo chmod &#43;x /usr/local/bin/gitlab-runner</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Once you have it installed, let&rsquo;s say you have a snippet like this in your <code>.gitlab-ci.yml</code> configuration:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">./bin/cc-test-reporter before-build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">bundle exec rspec --exclude-pattern &#34;**/features/**/*_spec.rb&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">after_script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">./bin/cc-test-reporter after-build --exit-code $? || true</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can configure many test jobs to run in parallel. I recommend that you separate front-end related JS tests, Rspec/minitest back-end unit tests, Capybara-based feature tests, Brakeman related security checks, for instance. Each will run in parallel and you won&rsquo;t have to wait for everything to run if you&rsquo;re only interested in the JS tests, for example. So you have faster feedback and can start fixing immediately.</p>
<p>Locally, from your project root directory, just run:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>gitlab-runner exec docker backend</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Of course, this is assuming that you already have Docker installed and properly configured. If you don&rsquo;t, make sure you read <a href="https://wiki.archlinux.org/index.php/Docker#Installation"target="_blank" rel="noopener">Arch Wiki&rsquo;s excellent entry on Docker</a> but it&rsquo;s basically installing the Docker package and adding yourself to the docker group.</p>
<p>This is it! Super easy, barely an inconvenience, and runner will do everything your GitLab CI server is doing.</p>
<p>One caveat is that unfortunately, it seems like you <a href="https://gitlab.com/gitlab-org/gitlab-runner/issues/2409"target="_blank" rel="noopener">don&rsquo;t have the ability to save cache or artifacts</a> between job runs (gems, npm packages, etc) so every time this local runner runs, it will start from scratch. This makes it less useful to run all the time, but it&rsquo;s still faster than pushing to the server and waiting in line for your turn in a busy server.</p>
<p>Saved me a couple times already.</p>
]]></content:encoded><category>devops</category><category>containers</category><category>testing</category></item><item><title>[Beginner] Long live PhantomJS, let's use Chrome Headless now</title><link>https://akitaonrails.github.io/en/2017/10/31/beginner-long-live-phantomjs-let-s-use-chrome-headless-now/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/10/31/beginner-long-live-phantomjs-let-s-use-chrome-headless-now/</guid><pubDate>Tue, 31 Oct 2017 19:48:00 -0200</pubDate><description>&lt;p&gt;If you do Feature Specs, the act of loading up a real app server and then a real headless browser to do real user feature testing, then you know &lt;a href="https://github.com/teamcapybara/capybara/issues/1860"target="_blank" rel="noopener"&gt;Capybara&lt;/a&gt; and one of its most well-known drivers, &lt;a href="https://github.com/teampoltergeist/poltergeist/issues/882"target="_blank" rel="noopener"&gt;Poltergeist&lt;/a&gt;. Poltergeist wraps up PhantomJS, which is a well known WebKit-based headless browser.&lt;/p&gt;
&lt;p&gt;But WebKit is known for being &lt;strong&gt;very&lt;/strong&gt; complicated to deal with. So I can only imagine the nightmare to maintain PhantomJS, which is akin to maintaining a full-blown web browser like Chrome or Safari.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If you do Feature Specs, the act of loading up a real app server and then a real headless browser to do real user feature testing, then you know <a href="https://github.com/teamcapybara/capybara/issues/1860"target="_blank" rel="noopener">Capybara</a> and one of its most well-known drivers, <a href="https://github.com/teampoltergeist/poltergeist/issues/882"target="_blank" rel="noopener">Poltergeist</a>. Poltergeist wraps up PhantomJS, which is a well known WebKit-based headless browser.</p>
<p>But WebKit is known for being <strong>very</strong> complicated to deal with. So I can only imagine the nightmare to maintain PhantomJS, which is akin to maintaining a full-blown web browser like Chrome or Safari.</p>
<p>So it&rsquo;s no wonder that when the Chrome team announced the availability of the <a href="https://developers.google.com/web/updates/2017/04/headless-chrome"target="_blank" rel="noopener">Chrome Driver</a>, then the maintainer of PhantomJS <a href="https://github.com/teampoltergeist/poltergeist/issues/882"target="_blank" rel="noopener">decided to step down</a>.</p>
<p>If you know the contributors of PhantomJS, say thank you, as it helped us build more solid user features.</p>
<p>That being said, fear not. You can easily replace Poltergeist/PhantomJS with Selenium WebDriver/Chrome Driver in your RSpec/Capybara setup.</p>
<p>My friend <a href="https://www.lucascaton.com.br/2017/06/22/how-to-run-your-feature-specs-using-capybara-and-headless-chrome/"target="_blank" rel="noopener">Lucas Caton</a> wrote about it in June this year. Follow his blog as well.</p>
<p>If you&rsquo;re using Linux with the Chromium browser, you don&rsquo;t need to install anything, as the Chrome Driver comes with Chromium. Otherwise, you need to install the proper packages for your operating system. For example, <code>brew install chromedriver</code> for OS X or <code>pacaur -S chromedriver</code> on Arch if you don&rsquo;t like to have Chromium around. You may need to <a href="https://askubuntu.com/questions/539498/where-does-chromedriver-install-to"target="_blank" rel="noopener">tweak your PATH on Ubuntu</a> though.</p>
<p>Rule of thumb: install Chromium.</p>
<p>In my case, this is what had to change:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"># Gemfile
</span></span><span class="line"><span class="cl"><span class="gd">- gem &#34;poltergeist&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ gem &#34;selenium-webdriver&#34;
</span></span></span><span class="line"><span class="cl"><span class="gi">+ gem &#34;rspec-retry&#34;
</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then in the Capybara setup:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">Capybara.server = :puma # Until your setup is working
</span></span><span class="line"><span class="cl">Capybara.server = :puma, { Silent: true } # To clean up your test output
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gd">- Capybara.register_driver :poltergeist do |app|
</span></span></span><span class="line"><span class="cl"><span class="gd">-   options = {
</span></span></span><span class="line"><span class="cl"><span class="gd">-     timeout: 3.minutes,
</span></span></span><span class="line"><span class="cl"><span class="gd">-     phantomjs_options: [
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--proxy-type=none&#39;,
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--load-images=no&#39;,
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--ignore-ssl-errors=yes&#39;,
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--ssl-protocol=any&#39;,
</span></span></span><span class="line"><span class="cl"><span class="gd">-       &#39;--web-security=false&#39;
</span></span></span><span class="line"><span class="cl"><span class="gd">-     ]
</span></span></span><span class="line"><span class="cl"><span class="gd">-   }
</span></span></span><span class="line"><span class="cl"><span class="gd">-   Capybara::Poltergeist::Driver.new(app, options)
</span></span></span><span class="line"><span class="cl"><span class="gd">- end
</span></span></span><span class="line"><span class="cl"><span class="gd">- Capybara.javascript_driver = :poltergeist
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gi">+ Capybara.register_driver :chrome do |app|
</span></span></span><span class="line"><span class="cl"><span class="gi">+   capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
</span></span></span><span class="line"><span class="cl"><span class="gi">+     chromeOptions: {
</span></span></span><span class="line"><span class="cl"><span class="gi">+       args: %w[ no-sandbox headless disable-popup-blocking disable-gpu window-size=1280,1024]
</span></span></span><span class="line"><span class="cl"><span class="gi">+     }
</span></span></span><span class="line"><span class="cl"><span class="gi">+   )
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 
</span></span></span><span class="line"><span class="cl"><span class="gi">+   Capybara::Selenium::Driver.new(app, browser: :chrome, desired_capabilities: capabilities)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ end
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 
</span></span></span><span class="line"><span class="cl"><span class="gi">+ Capybara::Screenshot.register_driver :chrome do |driver, path|
</span></span></span><span class="line"><span class="cl"><span class="gi">+   driver.save_screenshot(path)
</span></span></span><span class="line"><span class="cl"><span class="gi">+ end
</span></span></span><span class="line"><span class="cl"><span class="gi">+ 
</span></span></span><span class="line"><span class="cl"><span class="gi">+ Capybara.javascript_driver = :chrome
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Capybara.default_max_wait_time = 5 # you may want to increase this timeout if your app is heavy to load
</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In feature specs, sometimes Rails itself takes a long while to load up, compile assets, etc., and the first feature spec may time out. To avoid a failure in the test run, it&rsquo;s recommended to add the <code>rspec-retry</code> gem, as I did above, and add the following to your <code>spec/rails_helper.rb</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rspec/retry&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="no">RSpec</span><span class="o">.</span><span class="n">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># show retry status in spec process</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">verbose_retry</span> <span class="o">=</span> <span class="kp">true</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Try twice (retry once)</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">default_retry_count</span> <span class="o">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Only retry when Selenium raises Net::ReadTimeout</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">exceptions_to_retry</span> <span class="o">=</span> <span class="o">[</span><span class="no">Net</span><span class="o">::</span><span class="no">ReadTimeout</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that should be it. I didn&rsquo;t have to touch any of my feature specs and they all ran beautifully. So kudos to the respective teams that maintain Capybara, Selenium-WebDriver for supporting this.</p>
<p>If you&rsquo;re a Node.js developer as well, you probably used something like Casper, which is said to support Chrome Headless as well. But while we&rsquo;re at it, you should check out <a href="https://github.com/GoogleChrome/puppeteer"target="_blank" rel="noopener">Puppeteer</a> as well, from the Google team itself. It is a promise-based library where you can code like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">puppeteer</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;puppeteer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">browser</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">puppeteer</span><span class="p">.</span><span class="nx">launch</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="kr">goto</span><span class="p">(</span><span class="s1">&#39;https://example.com&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">screenshot</span><span class="p">({</span><span class="nx">path</span><span class="o">:</span> <span class="s1">&#39;example.png&#39;</span><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">})();</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>So yeah, Chrome Headless seems like a very good option as most users actually use the Chrome browser, so it means we should have more reliable feature specs and also web crawling tools.</p>
]]></content:encoded><category>rails</category><category>testing</category><category>tutorials</category></item><item><title>How NOT to zero out your Pusher development quota</title><link>https://akitaonrails.github.io/en/2017/10/27/how-not-to-zero-out-your-pusher-development-quota/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/10/27/how-not-to-zero-out-your-pusher-development-quota/</guid><pubDate>Fri, 27 Oct 2017 13:43:00 -0200</pubDate><description>&lt;p&gt;If you&amp;rsquo;re doing development with WebSockets (real-time notifications, real-time chat, etc), one of the best SaaS options out there is still &lt;a href="pusher.com"&gt;&lt;strong&gt;Pusher&lt;/strong&gt;&lt;/a&gt;. It was always reliable.&lt;/p&gt;
&lt;p&gt;For each application you create, it even offers you separate development, staging, and production environments, with separate key/secret tokens.&lt;/p&gt;
&lt;p&gt;One problem I stumbled upon these days is that I was quickly zeroing out the free development environment message quota (200,000 messages a day). That&amp;rsquo;s because all my team was using the same key and also the Continuous Integration server was doing live connections whenever it ran in the same environment. That can quickly consume all the quota you have and block both your development and CI.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If you&rsquo;re doing development with WebSockets (real-time notifications, real-time chat, etc), one of the best SaaS options out there is still <a href="pusher.com"><strong>Pusher</strong></a>. It was always reliable.</p>
<p>For each application you create, it even offers you separate development, staging, and production environments, with separate key/secret tokens.</p>
<p>One problem I stumbled upon these days is that I was quickly zeroing out the free development environment message quota (200,000 messages a day). That&rsquo;s because all my team was using the same key and also the Continuous Integration server was doing live connections whenever it ran in the same environment. That can quickly consume all the quota you have and block both your development and CI.</p>
<p>It&rsquo;s actually not a good practice to do live connections to external systems in testing situations. The tests can fail randomly for a number of reasons, so we should always mock those. But mocking a complex system (WebSockets and HTTP) like Pusher is not trivial.</p>
<p>Fortunately, I found this <a href="https://github.com/tristandunn/pusher-fake"target="_blank" rel="noopener">Pusher Fake</a>. It basically implements all the necessary APIs and WebSocket endpoints to mimic Pusher and fool both server and js/client to communicate with it.</p>
<p>The idea is for your Rails app, in development mode, to fork a separate server process for the Pusher client to connect to. This gem is both a simple Pusher-clone server and a series of wrappers to load it up in your setup.</p>
<p>First things first.</p>
<p>In your application, you will have both a Ruby side Pusher client connection setup to push messages to a channel in the Pusher server. And a Javascript, client-side, Pusher instance mainly to subscribe to a channel in a WebSocket connection and receive messages.</p>
<p>First we need to set up the Ruby side. Usually it&rsquo;s in a <code>config/initializers/pusher.rb</code> configuration like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">app_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_APP&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">key</span>    <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">secret</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_SECRET&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">cluster</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># never set Pusher.host or Pusher.port</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Notice that I am using environment variables to hold the configuration. You should use something like the <a href="https://github.com/laserlemon/figaro"target="_blank" rel="noopener">figaro gem</a> or the <a href="https://github.com/bkeepers/dotenv"target="_blank" rel="noopener">dotenv-rails gem</a>. For example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">PUSHER_APP</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;xpto&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_KEY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;abcd1234&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_SECRET</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;abcd1234&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_CLUSTER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;us2&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>At the very least you must have an application ID, a key, a secret, and a cluster name. All of these are provided by Pusher whenever you register a new application there.</p>
<p>Second, we need to set up the Javascript instance. Usually, you have something in the <code>assets/javascripts</code> directory like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// .js.erb example
</span></span></span><span class="line"><span class="cl"><span class="nb">window</span><span class="p">.</span><span class="nx">pusher</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pusher</span><span class="p">(</span><span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_APP] %&gt;, {
</span></span></span><span class="line"><span class="cl"><span class="s1">  cluster: &lt;%= ENV[&#39;</span><span class="nx">PUSHER_KEY</span><span class="s1">&#39;] %&gt;,
</span></span></span><span class="line"><span class="cl"><span class="s1">  encrypted: &lt;%= ENV[&#39;</span><span class="nx">PUSHER_ENCRYPTED</span><span class="err">&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">})</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In a Chrome Development Console, you can inspect this instance by typing:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Pusher.instances[0]</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This way you can make sure it&rsquo;s picking up the correct configurations for the connection and also debug problems.</p>
<p>The dependencies are the pusher gem in your <code>Gemfile</code> and the javascript client.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Gemfile</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;pusher&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In the case of the javascript file you can either add it to your project and rely on <a href="http://www.akitaonrails.com/en/2017/10/24/iniciante-experimentando-o-rails-5-1-x"target="_blank" rel="noopener">Webpacker</a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yarn add pusher</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then, in your ES6 javascript file, you can do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">Pusher</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;pusher-js&#39;</span><span class="p">);</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Or you can link it directly in your layout:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;https://js.pusher.com/4.2/pusher.min.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>For more information on the Pusher-js client, read its <a href="https://github.com/pusher/pusher-js"target="_blank" rel="noopener">README file</a>.</p>
<h2>Adding Pusher Fake<span class="hx:absolute hx:-mt-20" id="adding-pusher-fake"></span>
    <a href="#adding-pusher-fake" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>At this point, you should be able to connect to the real Pusher account and see the real-time magic happening.</p>
<p>You&rsquo;re also already consuming the free quota you have available in your development environment on Pusher. For most people, this should suffice.</p>
<p>But we want to NOT connect to Pusher over the internet and keep everything local for development and testing. Let&rsquo;s start by adding the <a href="https://github.com/tristandunn/pusher-fake"target="_blank" rel="noopener">Pusher Fake</a> to our <code>Gemfile</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">group</span> <span class="ss">:development</span><span class="p">,</span> <span class="ss">:test</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;pusher-fake&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, this is where the Pusher Fake setup can get convoluted if you don&rsquo;t understand what&rsquo;s happening. As I said before, PusherFake must fork a new process to load a local server that mimics Pusher.</p>
<p>To load it up you must point to the local server. Remember our <code>config/initializers/pusher.rb</code> ? We just need to require a simple file like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">app_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_APP&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">key</span>    <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">secret</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_SECRET&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">cluster</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="s2">&#34;pusher-fake/support/base&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This alone presents a LOT of problems if you&rsquo;re not careful. This <code>require</code> will <strong>fork</strong> a new process. If you&rsquo;re using a single-process web server like Webrick or Thin, you should be ok. If you&rsquo;re using Puma, Unicorn, or Passenger with a maximum of just one process, you should also be good. But if you load a web server that itself forks new processes, you will have trouble.</p>
<p>In practice, I&rsquo;d rather load the Pusher Fake server separately, in stand-alone mode. Fortunately it provides a command line command to start it up. And it&rsquo;s good practice to set that up in a <code>Procfile.dev</code> file and use <a href="https://github.com/ddollar/foreman"target="_blank" rel="noopener">foreman</a> to start everything for you. The <code>Procfile.dev</code> looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>web: bundle exec rails s -p 3000
db: postgres -D /usr/local/var/postgres
redis: redis-server /usr/local/etc/redis.conf
mailcatcher: mailcatcher -f
pusherfake: pusher-fake -i ${PUSHER_APP:-xpto} --socket-host 0.0.0.0 --socket-port ${PUSHER_WS_PORT:-45449} --web-host 0.0.0.0 --web-port ${PUSHER_PORT:-8888} -k ${PUSHER_KEY:-abcd1234} -s ${PUSHER_SECRET:-abcd1234}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As a bonus, look how I configure other services such as PostgreSQL, Redis, etc.</p>
<p>If you didn&rsquo;t know, you can use <code>${VARIABLE_NAME:-default_value}</code> to use an environment variable or have a default value in case the variable doesn&rsquo;t exist. This means that your environment variable configured with Figaro or Dotenv must have the same values.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">PUSHER_APP</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;xpto&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_KEY</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;abcd1234&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_SECRET</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;abcd1234&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_CLUSTER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;us2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;127.0.0.1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_PORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;8888&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_WS_HOST</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;127.0.0.1&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">PUSHER_WS_PORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;45449&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now your <code>config/initializers/pusher.rb</code> should be something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">app_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_APP&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">key</span>    <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">secret</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_SECRET&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="no">Pusher</span><span class="o">.</span><span class="n">cluster</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">development?</span>
</span></span><span class="line"><span class="cl">  <span class="no">Pusher</span><span class="o">.</span><span class="n">host</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_HOST&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="no">Pusher</span><span class="o">.</span><span class="n">port</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;PUSHER_PORT&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And Pusher-js config somewhere in your <code>app/assets/javascripts/</code> directory will resemble something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="o">&lt;%</span> <span class="k">if</span> <span class="nx">defined</span><span class="o">?</span><span class="p">(</span><span class="nx">PusherFake</span><span class="p">)</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="o">&lt;%</span> <span class="k">if</span> <span class="nx">Rails</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">test</span><span class="o">?</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">pusher</span> <span class="o">=</span> <span class="o">&lt;%=</span> <span class="nx">PusherFake</span><span class="p">.</span><span class="nx">javascript</span><span class="p">(</span><span class="nx">cluster</span><span class="o">:</span> <span class="nx">ENV</span><span class="p">[</span><span class="s2">&#34;PUSHER_CLUSTER&#34;</span><span class="p">])</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="o">&lt;%</span> <span class="k">else</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">window</span><span class="p">.</span><span class="nx">pusher</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pusher</span><span class="p">(</span><span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">cluster</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">wsHost</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_WS_HOST&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">wsPort</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_WS_PORT&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">encrypted</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_ENCRYPTED&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">})</span>    
</span></span><span class="line"><span class="cl">    <span class="o">&lt;%</span> <span class="nx">end</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl"><span class="o">&lt;%</span> <span class="k">else</span> <span class="o">%&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">window</span><span class="p">.</span><span class="nx">pusher</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pusher</span><span class="p">(</span><span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_KEY&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">cluster</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_CLUSTER&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">encrypted</span><span class="o">:</span> <span class="o">&lt;%=</span> <span class="nx">ENV</span><span class="p">[</span><span class="s1">&#39;PUSHER_ENCRYPTED&#39;</span><span class="p">]</span> <span class="o">%&gt;</span><span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="o">&lt;%</span> <span class="nx">end</span> <span class="o">%&gt;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remember that this is a Javascript+ERB file, so we can fetch the same environment variables for the configuration.</p>
<p>Now, whenever you <code>foreman start -f Procfile.dev -p 3000</code> it will load the Pusher Fake server with the proper development configurations and both your ruby server-side and javascript client-side should connect to it without any problems.</p>
<p>Also, notice the <code>if Rails.env.test?</code> bit. This is for your RSpec test suite. In the case of the testing environment, we won&rsquo;t load the fake server manually, instead, we will create something like <code>spec/support/pusher-fake.rb</code> with:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;pusher-fake/support/rspec&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s it. The <code>PusherFake.javascript</code> will define default WebSocket connection configuration and the <code>require</code> above will both fork the fake server and set Rspec to clean the channels on each test run (through <code>PusherFake::Channel.reset</code>).</p>
<p>This way your testing environment will also avoid connecting to the external, real, Pusher server.</p>
<p>The key to all this is the environment variables. You must make sure that every piece is loading the same configuration, otherwise you will have the fake server binding to a different port than the Pusher-js is connecting to, and you will have errors. Debug with care.</p>
<p>Most importantly: if you did it all correctly, you are now independent of the real Pusher server for development and testing environments, you won&rsquo;t ever reach any quota limits, and your team and your CI will be able to work uninterruptedly, with a deterministic behavior.</p>
]]></content:encoded><category>rails</category><category>testing</category><category>tutorials</category></item><item><title>Sharing models between Rails Apps - Part 1</title><link>https://akitaonrails.github.io/en/2016/10/03/sharing-models-between-rails-apps-part-1/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/10/03/sharing-models-between-rails-apps-part-1/</guid><pubDate>Mon, 03 Oct 2016 15:00:00 -0300</pubDate><description>&lt;p&gt;&lt;em&gt;&amp;ldquo;Build a microservice and expose an API.&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That would be the quick answer if you ask any developer how to share business logic between applications.&lt;/p&gt;
&lt;p&gt;Although it makes sense in many situations, it&amp;rsquo;s not a good answer all the time.&lt;/p&gt;
&lt;p&gt;My TL;DR for some situations is that you can organize your model logic as &lt;a href="http://api.rubyonrails.org/classes/ActiveSupport/Concern.html"target="_blank" rel="noopener"&gt;ActiveSupport::Concerns&lt;/a&gt; (or plain Ruby Modules if you will) and move them out to a Rubygem that your applications can consume.&lt;/p&gt;</description><content:encoded><![CDATA[<p><em>&ldquo;Build a microservice and expose an API.&rdquo;</em></p>
<p>That would be the quick answer if you ask any developer how to share business logic between applications.</p>
<p>Although it makes sense in many situations, it&rsquo;s not a good answer all the time.</p>
<p>My TL;DR for some situations is that you can organize your model logic as <a href="http://api.rubyonrails.org/classes/ActiveSupport/Concern.html"target="_blank" rel="noopener">ActiveSupport::Concerns</a> (or plain Ruby Modules if you will) and move them out to a Rubygem that your applications can consume.</p>
<p>Notice that I am only speaking of Models, not Controllers or Views. To share those you would need a full-blown Rails Engine instead. But in many cases I&rsquo;ve seen, the goal was to just share the business logic between applications while having separate front-end logic.</p>
<p>A small example of this scenario is the open sourced project I&rsquo;ve been working on in the last few weeks. <a href="https://github.com/Codeminer42/cm42-central"target="_blank" rel="noopener">Central</a>, which is a Pivotal Tracker/Trello alternative - if you&rsquo;re interested.</p>
<p>A few days ago I started a new project (for internal use only) that would query the same models as Central. I didn&rsquo;t want to implement HTTP APIs at this point, and the new application would itself have models with relationships to the models in Central (while treating them as read-only).</p>
<p>After a few refactorings, most of Central&rsquo;s models look like this one:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Team</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Associations</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Validations</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Scopes</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">DomainValidator</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And I have this dependency in the <code>Gemfile</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;central-support&#39;</span><span class="p">,</span> <span class="ss">github</span><span class="p">:</span> <span class="s1">&#39;Codeminer42/cm42-central-support&#39;</span><span class="p">,</span> <span class="ss">branch</span><span class="p">:</span> <span class="s1">&#39;master&#39;</span><span class="p">,</span> <span class="nb">require</span><span class="p">:</span> <span class="s1">&#39;central/support&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Whenever I change the concerns, I do a <code>bundle update central-support</code> in the projects (this is the one caveat to keep in mind to avoid dealing with outdated models).</p>
<p>This was possible because most of those models were mature and stable and I will not be changing them often. I don&rsquo;t recommend exposing unstable dependencies (as gems or APIs, it doesn&rsquo;t matter), because this is a recipe for huge headaches of cascading breaking changes due to outdated dependencies that are changing too often.</p>
<blockquote>
  <p>You should ONLY expose business logic that is reasonably stable (changes only every week or so).</p>

</blockquote>
<p>The whole endeavor was to build a certain Rubygems structure, organize the original models into Concerns (which breaks no behavior), make sure specs are still passing, and then move the content (models and specs) over to the new Rubygems and make sure the specs pass there.</p>
<p>That&rsquo;s how I built a secondary open source dependency for Central, called <a href="https://github.com/Codeminer42/cm42-central-support"target="_blank" rel="noopener">Central Support</a>. Like many gems, its main file <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/lib/central/support.rb"target="_blank" rel="noopener">lib/central/support.rb</a> is nothing but a bunch of &lsquo;require&rsquo;s to load all the dependencies.</p>
<p>So I methodically organized logic as concerns, such as <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/lib/central/support/concerns/team_concern/associations.rb"target="_blank" rel="noopener">lib/central/support/concerns/team_concern/association.rb</a>, which is just the extraction of the Active Record associations from the &lsquo;Team&rsquo; model.</p>
<p>Cut from Central, Paste into Support. When all relevant logic has been moved, I could move the entire Team model spec, mostly without any changes, and make it run. Every time I moved a piece, I <code>bundle update</code>d the gem and ran the main spec suite to make sure nothing broke.</p>
<p>And this is the difficult part: make a sandbox where those concerns could run and be tested.</p>
<p>To begin, I needed to build a minimal Rails app inside the spec folder, at <code>spec/support/rails_app</code>. And there I could put fake models that include the concerns I had just extracted from Central.</p>
<p>There is scarce documentation on how to do that, but I think you can just do <code>rails new</code> and start from there, or copy my <code>rails_app</code> folder for the bare minimum. My case is simpler because this gem is not meant to be general-purpose, so I don&rsquo;t need to run it against different Rails versions, for example.</p>
<p>This internal test app must have a carefully crafted <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/support/rails_app/Gemfile"target="_blank" rel="noopener"><code>Gemfile</code></a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;central-support&#39;</span><span class="p">,</span> <span class="ss">path</span><span class="p">:</span> <span class="no">File</span><span class="o">.</span><span class="n">expand_path</span><span class="p">(</span><span class="s2">&#34;../../../..&#34;</span><span class="p">,</span> <span class="bp">__FILE__</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;devise&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;pg&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;friendly_id&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">gem</span> <span class="s1">&#39;foreigner&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">group</span> <span class="ss">:test</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;test-unit&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="n">gem</span> <span class="s1">&#39;rspec-rails&#39;</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You don&rsquo;t have to add the gems from the main <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/central-support.gemspec"target="_blank" rel="noopener">gemspec</a>. But you can remove the development dependencies that you would put in the gemspec and keep them in the test app Gemfile.</p>
<p>Now, from the <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/Gemfile"target="_blank" rel="noopener">main <code>Gemfile</code></a> you can do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">source</span> <span class="s1">&#39;https://rubygems.org&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">eval_gemfile</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="bp">__FILE__</span><span class="p">),</span> <span class="s2">&#34;spec/support/rails_app/Gemfile&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Most tutorials to build a Rubygem will add a line to load dependencies from the gemspec, but here we are replacing it with the test app&rsquo;s Gemfile. This is the manifest that will be loaded when we run <code>bundle exec rspec</code>, for example.</p>
<p>Speaking of which, this is the <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/rails_helper.rb"target="_blank" rel="noopener"><code>spec/rails_helper.rb</code></a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">ENV</span><span class="o">[</span><span class="s1">&#39;RAILS_ENV&#39;</span><span class="o">]</span> <span class="o">||=</span> <span class="s1">&#39;test&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails/all&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;factory_girl&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;factory_girl_rails&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rspec/rails&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;shoulda/matchers&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="sb">`cd spec/support/rails_app ; bin/rails db:drop db:create db:schema:load RAILS_ENV=test`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;support/rails_app/config/environment&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;support/database_cleaner&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;support/factory_girl&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;support/factories&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;spec_helper&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To wrap your head around it:</p>
<ul>
<li><code>bundle exec rspec</code> will load the main <code>Gemfile</code></li>
<li>the main <code>Gemfile</code> will load from the internal test app&rsquo;s <code>Gemfile</code></li>
<li>that internal test app&rsquo;s <code>Gemfile</code> will require the gemspec from <code>../../../..</code> and the development and test groups of gems (including Rspec, Factory Girl, etc)</li>
<li>the gemspec will require the runtime dependencies such as &ldquo;activesupport&rdquo;, &ldquo;enumerize&rdquo;, etc</li>
<li>finally, the <code>rails_helper.rb</code> listed above will load.</li>
</ul>
<p>There at line 11, the runner will execute a command to <code>cd</code> into the internal test app&rsquo;s root folder and run the <code>db:schema:load</code>, therefore you need a <code>db/schema.rb</code> ready to load, as well as <code>config/database.yml</code>.</p>
<p>The <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/spec_helper.rb"target="_blank" rel="noopener"><code>spec/spec_helper.rb</code></a> is more standard, with optional configurations for test coverage, etc.</p>
<p>The models inside the internal test app are the important parts, because they are the means to include the extracted concerns into a runnable format. The <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/support/rails_app/app/models/team.rb"target="_blank" rel="noopener">&lsquo;spec/support/rails_app/app/models/team.rb&rsquo;</a> is such an example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Team</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Associations</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Validations</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">Scopes</span>
</span></span><span class="line"><span class="cl">  <span class="kp">include</span> <span class="no">Central</span><span class="o">::</span><span class="no">Support</span><span class="o">::</span><span class="no">TeamConcern</span><span class="o">::</span><span class="no">DomainValidator</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And with that, I could move the unmodified specs directly from the main project (Central), such as <a href="https://github.com/Codeminer42/cm42-central-support/blob/master/spec/central/support/team_spec.rb"target="_blank" rel="noopener"><code>spec/central/support/team_spec.rb</code></a>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s1">&#39;rails_helper&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">describe</span> <span class="no">Team</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:model</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">validate_presence_of</span> <span class="ss">:name</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">have_many</span> <span class="ss">:enrollments</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">have_many</span> <span class="ss">:users</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">have_many</span> <span class="ss">:ownerships</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="p">{</span> <span class="n">is_expected</span><span class="o">.</span><span class="n">to</span> <span class="n">have_many</span> <span class="ss">:projects</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you go back in the Central project, some commits back, you will find the very same file as <a href="https://github.com/Codeminer42/cm42-central/blob/a80eefadf233f4a8c5f88829836c872b199798cd/spec/models/team_spec.rb"target="_blank" rel="noopener"><code>spec/models/team_spec.rb</code></a>. And the main advantage of this approach is exactly being able to move most of the code out of the main project, together with their specs, into a dependency gem, without having to &ldquo;rewrite&rdquo; anything.</p>
<p>If I had to rewrite all or a big chunk of the code, it would&rsquo;ve been a more expensive choice and I would probably have deferred it to another time and focus on more valuable features first.</p>
<p>This approach is not perfect but it was super cheap. I could move all the relevant business logic out of the main project without having to rewrite anything but a few lines of wiring code. The new dependency gem received all the relevant bits and specs, and everything just runs.</p>
<p>So, if you have 2 or more Rails apps that could share the same models, this is how you can start it. Of course, there are always a lot of caveats to keep in mind.</p>
<p>In my case, the Central project is the one that can read-and-write to the database. My internal secondary app is just using the models as read-only. When 2 different apps write to the same database, you may have a number of conflicts to deal with.</p>
<p>This approach is useful if your secondary application is akin to an Administration dashboard, for example. You need to have some of the same associations, scopes, even validations for eventual editing, but it&rsquo;s limited to a few, controlled users.</p>
<p>This is also useful if you&rsquo;re doing data analysis, and again you can use the same associations, scopes, to build reports and dashboards. Essentially, if you need read-only access, this is a no-brainer.</p>
<p>In the <a href="http://www.akitaonrails.com/en/2016/10/03/compartilhando-models-entre-apps-rails-parte-2"target="_blank" rel="noopener">next article</a> I will explain how I wired a secondary application, using the central-support gem and dealing with 2 different databases at the same time.</p>
]]></content:encoded><category>rails</category><category>software-architecture</category><category>testing</category></item><item><title>Ex Manga Downloadr - Part 5: Making it more robust!</title><link>https://akitaonrails.github.io/en/2015/12/06/ex-manga-downloadr-part-5-making-it-more-robust/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2015/12/06/ex-manga-downloadr-part-5-making-it-more-robust/</guid><pubDate>Sun, 06 Dec 2015 19:20:00 -0200</pubDate><description>&lt;p&gt;And there I go again. I know some of you may be bored by this tool already, but as a playground project, I still want to make this good code. But there are 2 big problems right now.&lt;/p&gt;
&lt;p&gt;When I was testing only with MangaReader.net as a source, everything worked almost flawlessly. But adding MangaFox in &lt;a href="http://www.akitaonrails.com/en/2015/12/02/ex-manga-downloadr-parte-3-suporte-ao-mangafox"target="_blank" rel="noopener"&gt;Part 3&lt;/a&gt;, with its more restrictive rules towards scraper tools like mine (timing out more frequently, not allowing too many connections from the same place, etc), the process just kept crashing and I had to manually restart it (the resuming features I added in &lt;a href="http://www.akitaonrails.com/en/2015/12/03/ex-manga-downloadr-parte-4-aprendendo-atraves-do-refactoring"target="_blank" rel="noopener"&gt;Part 4&lt;/a&gt; paid off, but it&amp;rsquo;s not a reliable tool anymore).&lt;/p&gt;</description><content:encoded><![CDATA[<p>And there I go again. I know some of you may be bored by this tool already, but as a playground project, I still want to make this good code. But there are 2 big problems right now.</p>
<p>When I was testing only with MangaReader.net as a source, everything worked almost flawlessly. But adding MangaFox in <a href="http://www.akitaonrails.com/en/2015/12/02/ex-manga-downloadr-parte-3-suporte-ao-mangafox"target="_blank" rel="noopener">Part 3</a>, with its more restrictive rules towards scraper tools like mine (timing out more frequently, not allowing too many connections from the same place, etc), the process just kept crashing and I had to manually restart it (the resuming features I added in <a href="http://www.akitaonrails.com/en/2015/12/03/ex-manga-downloadr-parte-4-aprendendo-atraves-do-refactoring"target="_blank" rel="noopener">Part 4</a> paid off, but it&rsquo;s not a reliable tool anymore).</p>
<p>To recap, the Workflow just organizes each step of the process. Its functions are similar to this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">process_downloads</span><span class="p">(</span><span class="n">images_list</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">images_list</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Worker</span><span class="o">.</span><span class="n">page_download_image</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">,</span> <span class="n">directory</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Task</span><span class="o">.</span><span class="n">await</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">,</span> <span class="vi">@await_timeout_ms</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="n">directory</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It deals with a large list, maps over each element sending it to a Worker function to run, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">page_download_image</span><span class="p">(</span><span class="n">image_data</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Task</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="n">fn</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:poolboy</span><span class="o">.</span><span class="n">transaction</span> <span class="ss">:worker_pool</span><span class="p">,</span> <span class="n">fn</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">server</span><span class="p">,</span> <span class="p">{</span><span class="ss">:page_download_image</span><span class="p">,</span> <span class="n">image_data</span><span class="p">,</span> <span class="n">directory</span><span class="p">},</span> <span class="vi">@genserver_call_timeout</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="p">,</span> <span class="vi">@task_async_timeout</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It returns an asynchronous Task waiting for 2 things: for Poolboy to release a free process to use, and for the Worker/GenServer function to finish running inside that process. As I explained in <a href="http://www.akitaonrails.com/en/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener">Part 2</a> this is so we can limit the maximum number of connections to the external source. If we didn&rsquo;t have this restriction, sending tens of thousands of asynchronous requests at once, the external source would just fail them all.</p>
<p>The first thing to bear in mind is that a &ldquo;<tt>Task.async/2</tt>&rdquo; links itself to the caller process, so if something goes wrong, the parent process dies as well.</p>
<p>The correct thing to do is to add a <a href="http://elixir-lang.org/docs/stable/elixir/Task.Supervisor.html"target="_blank" rel="noopener">Task.Supervisor</a> and make it deal with each Task child. To do that, we can just add the Supervisor in our supervised tree at &ldquo;pool_management/supervisor.ex&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">PoolManagement</span><span class="o">.</span><span class="n">Supervisor</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Supervisor</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="n">children</span> <span class="o">=</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">    <span class="n">supervisor</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">Supervisor</span><span class="p">,</span> <span class="o">[[</span><span class="nb">name</span><span class="p">:</span> <span class="no">Fetcher</span><span class="o">.</span><span class="n">TaskSupervisor</span><span class="o">]]</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:poolboy</span><span class="o">.</span><span class="n">child_spec</span><span class="p">(</span><span class="ss">:worker_pool</span><span class="p">,</span> <span class="n">pool_options</span><span class="p">,</span> <span class="o">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And we can replace the &ldquo;<tt>Task.async/2</tt>&rdquo; calls to &ldquo;<tt>Task.Supervisor.async(Fetcher.TaskSupervisor, &hellip;)</tt>&rdquo; like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">page_download_image</span><span class="p">(</span><span class="n">image_data</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Task</span><span class="o">.</span><span class="n">Supervisor</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="no">Fetcher</span><span class="o">.</span><span class="n">TaskSupervisor</span><span class="p">,</span> <span class="n">fn</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:poolboy</span><span class="o">.</span><span class="n">transaction</span> <span class="ss">:worker_pool</span><span class="p">,</span> <span class="n">fn</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">server</span><span class="p">,</span> <span class="p">{</span><span class="ss">:page_download_image</span><span class="p">,</span> <span class="n">image_data</span><span class="p">,</span> <span class="n">directory</span><span class="p">},</span> <span class="vi">@genserver_call_timeout</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="p">,</span> <span class="vi">@task_async_timeout</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This still creates Tasks that we need to await on, and as before, if the function inside crashes, it still brings down the main process. Now my refactoring found a dead end.</p>
<p>This is the 2nd problem I mentioned in the beginning of the article: a <strong>flaw in my design</strong>.</p>
<p>Instead of just mapping through each element of a large list, I should have created an Agent based GenServer to keep the list as the state and make the entire Workflow system a new supervised GenServer. If fetching one URL crashed the GenServer, its supervisor would restart it and pick up the next element in the list.</p>
<p>But, as I am in no mood for this refactoring right now (it&rsquo;s Sunday afternoon) I will concentrate on a quick fix (yes, jerry-rigged patch), just so the function in the async call does not raise exceptions.</p>
<h3>OMG! It&rsquo;s a Try/Catch block!<span class="hx:absolute hx:-mt-20" id="omg-its-a-trycatch-block"></span>
    <a href="#omg-its-a-trycatch-block" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Turns out that everything I run inside the Poolboy processes are HTTP get requests through HTTPotion. Fortunately I had already refactored every HTTPotion get call into a neat macro:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmacro</span> <span class="n">fetch</span><span class="p">(</span><span class="n">link</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="n">expression</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Fetching from </span><span class="si">#{</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">),</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">http_headers</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">body</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">headers</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="ss">:ok</span><span class="p">,</span> <span class="n">body</span> <span class="o">|&gt;</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">gunzip</span><span class="p">(</span><span class="n">headers</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="n">unquote</span><span class="p">(</span><span class="n">expression</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="ss">:err</span><span class="p">,</span> <span class="s2">&#34;not found&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now I only need to replace 1 line in this macro:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">-</span>    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">),</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">http_headers</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="k">case</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">retryable_http_get</span><span class="p">(</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">))</span> <span class="k">do</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And define this new retryable logic in the main module:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="no">Logger</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># will retry failed fetches over 50 times, sleeping 1 second between each retry</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@max_retries</span>  <span class="mi">50</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@time_to_wait_to_fetch_again</span> <span class="mi">1_000</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">retryable_http_get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="k">raise</span> <span class="s2">&#34;Failed to fetch from </span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2"> after </span><span class="si">#{</span><span class="vi">@max_retries</span><span class="si">}</span><span class="s2"> retries.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">retryable_http_get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">retries</span> <span class="p">\\</span> <span class="vi">@max_retries</span><span class="p">)</span> <span class="k">when</span> <span class="n">retries</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">try</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Fetching from </span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2"> for the </span><span class="si">#{</span><span class="vi">@max_retries</span> <span class="o">-</span> <span class="n">retries</span><span class="si">}</span><span class="s2"> time.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">response</span> <span class="o">=</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">http_headers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">case</span> <span class="n">response</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">_</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">_</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="n">status</span> <span class="p">}</span> <span class="k">when</span> <span class="n">status</span> <span class="o">&gt;</span> <span class="mi">499</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="k">raise</span> <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">HTTPError</span><span class="p">{</span><span class="ss">message</span><span class="p">:</span> <span class="s2">&#34;req_timedout&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="n">response</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">rescue</span>
</span></span><span class="line"><span class="cl">      <span class="n">error</span> <span class="k">in</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">HTTPError</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="n">error</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">          <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">HTTPError</span><span class="p">{</span><span class="ss">message</span><span class="p">:</span> <span class="n">message</span><span class="p">}</span> <span class="k">when</span> <span class="n">message</span> <span class="k">in</span> <span class="vi">@http_errors</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="ss">:timer</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="vi">@time_to_wait_to_fetch_again</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">retryable_http_get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">retries</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">_</span> <span class="o">-&gt;</span> <span class="k">raise</span> <span class="n">error</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I strongly <a href="http://www.akitaonrails.com/en/2015/12/01/o-obrigatorio-por-que-elixir-visao-pessoal"target="_blank" rel="noopener">stated</a> that in Elixir we should <strong>not</strong> use &ldquo;try/catch&rdquo; blocks, but there you have it.</p>
<p>This is the consequence of the flaw in my initial Workflow design. If I had coded the Workflow module to be a GenServer, with each list managed by an Agent, each failed HTTPotion call would allow the supervisor to restart it and try again. Without resorting to the ugly &ldquo;try/catch&rdquo; code.</p>
<p>Maybe this will force me to write Part 6 as being the code to remove this ugly try/catch later, so consider this a <strong>Technical Debt</strong> to make everything work now so we can refactor later and pay the debt back.</p>
<p>&ldquo;<tt>HTTPotion.get/2</tt>&rdquo; calls can raise &ldquo;HTTPotion.HTTPError&rdquo; exceptions. I am catching those errors for the time being, matching the messages against a list of errors I had already, sleeping for a certain amount of time (just a heuristic to see if the external sources respond better that way) and I recurse to itself through a limited number of &ldquo;retries&rdquo;, until it reaches zero, in which case it may even be the case that the internet connection is down or some other severe error that we would not be able to recover soon.</p>
<p>With this code in place, now even fetching from MangaFox, without tweaking down the POOL_SIZE, will run until the end, and this solves my needs for now. If anyone is interested in suggesting a better, GenServer based Workflow design, I would really appreciate a Pull Request.</p>
<p>Cheers.</p>
]]></content:encoded><category>elixir-and-erlang</category><category>software-engineering</category><category>testing</category></item><item><title>Ex Manga Downloadr - Part 4: Learning through Refactoring</title><link>https://akitaonrails.github.io/en/2015/12/03/ex-manga-downloadr-part-4-learning-through-refactoring/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2015/12/03/ex-manga-downloadr-part-4-learning-through-refactoring/</guid><pubDate>Thu, 03 Dec 2015 17:40:00 -0200</pubDate><description>&lt;p&gt;&lt;a href="http://www.akitaonrails.com/en/2015/12/02/ex-manga-downloadr-parte-3-suporte-ao-mangafox"target="_blank" rel="noopener"&gt;Yesterday I added Mangafox support&lt;/a&gt; to my downloader tool and it also added a bit of dirty code into my already not-so-good coding. It&amp;rsquo;s time for some serious cleanup.&lt;/p&gt;
&lt;p&gt;You can see everything I did since yesterday to clean things up through &lt;a href="https://github.com/akitaonrails/ex_manga_downloadr/compare/59694565592f8a3bea95115b858dd2ddfdc89873...3ae7dd98a8fd7bae47ffd7a24c0c42a2c3777fad"target="_blank" rel="noopener"&gt;Github&amp;rsquo;s awesome compare page&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;First things first: now the choice to have added a reasonable amount of tests will pay off. In this refactoring I changed function signatures, response formats, moved a fair amount of code around, and without the tests this endeavor would have taken me the entire day or more, rendering the refactor efforts questionable to begin with.&lt;/p&gt;</description><content:encoded><![CDATA[<p><a href="http://www.akitaonrails.com/en/2015/12/02/ex-manga-downloadr-parte-3-suporte-ao-mangafox"target="_blank" rel="noopener">Yesterday I added Mangafox support</a> to my downloader tool and it also added a bit of dirty code into my already not-so-good coding. It&rsquo;s time for some serious cleanup.</p>
<p>You can see everything I did since yesterday to clean things up through <a href="https://github.com/akitaonrails/ex_manga_downloadr/compare/59694565592f8a3bea95115b858dd2ddfdc89873...3ae7dd98a8fd7bae47ffd7a24c0c42a2c3777fad"target="_blank" rel="noopener">Github&rsquo;s awesome compare page</a></p>
<p>First things first: now the choice to have added a reasonable amount of tests will pay off. In this refactoring I changed function signatures, response formats, moved a fair amount of code around, and without the tests this endeavor would have taken me the entire day or more, rendering the refactor efforts questionable to begin with.</p>
<p>At each step of the refactoring I could run &ldquo;<tt>mix test</tt>&rdquo; and work until I ended up with the green status:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Finished in 13.5 seconds (0.1s on load, 13.4s on tests)
12 tests, 0 failures</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The tests take a long time because I made a choice for the MangaReader and Mangafox unit tests to actually go online and fetch from the sites. It takes longer to run the suite but I know that if it breaks and I didn&rsquo;t touch that code, the source websites changed their formats and I need to change the parser. I could have added fixtures to make the tests run faster, but the point of my parser is for them to be correct.</p>
<h3>Macros to the Rescue!<span class="hx:absolute hx:-mt-20" id="macros-to-the-rescue"></span>
    <a href="#macros-to-the-rescue" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Each source module has 3 sub-modules: ChapterPage, IndexPage and Page. All of them have a main function that resembles this piece of code:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">Mangafox</span><span class="o">.</span><span class="n">ChapterPage</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="no">Logger</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Fetching pages from chapter </span><span class="si">#{</span><span class="n">chapter_link</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">,</span> <span class="o">[</span><span class="ss">headers</span><span class="p">:</span> <span class="o">[</span><span class="s2">&#34;User-Agent&#34;</span><span class="p">:</span> <span class="vi">@user_agent</span><span class="p">,</span> <span class="s2">&#34;Accept-encoding&#34;</span><span class="p">:</span> <span class="s2">&#34;gzip&#34;</span><span class="o">]</span><span class="p">,</span> <span class="ss">timeout</span><span class="p">:</span> <span class="mi">30_000</span><span class="o">]</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">body</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">headers</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="n">body</span> <span class="o">=</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">Mangafox</span><span class="o">.</span><span class="n">gunzip</span><span class="p">(</span><span class="n">body</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="ss">:ok</span><span class="p">,</span> <span class="n">fetch_pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="ss">:err</span><span class="p">,</span> <span class="s2">&#34;not found&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(Listing 1.1)</p>
<p>It calls &ldquo;<tt>HTTPotion.get/2</tt>&rdquo; sending a bunch of HTTP options and receives a &ldquo;<tt>%HTTPotion.Response</tt>&rdquo; struct that is then decomposed to get the body and headers. It gunzips the body if necessary and goes to parse the HTML itself.</p>
<p>Similar code exists in 6 different modules, with different links and different parser functions. It&rsquo;s a lot of repetition, but what about making the above code look like the snippet below?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">Mangafox</span><span class="o">.</span><span class="n">ChapterPage</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="no">Logger</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="no">ExMangaDownloadr</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">fetch</span> <span class="n">chapter_link</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="n">fetch_pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(Listing 1.2)</p>
<p>Changed 9 lines to just 1. And by the way, this same line can be written like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">fetch</span> <span class="n">chapter_link</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">fetch_pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Seems familiar? It&rsquo;s like every block in the Elixir language, you can write it in the &ldquo;do/end&rdquo; block format or the way it really is under the covers: a keyword list with a key named &ldquo;:do&rdquo;. And the way this macro is defined is like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmacro</span> <span class="n">fetch</span><span class="p">(</span><span class="n">link</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="n">expression</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Fetching from </span><span class="si">#{</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">),</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">http_headers</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">body</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">headers</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="p">{</span> <span class="ss">:ok</span><span class="p">,</span> <span class="n">body</span> <span class="o">|&gt;</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">gunzip</span><span class="p">(</span><span class="n">headers</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="n">unquote</span><span class="p">(</span><span class="n">expression</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="p">{</span> <span class="ss">:err</span><span class="p">,</span> <span class="s2">&#34;not found&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(Listing 1.3)</p>
<p>There are a lot of details to consider when writing a macro and I recommend reading the documentation on <a href="http://elixir-lang.org/getting-started/meta/macros.html"target="_blank" rel="noopener">Macros</a>. The code is basically copying the function body from the &ldquo;<tt>ChapterPage.pages/1</tt>&rdquo; (Listing 1.1) and pasting it into the &ldquo;<tt>quote do .. end</tt>&rdquo; block (Listing 1.3).</p>
<p>Inside that code we have &ldquo;<tt>unquote(link)</tt>&rdquo; and &ldquo;<tt>unquote(expression)</tt>&rdquo;. You also must read the documentation on <a href="http://elixir-lang.org/getting-started/meta/quote-and-unquote.html"target="_blank" rel="noopener">&ldquo;Quote and Unquote&rdquo;</a>. It just expands this &ldquo;external&rdquo; code inside the macro code to defer execution until the macro quote code is actually executed, instead of running it at that exact moment. I know, tricky to wrap your head around this the first time.</p>
<p>The bottom line is: whatever code is inside the &ldquo;quote&rdquo; block will be &ldquo;inserted&rdquo; where we called &ldquo;<tt>ExMangaDownloadr.fetch/2</tt>&rdquo; in the &ldquo;pages/1&rdquo; function in Listing 1.2, together with the unquoted code you passed as a parameter.</p>
<p>The resulting code will resemble the original code in Listing 1.1.</p>
<p>To make it simpler, if you were in Javascript this would be similar code:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">eval</span><span class="p">(</span><span class="s2">&#34;doSomething(&#39;&#34;</span> <span class="o">+</span> <span class="nx">url</span> <span class="o">+</span> <span class="s2">&#34;&#39;)&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">pages</span><span class="p">(</span><span class="nx">page_link</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">fetch</span><span class="p">(</span><span class="nx">page_link</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>&ldquo;Quote&rdquo; would be like the string body in an eval and &ldquo;unquote&rdquo; just concatenating the value you passed inside the code being eval-ed. This is a crude metaphor as &ldquo;quote/unquote&rdquo; is <strong>way</strong> more powerful and cleaner than ugly &ldquo;eval&rdquo; (which you shouldn&rsquo;t be using, by the way!) But this metaphor should do to make you understand the code above.</p>
<p>Another place I used a macro was to save the images list in a dump file and load it later if the tool crashes for some reason, in order not to have to start over from scratch. The original code was like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">dump_file</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">directory</span><span class="si">}</span><span class="s2">/images_list.dump&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">images_list</span> <span class="o">=</span> <span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">exists?</span><span class="p">(</span><span class="n">dump_file</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">                <span class="ss">:erlang</span><span class="o">.</span><span class="n">binary_to_term</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">read!</span><span class="p">(</span><span class="n">dump_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">              <span class="k">else</span>
</span></span><span class="line"><span class="cl">                <span class="n">list</span> <span class="o">=</span> <span class="o">[</span><span class="n">url</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">                  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">chapters</span>
</span></span><span class="line"><span class="cl">                  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">pages</span>
</span></span><span class="line"><span class="cl">                  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">images_sources</span>
</span></span><span class="line"><span class="cl">                <span class="no">File</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">dump_file</span><span class="p">,</span> <span class="ss">:erlang</span><span class="o">.</span><span class="n">term_to_binary</span><span class="p">(</span><span class="n">list</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                <span class="n">list</span>
</span></span><span class="line"><span class="cl">              <span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(Listing 1.4)</p>
<p>And now that you understand macros, you will understand what I did here:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmacro</span> <span class="n">managed_dump</span><span class="p">(</span><span class="n">directory</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="n">expression</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">dump_file</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">unquote</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span><span class="si">}</span><span class="s2">/images_list.dump&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="n">images_list</span> <span class="o">=</span> <span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">exists?</span><span class="p">(</span><span class="n">dump_file</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">          <span class="ss">:erlang</span><span class="o">.</span><span class="n">binary_to_term</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">read!</span><span class="p">(</span><span class="n">dump_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span>
</span></span><span class="line"><span class="cl">          <span class="n">list</span> <span class="o">=</span> <span class="n">unquote</span><span class="p">(</span><span class="n">expression</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="no">File</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">dump_file</span><span class="p">,</span> <span class="ss">:erlang</span><span class="o">.</span><span class="n">term_to_binary</span><span class="p">(</span><span class="n">list</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">          <span class="n">list</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">CLI</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">Workflow</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="no">ExMangaDownloadr</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="p">{</span><span class="n">_url</span><span class="p">,</span> <span class="n">_source</span><span class="p">}</span> <span class="o">=</span> <span class="n">manga_site</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">File</span><span class="o">.</span><span class="n">mkdir_p!</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">images_list</span> <span class="o">=</span> 
</span></span><span class="line"><span class="cl">      <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">managed_dump</span> <span class="n">directory</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="n">manga_site</span>
</span></span><span class="line"><span class="cl">          <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">chapters</span>
</span></span><span class="line"><span class="cl">          <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">pages</span>
</span></span><span class="line"><span class="cl">          <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">images_sources</span> 
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And there you have it! And now you see how &ldquo;do .. end&rdquo; blocks are implemented. It just passes the expression as the value in the keyword list of the macro definition. Let&rsquo;s define a dumb macro:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Foo</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmacro</span> <span class="n">foo</span><span class="p">(</span><span class="k">do</span><span class="p">:</span> <span class="n">expression</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">     <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">       <span class="n">unquote</span><span class="p">(</span><span class="n">expression</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">     <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now the following calls are all equivalent:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="no">Foo</span>
</span></span><span class="line"><span class="cl"><span class="no">Foo</span><span class="o">.</span><span class="n">foo</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="no">Foo</span><span class="o">.</span><span class="n">foo</span> <span class="k">do</span><span class="p">:</span> <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="no">Foo</span><span class="o">.</span><span class="n">foo</span><span class="p">(</span><span class="k">do</span><span class="p">:</span> <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="no">Foo</span><span class="o">.</span><span class="n">foo</span><span class="p">(</span><span class="o">[</span><span class="k">do</span><span class="p">:</span> <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="no">Foo</span><span class="o">.</span><span class="n">foo</span><span class="p">(</span><span class="o">[</span><span class="p">{</span><span class="ss">:do</span><span class="p">,</span> <span class="no">IO</span><span class="o">.</span><span class="n">puts</span><span class="p">(</span><span class="mi">1</span><span class="p">)}</span><span class="o">]</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is macros combined with <a href="http://elixir-lang.org/getting-started/maps-and-dicts.html"target="_blank" rel="noopener">Keyword Lists</a> which I explained in previous articles and it&rsquo;s simply a List with tuples where each tuple has an atom key and a value.</p>
<h3>More Macros<span class="hx:absolute hx:-mt-20" id="more-macros"></span>
    <a href="#more-macros" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Another opportunity to refactor was the &ldquo;mangareader.ex&rdquo; and &ldquo;mangafox.ex&rdquo; modules that were just used in the unit tests &ldquo;mangareader_test.ex&rdquo; and &ldquo;mangafox_test.ex&rdquo;. This is the old &ldquo;mangareader.ex&rdquo; code:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmacro</span> <span class="n">__using__</span><span class="p">(</span><span class="n">_opts</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">IndexPage</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">ChapterPage</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">Page</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span> </span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is how it was used in &ldquo;mangareader_test.ex&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReaderTest</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExUnit</span><span class="o">.</span><span class="n">Case</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It was just a shortcut to alias the modules in order to use them directly inside the tests. I just moved the entire module as a macro in &ldquo;ex_manga_downloadr.ex&rdquo; module:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">mangareader</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">IndexPage</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">ChapterPage</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">Page</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">mangafox</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defmacro</span> <span class="n">__using__</span><span class="p">(</span><span class="n">which</span><span class="p">)</span> <span class="k">when</span> <span class="n">is_atom</span><span class="p">(</span><span class="n">which</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">apply</span><span class="p">(</span><span class="n">__MODULE__</span><span class="p">,</span> <span class="n">which</span><span class="p">,</span> <span class="o">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And now I can use it like this in the test file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReaderTest</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExUnit</span><span class="o">.</span><span class="n">Case</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExMangaDownloadr</span><span class="p">,</span> <span class="ss">:mangareader</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The special &ldquo;<strong>using</strong>&rdquo; macro is called when I &ldquo;use&rdquo; a module, and I can even pass arguments to it. The implementation then uses &ldquo;apply/3&rdquo; to dynamically call the correct macro. This is exactly how Phoenix imports the proper behaviors for Models, Views, Controllers, Router, for example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">PageController</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Pxblog</span><span class="o">.</span><span class="n">Web</span><span class="p">,</span> <span class="ss">:controller</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The macros are open in a Phoenix file and available in the &ldquo;web/web.ex&rdquo; module, so I just copied the same behavior. And now I have 2 fewer files to worry about.</p>
<h3>Tiny refactorings<span class="hx:absolute hx:-mt-20" id="tiny-refactorings"></span>
    <a href="#tiny-refactorings" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the previous code I used the &ldquo;<tt>String.to_atom/1</tt>&rdquo; to convert the string of the module name to an atom, to be later used in &ldquo;apply/3&rdquo; calls:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">manga_source</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">module</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">source</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mangareader&#34;</span> <span class="o">-&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">to_atom</span><span class="p">(</span><span class="s2">&#34;Elixir.ExMangaDownloadr.MangaReader.</span><span class="si">#{</span><span class="n">module</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mangafox&#34;</span>    <span class="o">-&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">to_atom</span><span class="p">(</span><span class="s2">&#34;Elixir.ExMangaDownloadr.Mangafox.</span><span class="si">#{</span><span class="n">module</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I changed it to this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl">    <span class="s2">&#34;mangareader&#34;</span> <span class="o">-&gt;</span> <span class="ss">:&#34;Elixir.ExMangaDownloadr.MangaReader.</span><span class="si">#{</span><span class="n">module</span><span class="si">}</span><span class="ss">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mangafox&#34;</span>    <span class="o">-&gt;</span> <span class="ss">:&#34;Elixir.ExMangaDownloadr.Mangafox.</span><span class="si">#{</span><span class="n">module</span><span class="si">}</span><span class="ss">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It&rsquo;s just a shortcut to do the same thing.</p>
<p>In the parser I was also not using <a href="https://github.com/philss/floki"target="_blank" rel="noopener">Floki</a> correctly. So take a look at this piece of old code:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">fetch_manga_title</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s2">&#34;#mangaproperties h1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;h1&#34;</span><span class="p">,</span> <span class="o">[]</span><span class="p">,</span> <span class="o">[</span><span class="n">title</span><span class="o">]</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">title</span> <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s2">&#34;#listing a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span> <span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="o">[</span><span class="p">{</span><span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="n">url</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">url</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now using the better helper functions that Floki provides:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">fetch_manga_title</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">html</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;#mangaproperties h1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Floki</span><span class="o">.</span><span class="n">text</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">html</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;#listing a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Floki</span><span class="o">.</span><span class="n">attribute</span><span class="p">(</span><span class="s2">&#34;href&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This was a case of not reading the documentation as I should have. Much cleaner!</p>
<p>I did other bits of cleanup but I think this should cover the major changes. And finally, I bumped up the version to &ldquo;1.0.0&rdquo; as well!</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl">   <span class="k">def</span> <span class="nf">project</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">     <span class="o">[</span><span class="ss">app</span><span class="p">:</span> <span class="ss">:ex_manga_downloadr</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>     <span class="ss">version</span><span class="p">:</span> <span class="s2">&#34;0.0.1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>     <span class="ss">version</span><span class="p">:</span> <span class="s2">&#34;1.0.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">elixir</span><span class="p">:</span> <span class="s2">&#34;~&gt; 1.1&#34;</span><span class="p">,</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And speaking of versions, I&rsquo;m using Elixir 1.1 but pay attention as <a href="https://github.com/elixir-lang/elixir/blob/ef5ba3af059f76489631dc26b52ecaeff09af3fe/CHANGELOG.md"target="_blank" rel="noopener">Elixir 1.2</a> is just around the corner and it brings some niceties. For example, that macro that aliased a few modules could be written this way now:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">mangareader</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="p">{</span><span class="no">IndexPage</span><span class="p">,</span> <span class="no">ChapterPage</span><span class="p">,</span> <span class="no">Page</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is just 1 feature among many other syntax improvements and support for the newest <a href="http://www.erlang.org/news/88"target="_blank" rel="noopener">Erlang R18</a>. Keep an eye on both!</p>
]]></content:encoded><category>elixir-and-erlang</category><category>software-engineering</category><category>testing</category></item><item><title>[Off-Topic] The 5 Team Dysfunctions in Code</title><link>https://akitaonrails.github.io/en/2009/05/31/off-topic-as-5-disfun--es-de-equipes-em-c-digo/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2009/05/31/off-topic-as-5-disfun--es-de-equipes-em-c-digo/</guid><pubDate>Sun, 31 May 2009 16:10:00 -0300</pubDate><description>&lt;p&gt;I keep repeating to every team I manage that 99% of the problems on any project come down to &amp;ldquo;communication.&amp;rdquo; Stresses that drag on for days and could have been settled in a 30-second hallway chat. I say &amp;ldquo;communication,&amp;rdquo; but it&amp;rsquo;s a little more than that: a lack of respect, a lack of trust, a lack of &lt;a href="http://en.wikipedia.org/wiki/Empathy"target="_blank" rel="noopener"&gt;empathy&lt;/a&gt; (empathy, mind you, not &amp;ldquo;sympathy&amp;rdquo;). Here&amp;rsquo;s a &lt;strong&gt;translation&lt;/strong&gt; of &lt;a href="http://www.markhneedham.com/blog/2009/05/28/the-5-dysfunctions-of-teams-in-code/"target="_blank" rel="noopener"&gt;Mark Needham&amp;rsquo;s&lt;/a&gt; article.&lt;/p&gt;
&lt;p&gt;I recently came across an &lt;a href="http://www.thekua.com/atwork/2009/05/evidence-in-favour-of-conways-law/"target="_blank" rel="noopener"&gt;interesting post by my colleague Pat Kua&lt;/a&gt; where he talks about how some patterns he&amp;rsquo;s noticed in code can be linked to &lt;a href="http://en.wikipedia.org/wiki/Conway%27s_Law"target="_blank" rel="noopener"&gt;Conway&amp;rsquo;s Law&lt;/a&gt;, which suggests that the structure of systems designed in organisations will mirror the communication structure of that organisation. (AkitaOnRails: also read &lt;a href="http://en.wikipedia.org/wiki/The_Mythical_Man-Month"target="_blank" rel="noopener"&gt;The Mythical Man-Month&lt;/a&gt;, where Conway&amp;rsquo;s topic is also explored, and understand &lt;a href="http://www.infoq.com/articles/scaling-lean-agile-feature-teams"target="_blank" rel="noopener"&gt;Cross Functional Teams&lt;/a&gt; to understand one of the solutions.)&lt;/p&gt;</description><content:encoded><![CDATA[<p>I keep repeating to every team I manage that 99% of the problems on any project come down to &ldquo;communication.&rdquo; Stresses that drag on for days and could have been settled in a 30-second hallway chat. I say &ldquo;communication,&rdquo; but it&rsquo;s a little more than that: a lack of respect, a lack of trust, a lack of <a href="http://en.wikipedia.org/wiki/Empathy"target="_blank" rel="noopener">empathy</a> (empathy, mind you, not &ldquo;sympathy&rdquo;). Here&rsquo;s a <strong>translation</strong> of <a href="http://www.markhneedham.com/blog/2009/05/28/the-5-dysfunctions-of-teams-in-code/"target="_blank" rel="noopener">Mark Needham&rsquo;s</a> article.</p>
<p>I recently came across an <a href="http://www.thekua.com/atwork/2009/05/evidence-in-favour-of-conways-law/"target="_blank" rel="noopener">interesting post by my colleague Pat Kua</a> where he talks about how some patterns he&rsquo;s noticed in code can be linked to <a href="http://en.wikipedia.org/wiki/Conway%27s_Law"target="_blank" rel="noopener">Conway&rsquo;s Law</a>, which suggests that the structure of systems designed in organisations will mirror the communication structure of that organisation. (AkitaOnRails: also read <a href="http://en.wikipedia.org/wiki/The_Mythical_Man-Month"target="_blank" rel="noopener">The Mythical Man-Month</a>, where Conway&rsquo;s topic is also explored, and understand <a href="http://www.infoq.com/articles/scaling-lean-agile-feature-teams"target="_blank" rel="noopener">Cross Functional Teams</a> to understand one of the solutions.)</p>
<p>I recently read a book called <a href="http://www.markhneedham.com/blog/2009/04/22/the-five-dysfunctions-of-a-team-book-review/"target="_blank" rel="noopener">The Five Dysfunctions of Teams</a> which describe some behaviours in teams which aren&rsquo;t working in an effective way.</p>
<p>Playing the devil&rsquo;s advocate I became intrigued as to if there is some sort of link between these dysfunctions and whether they manifest themselves in our code as anti patterns.</p>
<p>The five dysfunctions are:</p>
<ol>
<li>Absence of Trust – team members are unwilling to be vulnerable within the group</li>
<li>Fear of Conflict – team cannot engage in unfiltered and passionate debate of ideas</li>
<li>Lack of Commitment – team members rarely have buy in or commit to decisions</li>
<li>Avoidance of Accountability – team members don&rsquo;t call their peers on actions/behaviours which hurt the team</li>
<li>Inattention to Results – team members put their individual needs before those of the team</li>
</ol>
<h2>Absence of Trust<span class="hx:absolute hx:-mt-20" id="absence-of-trust"></span>
    <a href="#absence-of-trust" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I think having <strong>null checks all over the code</strong> is the most obvious indicator that people don&rsquo;t trust the code that they are working with.</p>
<p>If the person writing the code had faith in their colleagues who had written the code they now need to interact with then I think it would be more likely that they would trust the code to do the right thing and they wouldn&rsquo;t feel driven to such a <a href="http://www.thekua.com/atwork/2008/08/defensive-programming-depends-on-context/"target="_blank" rel="noopener">defensive approach</a>.</p>
<h2>Fear of Conflict<span class="hx:absolute hx:-mt-20" id="fear-of-conflict"></span>
    <a href="#fear-of-conflict" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Fear of conflict in a team seems to manifest itself most obviously in code when we have <strong>a lot of duplication happening</strong> (the &ldquo;copy &amp; paste&rdquo; syndrome) - there are several reasons why duplication can happen but I think one of them is when people aren&rsquo;t engaging in discussions when they disagree with something that a colleague has written and therefore end up writing their own version of something that&rsquo;s already been done.</p>
<p>This probably manifests itself even more obviously when you end up with multiple different frameworks all in the same code base and all doing the same thing just because people don&rsquo;t want to engage in a conversation to choose which one the team is going to use.</p>
<h2>Lack of Commitment<span class="hx:absolute hx:-mt-20" id="lack-of-commitment"></span>
    <a href="#lack-of-commitment" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is one which seems to overlap a lot with the previous two although perhaps one specific way that this would manifest itself in the code might be if we see <strong>sloppy mistakes or lack of care being shown with the code</strong> (the &ldquo;sloppy job&rdquo; syndrome) - an example of this could be changing the name of a class but then not ensuring that all the places where the old name was used in variables have been changed accordingly.</p>
<p>This leaves the code in a half baked state which becomes quite difficult for other people to work with and they have to do some clean up work before being able to effectively make changes to the code.</p>
<h2>Avoidance of Accountability<span class="hx:absolute hx:-mt-20" id="avoidance-of-accountability"></span>
    <a href="#avoidance-of-accountability" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The coding anti pattern that stands out for me here is <strong>when we allow people to write code without tests</strong> and then check those into source control.</p>
<p>From my experience so far this never seems to work out well and I think it shows a lack of respect for the rest of the team since we don&rsquo;t have an easy way of verifying whether this code actually works and other people can&rsquo;t make use of it elsewhere in the code base with any degree of confidence.</p>
<h2>Inattention to Results<span class="hx:absolute hx:-mt-20" id="inattention-to-results"></span>
    <a href="#inattention-to-results" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Team members putting their individual needs before the team manifests itself in code when we end up with <strong>code that has been written in such a way that only the person who wrote it is really able to understand it</strong>.</p>
<p>I think this manifests itself in &ldquo;clever code&rdquo; which is fine in your own projects but in a team context is very detrimental as you become a bit of a bottleneck when people want to make changes in this area of the code and can&rsquo;t do it because they can&rsquo;t understand what&rsquo;s going on.</p>
<p>Something else that falls under this dysfunctions is <strong>when there is a convention for how to do certain things in the code but we decide to go off and do it our own way</strong>. Now granted sometimes it&rsquo;s fine to do this if you&rsquo;re working the code towards a better state and the rest of the team are aware you&rsquo;re trying to work towards this goal but otherwise it&rsquo;s not a very effective approach.</p>
<h2>In Summary<span class="hx:absolute hx:-mt-20" id="in-summary"></span>
    <a href="#in-summary" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I found it quite intriguing that in my mind at least some of the problems we see in code do seem to have some correlation to the problems that we see in teams.</p>
<p>One thing I remember from reading Gerald Weinberg&rsquo;s <a href="http://www.amazon.com/Secrets-Consulting-Giving-Getting-Successfully/dp/0932633013/ref=sr_1_1?ie=UTF8&amp;s=books&amp;qid=1243452602&amp;sr=1-1"target="_blank" rel="noopener">The Secrets of Consulting</a> is his claim that <a href="https://web.archive.org/web/20090606022501/http://www.codinghorror.com/blog/archives/001033.html"target="_blank" rel="noopener">no matter what the problem is it&rsquo;s always a people problem</a> - if indeed this is true then in theory problems that we see in code should be indicative of a people problem which I think probably to an extent is true.</p>
<p>I think certainly not all problems in code are linked to the dysfunctions of teams - certainly some anti patterns creep into our code due to a lack of experience of team members of how to do things better but then again maybe that&rsquo;s indicative of the team not having senior members working closely enough with their colleagues!</p>
<p>Maybe we can therefore work out how we can identify ways to improve our team by starting with a look at the code.</p>
<h2>Rant<span class="hx:absolute hx:-mt-20" id="rant"></span>
    <a href="#rant" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>by AkitaOnRails:</strong> Honestly, I&rsquo;m thoroughly convinced that the problems that show up in code are just symptoms of structural problems in teams and organizations.</p>
<p>It starts with a lack of respect. When the team watches the boss (don&rsquo;t call a hierarchical boss a &ldquo;leader&rdquo;; they&rsquo;re almost never real leaders) pull rank to get what he wants out of other teams, the developers themselves turn it into an arm-wrestling match: <em>&ldquo;I did my part, and if the other guy complains I&rsquo;ll have my boss talk to his and that&rsquo;s that.&rdquo;</em> In my experience, almost every problem in an inefficient, dysfunctional team traces back to the manager.</p>
<p>Managers who run on <em>&ldquo;command-and-control&rdquo;</em> are exactly the types who should be run out of an organization. They&rsquo;re the ones who don&rsquo;t trust the team, who yell, who lean on the power of the title, who insist on being the communication bottleneck and demand that everything pass through them. They have no real knowledge, and it shows the moment they have to actually argue a point.</p>
<p>Then there&rsquo;s the manager who never gives honest, daily feedback, good or bad, and instead saves it all up to throw in your face months later. To me, that&rsquo;s the loudest display of <strong>cowardice</strong> from someone who is supposed to be a <em>&ldquo;leader.&rdquo;</em></p>
<p>There&rsquo;s the manager who loves to <em>&ldquo;micro-manage&rdquo;</em> whenever it suits him but never spells out what he expects. His feedback tracks his mood that day instead of the work you delivered, which keeps things nice and comfortable on his end. And there&rsquo;s the manager who, to look a little less bad, spends his time trying to make the other teams look worse. These are the nitpickers, the ones who dig up trivial stuff (what time you showed up, what you wore, some hallway chat) as an excuse to badmouth people, instead of looking at real results like profit and cost.</p>
<p>If you&rsquo;re genuinely worried about why your teams aren&rsquo;t performing the way they should, look at the management layer. Especially the ones who&rsquo;ve spent <strong>many years</strong> in the same organization, hooked on it, who already know every <em>&ldquo;workaround&rdquo;</em> in the place. They&rsquo;re dangerous: they smile at everyone, they seem confident, they seem efficient, and most of their reports praise them (under coercion, obviously).</p>
<p>The team reflects the system and the structure dropped on top of it. Strip away their autonomy, treat them like children, leave them confused and afraid, and this is exactly what you get: sloppy work, poor quality, defects all the time, high cost.</p>
<p>Some people call that an exaggeration, especially a distant, barely present upper management (anything short of every day isn&rsquo;t presence) when it comes to what happens on the shop floor. Then they&rsquo;re shocked to find their employees have gone stagnant, incompetent, and obsolete, and grown distant and indifferent to the future of the organization. At that point the only thing they care about is their own job.</p>
<p>Meanwhile those &ldquo;managers&rdquo; stay comfortable. When things go well (by luck, and only luck), they take the credit. When things go wrong, the blame lands on the team members he was going to get rid of anyway, or on the other teams, or on the whole organization that didn&rsquo;t back him up, or on historical decisions that can&rsquo;t be undone now. The blame is never his.</p>
<p>Remember this: years of tenure can&rsquo;t buy immunity. Years of tenure should be irrelevant when an organization wants to be efficient. And don&rsquo;t be afraid to let a manager like that go out of some fear that <em>&ldquo;only he knows how things work.&rdquo;</em> Things won&rsquo;t fall apart. Trust the teams, give the autonomy back, they know what to do.</p>
<p>So yes: the vast majority of a team&rsquo;s dysfunctions are a direct result of the organization&rsquo;s dysfunctions. There&rsquo;s no point applying localized techniques, preaching pair programming, test-driven development, refactoring and the like, if the organization stays the same. Want to change? Change everything. Or don&rsquo;t even bother.</p>
]]></content:encoded><category>software-engineering</category><category>testing</category><category>management</category><category>off-topic</category></item><item><title>Translation: Flaccid Scrum</title><link>https://akitaonrails.github.io/en/2009/02/03/tradu-o-scrum-fl-cido/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2009/02/03/tradu-o-scrum-fl-cido/</guid><pubDate>Tue, 03 Feb 2009 03:32:00 -0200</pubDate><description>&lt;p&gt;5 days ago, Martin Fowler &lt;a href="http://martinfowler.com/bliki/FlaccidScrum.html"target="_blank" rel="noopener"&gt;published an article&lt;/a&gt; that may sound controversial to Scrummers. His target, though, is the people who apply Scrum the wrong way and the ones who don&amp;rsquo;t bother to make that problem apparent. Below is the full translation of his article; my own comments come at the end.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://martinfowler.com/bliki/FlaccidScrum.html"target="_blank" rel="noopener"&gt;&lt;img src="http://s3.amazonaws.com/akitaonrails/assets/2009/2/3/Picture_1.png" alt="" loading="lazy" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a mess I&amp;rsquo;ve heard about with quite a few projects recently. It works out like this:&lt;/p&gt;</description><content:encoded><![CDATA[<p>5 days ago, Martin Fowler <a href="http://martinfowler.com/bliki/FlaccidScrum.html"target="_blank" rel="noopener">published an article</a> that may sound controversial to Scrummers. His target, though, is the people who apply Scrum the wrong way and the ones who don&rsquo;t bother to make that problem apparent. Below is the full translation of his article; my own comments come at the end.</p>
<p><a href="http://martinfowler.com/bliki/FlaccidScrum.html"target="_blank" rel="noopener"><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/2/3/Picture_1.png" alt=""  loading="lazy" /></a></p>
<p>There&rsquo;s a mess I&rsquo;ve heard about with quite a few projects recently. It works out like this:</p>
<ul>
<li>They want to use an agile process, and pick Scrum</li>
<li>They adopt the Scrum practices, and maybe even the principles</li>
<li>After a while progress is slow because the code base is a mess</li>
</ul>
<p>What&rsquo;s happened is that they haven&rsquo;t paid enough attention to the internal quality of their software. If you make that mistake you&rsquo;ll soon find your productivity dragged down because it&rsquo;s much harder to add new features than you&rsquo;d like. You&rsquo;ve taken on a crippling <a href="http://martinfowler.com/bliki/TechnicalDebt.html"target="_blank" rel="noopener">Technical Debt</a> and your scrum has gone weak at the knees. (And if you&rsquo;ve been in a real scrum, you&rsquo;ll know that&rsquo;s a Bad Thing.)</p>
<p>I&rsquo;ve mentioned Scrum because when we see this problem, Scrum seems to be particularly common as the nominative process the team is following. For many people, this situation is exacerbated by Scrum because Scrum is process that&rsquo;s centered on project management techniques and deliberately omits any technical practices, in contrast to (for example) Extreme Programming.</p>
<p>In defense of Scrum, it&rsquo;s important to point out that just because it doesn&rsquo;t include technical activities within its scope should not lead anyone to conclude that it doesn&rsquo;t think they are important. Whenever I&rsquo;ve listened to prominent Scrummers they&rsquo;ve always emphasized that you must have good technical practices to succeed with a Scrum project. They don&rsquo;t mandate what those technical practices should be, but you do need them. After all projects get into trouble for poor internal quality all the time, the fact that a lot crop up under Scrum&rsquo;s flag may be more due to the fact that Scrum is so popular at the moment then anything particular to Scrum. Popularity and <a href="http://martinfowler.com/bliki/SemanticDiffusion.html"target="_blank" rel="noopener">Semantic Diffusion</a> tend to go together.</p>
<p>So what to do about it?</p>
<p>The scrum community needs to redouble its efforts to ensure that people understand the importance of strong technical practices. Certainly any kind of project review should include examining what kinds of technical practices are present. If you&rsquo;re involved or connected to such a project, make a fuss if the technical side is being neglected.</p>
<p>If you&rsquo;re looking to introduce scrum, make sure you pay good attention to technical practices. We tend to apply many of those from Extreme Programming and they fit just fine. XPers often joke, with some justification, that Scrum is just XP without the technical practices that make it work. Sniping aside, the XP practices make a good starting point - and are certainly going to be much better than doing nothing at all.</p>
<p>I always like to point out that it isn&rsquo;t methodologies that succeed or fail, it&rsquo;s teams that succeed or fail. Taking on a process can help a team raise its game, but in the end it&rsquo;s the team that matters and carries the responsibility to do what works for them. I&rsquo;m sure that the many Flaccid Scrum projects being run will harm Scrum&rsquo;s reputation, and probably the broader agile reputation as well. But since I see <a href="http://martinfowler.com/bliki/SemanticDiffusion.html"target="_blank" rel="noopener">Semantic Diffusion</a> as an inevitability I&rsquo;m not unduly alarmed. Teams that fail will probably fail whatever methodology they mis-apply, teams that succeed will build their practices on good ideas and the scrum community&rsquo;s role is to spread these good ideas around widely.</p>
<p>Many people are looking to Lean as the <em>Next Big Agile Thing</em>. But the more popular lean becomes the more it will run into the same kind of issues as Scrum is facing now. That doesn&rsquo;t make Lean (or Scrum) worthless, it just reminds us Individuals and Interactions are more valuable than Processes and Tools.</p>
<h3>Akita&rsquo;s Notes<span class="hx:absolute hx:-mt-20" id="akitas-notes"></span>
    <a href="#akitas-notes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In practice, what I&rsquo;ve noticed is that the planning and management parts meet the least resistance: <a href="http://www.extremeprogramming.org/rules/userstories.html"target="_blank" rel="noopener">User Stories</a>, <a href="http://www.extremeprogramming.org/rules/planninggame.html"target="_blank" rel="noopener">Release Planning</a>, <a href="http://www.extremeprogramming.org/rules/releaseoften.html"target="_blank" rel="noopener">Small Releases</a>, <a href="http://www.extremeprogramming.org/rules/velocity.html"target="_blank" rel="noopener">Project Velocity</a>, <a href="http://www.extremeprogramming.org/rules/iterative.html"target="_blank" rel="noopener">Iterations</a>, <a href="http://www.extremeprogramming.org/rules/iterationplanning.html"target="_blank" rel="noopener">Iteration Planning</a>, <a href="http://www.extremeprogramming.org/rules/standupmeeting.html"target="_blank" rel="noopener">Stand Up Meetings</a>. The reason is simple. When a company decides to adopt Scrum, the decision came from some level above; without that, it never gets off the ground. And managers and bosses tend to understand and swallow these concepts well enough, especially if they&rsquo;ve already been through traditional project management methodologies without success.</p>
<p>And this is where most people <strong>go wrong</strong>: if the team isn&rsquo;t yet mature enough to be self-managing, that is, to produce quality code on its own, it&rsquo;s up to upper management to lead in that direction. That&rsquo;s hard because many managers aren&rsquo;t technical, which leaves them blind to the problem of missing good technical practices.</p>
<p>The mess Martin Fowler refers to lives in design, coding, and testing. The ones who struggle most with these practices are developers who came from the old ways of building software, the &ldquo;suicidal cowboy&rdquo; style, or who have little experience and little study of software development in general.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/2/3/22124.jpg" alt=""  loading="lazy" /></p>
<p>Most developers don&rsquo;t understand <a href="http://www.extremeprogramming.org/rules/simple.html"target="_blank" rel="noopener">Simplicity</a>, the famous &ldquo;do the simplest thing that works,&rdquo; or <a href="http://en.wikipedia.org/wiki/You_Ain%27t_Gonna_Need_It"target="_blank" rel="noopener">YAGNI</a> (You Ain&rsquo;t Gonna Need It). In the younger ones, the &ldquo;cowboy&rdquo; spirit is hard to tame: they always want to build things in more complex ways than necessary. This leads precisely to the problem of <a href="http://www.extremeprogramming.org/rules/early.html"target="_blank" rel="noopener">adding things too early</a>, complexity for the pleasure of complexity. The team needs to police itself against this. Fortunately <a href="http://www.extremeprogramming.org/rules/spike.html"target="_blank" rel="noopener">Spike Solutions</a> help: the team stops for a moment, understands the problem, and studies solution alternatives.</p>
<p>Another mix-up I&rsquo;ve seen a lot is confusing <a href="http://www.extremeprogramming.org/rules/refactor.html"target="_blank" rel="noopener">Refactoring</a> with rewriting. A rewrite in itself isn&rsquo;t a problem, but it becomes one when applied with the notion that &ldquo;just because it&rsquo;s new, it&rsquo;s better.&rdquo; First and foremost, Refactoring is a set of techniques aimed at rejuvenating code, making it more manageable, without modifying its behavior.</p>
<p><img src="http://s3.amazonaws.com/akitaonrails/assets/2009/2/3/cowboy.jpg" alt=""  loading="lazy" /></p>
<p>This leads to the Achilles&rsquo; heel of most amateur developers: the aversion to testing. <em>&ldquo;I&rsquo;m too good to make mistakes, I don&rsquo;t need tests.&rdquo;</em> That&rsquo;s the developer who will effectively lead his own work to certain failure. <a href="http://www.extremeprogramming.org/rules/testfirst.html"target="_blank" rel="noopener">Testing first</a> is an effective way to refine design, and it also leads to simplicity, doing only what is truly needed.</p>
<p>Without good tests, effective refactoring is impossible: how do you guarantee that a change didn&rsquo;t alter the code&rsquo;s behavior? The corollary is that these developers also <strong>don&rsquo;t</strong> practice Refactoring, which leads to the mass of messy code. The irony is that even rewrites, where it was assumed the new version would be better, end up becoming &ldquo;legacy&rdquo; very quickly.</p>
<p>Worse yet, when a <a href="http://www.extremeprogramming.org/rules/bugs.html"target="_blank" rel="noopener">bug is found</a>, tests to prevent a regression to the same bug are rarely created. And everyone must have seen bugs that were supposedly already fixed coming back shortly after. Another problem is that many teams never settled on a good definition of &ldquo;story done&rdquo; and therefore also struggle to maintain <a href="http://www.extremeprogramming.org/rules/functionaltests.html"target="_blank" rel="noopener">acceptance tests</a> for those stories. One of the reasons: instead of writing User Stories (&ldquo;as X, I want Y because of Z,&rdquo; defining what to implement, for whom, and what value it brings), they write tasks (&ldquo;do X&rdquo;), skipping the for-whom and the why.</p>
<p>Still in the &ldquo;cowboy&rdquo; spirit, inexperienced programmers don&rsquo;t understand the concept of <a href="http://www.extremeprogramming.org/rules/collective.html"target="_blank" rel="noopener">collective code ownership</a>. What happens is that each developer tries to limit himself to the code he thinks he&rsquo;s responsible for and doesn&rsquo;t worry about the whole. It should work the other way around: every developer must feel responsible for all the code. This also explains one more reason for the importance of unit tests: without them it&rsquo;s impossible to help with code you didn&rsquo;t write, and impossible to know if your own code doesn&rsquo;t break something someone else made. In summary: this leads to an immeasurable accumulation of &ldquo;Technical Debt&rdquo; that only becomes visible when it&rsquo;s already too late, turning your &ldquo;new system&rdquo; prematurely into an unmanageable &ldquo;legacy.&rdquo;</p>
<p>In that same spirit, &ldquo;cowboy&rdquo; developers don&rsquo;t <a href="http://www.extremeprogramming.org/rules/integrateoften.html"target="_blank" rel="noopener">integrate frequently</a> as they should. It&rsquo;s not uncommon to see developers spend the whole week with new code only on their own machine and, at the end of the week, commit to the repository and consider it done. No tests created, no full test suite run. One more developer deliberately creating Technical Debt.</p>
<p>To make matters worse, it&rsquo;s not hard to find teams that don&rsquo;t even understand the value of a repository: for them, it&rsquo;s enough to change things directly on the production server or edit files right there. That&rsquo;s the prehistory of development. A code versioning repository is mandatory, and treating it as a sanctuary is the greatest responsibility of a team. That means all code in the repository must always be code with no deliberately introduced problems, from not writing tests, not integrating frequently, not refactoring where needed. Bugs will always exist and must be fixed, but deliberate errors make the developer irresponsible and a problem for the entire team.</p>
<p>Finally, it&rsquo;s common to see developers who think they&rsquo;re very clever <a href="http://www.extremeprogramming.org/rules/optimize.html"target="_blank" rel="noopener">optimizing</a> very early in the project, based on nothing but guesswork, without measurements. By the way, most programmers I&rsquo;ve met have as much aversion to measurement as they do to testing. And optimizing without being based on metrics is the purest waste of time. You can make a certain piece of code 100 times faster, but if in overall time that doesn&rsquo;t represent even a 0.5% gain, it was a waste of time. Again, inexperience.</p>
<p>Still regarding &ldquo;suicidal cowboys,&rdquo; they&rsquo;re normally attached to their limited tools, the ones they&rsquo;re already used to. They&rsquo;re too lazy to learn new things, whether from inability or lack of will, and they always try to take the apparently easiest route, like using automatic code generators, which generally produce code that&rsquo;s hard to maintain and outside the most modern standards accepted as good practices.</p>
<p>In the end, the result is the same: inexperienced developers who think they know everything, or long-tenured developers addicted to anti-practices and too stubborn to learn the good ones. Building an efficient and truly agile development team is very hard. And the reality is that simply not everyone is cut out for the job. An agile developer is someone proactive, self-taught, sociable, and communicative. And all of that matters more than their supposed technical competence.</p>
<p>PS: some people might take this personally. But in reality, this is more common than you&rsquo;d imagine; I&rsquo;ve seen it at many clients and will keep seeing it. To be fair, I myself have done many projects (more than I&rsquo;d like) that immediately became &ldquo;legacy,&rdquo; code poorly tested and hard to maintain. Complaining is easy; doing something to improve is the hard part. And yesterday&rsquo;s cowboy developer has everything it takes to become a good programmer tomorrow; they just need to want it.</p>
]]></content:encoded><category>agile</category><category>software-engineering</category><category>testing</category><category>off-topic</category></item></channel></rss>