<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jon Lauridsen</title>
    <description>The latest articles on Forem by Jon Lauridsen (@jonlauridsen).</description>
    <link>https://forem.com/jonlauridsen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F16800%2F0e83a783-3e86-4cbf-9a28-d2c91fdf7a66.jpg</url>
      <title>Forem: Jon Lauridsen</title>
      <link>https://forem.com/jonlauridsen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jonlauridsen"/>
    <language>en</language>
    <item>
      <title>Perfect Elixir: Testing Shell Scripts with Bats</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Sat, 05 Apr 2025 15:41:35 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/perfect-elixir-test-automating-shell-scripts-18ii</link>
      <guid>https://forem.com/jonlauridsen/perfect-elixir-test-automating-shell-scripts-18ii</guid>
      <description>&lt;p&gt;Automated testing is &lt;strong&gt;crucial&lt;/strong&gt; for reliable software development, yet shell scripts are frequently left untested, despite managing critical tasks like deployments, database migrations, and CI pipelines. Neglecting tests here can lead to subtle bugs and fragile workflows. Today, let's make sure &lt;strong&gt;all our code is testable&lt;/strong&gt;, treating shell scripts with the same discipline we apply to application code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW test automation doesn't mean every line absolutely must be tested; rather, it's about ensuring we &lt;strong&gt;can&lt;/strong&gt; test. There are cases where skipping tests can make sense — but we can never do so out of laziness.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shell Script Testing with &lt;strong&gt;Bats&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
Bats Helper Libraries

&lt;ul&gt;
&lt;li&gt;Creating a &lt;strong&gt;Bats Download Manager&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Mocking

&lt;ul&gt;
&lt;li&gt;jasonkarns/bats-mock 🛑&lt;/li&gt;
&lt;li&gt;grayhemp/bats-mock ✅&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Speeding Up Bats Tests&lt;/li&gt;

&lt;li&gt;Integrating Bats Into the Workflow&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shell Script Testing with &lt;strong&gt;Bats&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcn43s3x4j1a11pcf4o1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcn43s3x4j1a11pcf4o1.png" width="800" height="56"&gt;&lt;/a&gt;Bats, &lt;a href="https://bats-core.readthedocs.io" rel="noopener noreferrer"&gt;Bash Automated Testing System&lt;/a&gt;, is a testing framework for shell scripts, with a nice test-runner and various commands to test what a script does when run. Let's see how it works!&lt;/p&gt;

&lt;p&gt;First we need Bats installed, and &lt;code&gt;pkgx&lt;/code&gt; makes that easy (to refresh on &lt;code&gt;pkgx&lt;/code&gt; please refer to the "&lt;a href="https://dev.to/jonlauridsen/perfect-elixir-environment-setup-1145"&gt;Setting Up an Elixir Dev Environment&lt;/a&gt;" article):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/.pkgx.yml
&lt;span class="p"&gt;@@ -7 +7,2 @@&lt;/span&gt; dependencies:
   postgresql.org: =17.0.0
&lt;span class="gi"&gt;+  github.com/bats-core/bats-core: =1.11.1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now write a test, e.g. here we assert &lt;code&gt;bin/doctor --help&lt;/code&gt; prints out usage information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat test&lt;/span&gt;/bin/doctor.bats
@test &lt;span class="s2"&gt;"--help outputs usage"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
  run bin/doctor &lt;span class="nt"&gt;--help&lt;/span&gt;  

  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;  
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Fq&lt;/span&gt; &lt;span class="s2"&gt;"Usage: bin/doctor [options]"&lt;/span&gt;  
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to run our test-suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bats &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;test
&lt;/span&gt;bin/doctor.bats
 ✓ displays &lt;span class="nb"&gt;help &lt;/span&gt;message when &lt;span class="nt"&gt;--help&lt;/span&gt; is used

1 &lt;span class="nb"&gt;test&lt;/span&gt;, 0 failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a great start, but we quickly hit a blocker: How do we test how our script behaves if a tool such as &lt;code&gt;pkgx&lt;/code&gt; is missing? We can't mess with the real &lt;code&gt;pkgx&lt;/code&gt; because it's core to our entire project, so the solution is dependency injection — inject mocks instead of real commands. This is similar to what we covered in the &lt;a href="https://dev.to/jonlauridsen/perfect-elixir-test-automation-58fd#dependency-injection"&gt;previous "Automating Tests in Elixir Projects" article&lt;/a&gt;, and the Bats community has created &lt;strong&gt;mock libraries&lt;/strong&gt; that promises to make it simple to create mocks. So let's go explore them.&lt;/p&gt;

&lt;p&gt;But wait, before we try those helper libraries, we first need to talk about how we &lt;strong&gt;download&lt;/strong&gt; libraries.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bats Helper Libraries
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvs4gru2pbvaekrbzxepz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvs4gru2pbvaekrbzxepz.png" width="800" height="56"&gt;&lt;/a&gt;The Bats ecosystem includes excellent helper libraries, but incorporating these libraries into our project isn't straightforward, with common approaches relying on Git submodules or committing their code into our repository directly.&lt;/p&gt;

&lt;p&gt;But Git submodules rely on cumbersome commands, and committing external source files is a hack that goes against how we deal with dependencies. Basically, both add complexity. Especially compared to how simple Elixir handles it with &lt;code&gt;mix deps.get&lt;/code&gt;, where well-defined dependencies are simply pulled down.&lt;/p&gt;

&lt;p&gt;So, we'll create a lightweight &lt;strong&gt;Bats Download Manager&lt;/strong&gt; to automate the fetching of Bats libraries, so we can use helper libraries without adding new hacky ways of dealing with source code to our project.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW &lt;strong&gt;simple&lt;/strong&gt; doesn’t mean &lt;strong&gt;easy&lt;/strong&gt;, e.g. Git submodules are &lt;strong&gt;easy&lt;/strong&gt; to start but &lt;strong&gt;add cognitive complexity&lt;/strong&gt; with their commands and mental model. &lt;a href="https://www.youtube.com/watch?v=SxdOUGdseq4" rel="noopener noreferrer"&gt;Rich Hickey’s “Simple Made Easy”&lt;/a&gt;) speaks into this. Optimizing for simplicity — even when it's initially harder — pays off long-term.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a &lt;strong&gt;Bats Download Manager&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A good software principle is to download external files as dependencies, so we're creating our own "&lt;strong&gt;Bats download manager&lt;/strong&gt;" to handle this for us.&lt;/p&gt;

&lt;p&gt;Our first goal will be to download the library &lt;a href="https://github.com/bats-core/bats-assert" rel="noopener noreferrer"&gt;&lt;code&gt;bats-assert&lt;/code&gt;&lt;/a&gt;. We can easily download it via the &lt;strong&gt;GitHub CLI&lt;/strong&gt; tool, and since the download will be a tarball archive we'll also need the &lt;strong&gt;&lt;code&gt;tar&lt;/code&gt;&lt;/strong&gt; command to unpack it. These are both available via &lt;code&gt;pkgx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/.pkgx.yml
&lt;span class="p"&gt;@@ -8 +8,3 @@&lt;/span&gt; dependencies:
   github.com/bats-core/bats-core: =1.11.1
&lt;span class="gi"&gt;+  cli.github.com: =2.68.1
+  gnu.org/tar: =1.35.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make use of the tools we wrap them in a shell script that takes arguments for a repository and its release-tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash  &lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail  

&lt;span class="nv"&gt;BATS_DEPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;realpath&lt;/span&gt; &lt;span class="nt"&gt;--relative-to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/../.bats_deps"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  

usage&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Bats manager to download helper libraries"&lt;/span&gt;  
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;  
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Usage:"&lt;/span&gt;  
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  batsman &amp;lt;owner/repo&amp;gt; &amp;lt;tag&amp;gt;"&lt;/span&gt;  
&lt;span class="o"&gt;}&lt;/span&gt;  

 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"--help"&lt;/span&gt;  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; usage&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;  
 &lt;span class="s2"&gt;"$#"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 2  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; usage&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;  
&lt;span class="nv"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  
&lt;span class="nv"&gt;tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  
&lt;span class="nv"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BATS_DEPS&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  
&lt;span class="nv"&gt;sha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gh api &lt;span class="s2"&gt;"/repos/&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;/commits/&lt;/span&gt;&lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--jq&lt;/span&gt; .sha&lt;span class="si"&gt;)&lt;/span&gt;  

&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  
gh release download &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--archive&lt;/span&gt; &lt;span class="s2"&gt;"tar.gz"&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; &lt;span class="s2"&gt;"release.tar.gz"&lt;/span&gt;  
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; &lt;span class="s2"&gt;"release.tar.gz"&lt;/span&gt; &lt;span class="nt"&gt;--strip-components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1  
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="s2"&gt;"release.tar.gz"&lt;/span&gt;  

&lt;span class="nv"&gt;version_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="s2"&gt; (GitHub release) &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;sha&lt;/span&gt;:0:8&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version_info&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .bats_version.txt  
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$version_info&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$destination&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW this is just the basics of a download manager, it can be extended as far as is needed. E.g. &lt;a href="https://github.com/gaggle/perfect-elixir/commit/960c028cec2014f79bce700ad21bb9867e3b851e#diff-0b918afe97176671a9ff983f4663330e474297c567aa8a92d70a6b08c7d4036cR64-R65" rel="noopener noreferrer"&gt;in this later commit&lt;/a&gt; security is improved by checking for specific SHAs.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's now straightforward to download &lt;code&gt;bats-core/bats-assert&lt;/code&gt; (which itself depends on the library &lt;code&gt;bats-core/bats-support&lt;/code&gt;, so we'll actually download both):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bin/batsman bats-core/bats-assert v2.1.0
bats-core/bats-assert v2.1.0 &lt;span class="o"&gt;(&lt;/span&gt;GitHub release&lt;span class="o"&gt;)&lt;/span&gt; 78fa631d -&amp;gt; .bats_deps/bats-core/bats-assert

&lt;span class="nv"&gt;$ &lt;/span&gt;bin/batsman bats-core/bats-support v0.3.0
bats-core/bats-support v0.3.0 &lt;span class="o"&gt;(&lt;/span&gt;GitHub release&lt;span class="o"&gt;)&lt;/span&gt; 24a72e14 -&amp;gt; .bats_deps/bats-core/bats-support

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; .bats_deps/bats-core/bats-assert/
LICENSE  load.bash  package.json  README.md  src  &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can use the nice assert-functions that come with the &lt;code&gt;bats-assert&lt;/code&gt; library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/test/bin/doctor.bats
&lt;span class="p"&gt;L#1:
&lt;/span&gt;&lt;span class="gi"&gt;+setup() {
+  load "$(pwd)/.bats_deps/bats-core/bats-support/load"
+  load "$(pwd)/.bats_deps/bats-core/bats-assert/load"
+}
+
&lt;/span&gt; @test "displays help message when --help is used" {
   run bin/doctor --help
&lt;span class="gd"&gt;-  [ "$status" -eq 0 ]
-  echo "$output" | grep -Fq "Usage: bin/doctor [options]"
&lt;/span&gt;&lt;span class="gi"&gt;+  assert_success
+  assert_line "Usage: bin/doctor [options]"
&lt;/span&gt; }
&lt;span class="err"&gt;
&lt;/span&gt;$ bats -r test
&lt;span class="p"&gt;bin/doctor.bats
&lt;/span&gt; ✓ displays help message when --help is used
&lt;span class="p"&gt;1 test, 0 failures
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great stuff. And to ensure our entire team downloads these dependencies, we add a simple check to &lt;code&gt;bin/doctor&lt;/code&gt; that prompts everyone to run the proper &lt;code&gt;bin/batsman&lt;/code&gt; calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/bin/doctor
&lt;span class="p"&gt;L#56:
&lt;/span&gt;   "mix ecto.create"
&lt;span class="gi"&gt;+check "Check Bats helpers" \
+  "ls .bats_deps/bats-core/bats-assert &amp;gt; /dev/null" \
+  "bin/batsman bats-core/bats-assert v2.1.0 &amp;amp;&amp;amp; bin/batsman bats-core/bats-support v0.3.0"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have a simple way for the whole team to consistently fetch the needed Bats libraries, mirroring the familiar dependency management we know from &lt;code&gt;mix deps.get&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;now&lt;/strong&gt; we can return to the original problem: How can we test &lt;code&gt;bin/doctor&lt;/code&gt; without actually having it run all the commands it depends on?&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mocking
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1bv1ezqkiazc6xguvor1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1bv1ezqkiazc6xguvor1.png" width="800" height="56"&gt;&lt;/a&gt;We have Bats working, we can download helper libraries, now it's time to find an effective way to test shell scripts in isolation from the external commands they call. We need a reliable way to mock out those command calls, and for that we'll explore mocking libraries and see how they can help us achieve this.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  jasonkarns/bats-mock 🛑
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/jasonkarns/bats-mock" rel="noopener noreferrer"&gt;bats-mock&lt;/a&gt; library by Jason Karns promises straightforward mocking of external commands. Let’s download and integrate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bin/batsman jasonkarns/bats-mock v1.2.5
jasonkarns/bats-mock v1.2.5 &lt;span class="o"&gt;(&lt;/span&gt;GitHub release&lt;span class="o"&gt;)&lt;/span&gt; 24f995f9 -&amp;gt; .bats_deps/jasonkarns/bats-mock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we load it, and use its stub command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/test/bin/doctor.bats
&lt;span class="p"&gt;L#3:
&lt;/span&gt;   load "$(pwd)/.bats_deps/bats-core/bats-assert/load"
&lt;span class="gi"&gt;+  load "$(pwd)/.bats_deps/jasonkarns/bats-mock/stub"
&lt;/span&gt; }
&lt;span class="p"&gt;@@ -11 +11,12 @@&lt;/span&gt; setup() {
 }
&lt;span class="gi"&gt;+
+@test "advise to reinitialize devenv if pkgx is absent" {
+  stub which "pkgx : exit 1"
+
+  run bin/doctor
+
+  assert_failure
+  assert_line "Suggested remedy: dev off; dev || source bin/bootstrap"
+
+  unstub which
+}
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;$ bats -r test
&lt;span class="p"&gt;bin/doctor.bats
&lt;/span&gt; ✓ displays help message when --help is used
 ✓ advise to reinitialize devenv if pkgx is absent
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;2 tests, 0 failures
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This initial success is encouraging: we stub &lt;code&gt;which&lt;/code&gt; to simulate &lt;code&gt;pkgx&lt;/code&gt; is missing, and verify our script responds correctly. Simple and effective!&lt;/p&gt;

&lt;p&gt;Unfortunately, the approach quickly breaks down in more complex cases. For example, this stub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;stub psql &lt;span class="s1"&gt;'-U postgres -c "\q" : echo ""'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should simulate &lt;code&gt;psql&lt;/code&gt; producing no output — but the library fails with obscure errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bats &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/bin/doctor.bats
bin/doctor.bats
 ✗ advise to create a user when psql fails to connect
   &lt;span class="o"&gt;(&lt;/span&gt;from &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;unstub&lt;span class="s1"&gt;' in file .bats_deps/jasonkarns/bats-mock/stub.bash, line 46,
    in test file test/bin/doctor.bats, line 40)
     `unstub psql'&lt;/span&gt; failed
1 tests, 1 failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/jasonkarns/bats-mock/issues/55" rel="noopener noreferrer"&gt;This appears to be a known bug&lt;/a&gt;. Rather than get caught up troubleshooting brittle quoting issues, let's acknowledge this limits and move on quickly to try other libraries.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  grayhemp/bats-mock ✅
&lt;/h3&gt;

&lt;p&gt;Sergey Konoplev has authored the &lt;a href="https://github.com/grayhemp/bats-mock" rel="noopener noreferrer"&gt;grayhemp/bats-mock&lt;/a&gt; library, which supports creating mocked commands. It doesn't mock existing commands, rather it generates &lt;strong&gt;generic mocks&lt;/strong&gt; and we're responsible for injecting them into our scripts as needed. Let's see how that works.&lt;/p&gt;

&lt;p&gt;First, fetch the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bin/batsman grayhemp/bats-mock v1.0-beta.1
grayhemp/bats-mock v1.0-beta.1 &lt;span class="o"&gt;(&lt;/span&gt;GitHub release&lt;span class="o"&gt;)&lt;/span&gt; ac1a4475 -&amp;gt; .bats_deps/grayhemp/bats-mock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we change &lt;code&gt;bin/doctor&lt;/code&gt; to allow the &lt;code&gt;which&lt;/code&gt; command to be overwritten by specifying the environment variable &lt;code&gt;_WHICH&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/bin/doctor
&lt;span class="p"&gt;L#34:
&lt;/span&gt;&lt;span class="gi"&gt;+WHICH=${_WHICH:-which}
&lt;/span&gt; section "Running checks…"
 check "Check system dependencies" \
&lt;span class="gd"&gt;-  "command which pkgx &amp;amp;&amp;amp; which erl &amp;amp;&amp;amp; which elixir" \
&lt;/span&gt;&lt;span class="gi"&gt;+  "command $WHICH pkgx &amp;amp;&amp;amp; $WHICH erl &amp;amp;&amp;amp; $WHICH elixir" \
&lt;/span&gt;   "dev off; dev || source bin/bootstrap"
 check "Check developer environment" \
&lt;span class="gd"&gt;-  "which stat | grep -q '.pkgx'" \
&lt;/span&gt;&lt;span class="gi"&gt;+  "$WHICH stat | grep -q '.pkgx'" \
&lt;/span&gt;   "dev off; dev"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally we create a generic mock and plug it into the &lt;code&gt;_WHICH&lt;/code&gt; variable (by exporting it, it automatically becomes part of the environment that &lt;code&gt;run&lt;/code&gt; uses):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/test/bin/doctor.bats
&lt;span class="p"&gt;L#3:
&lt;/span&gt;   load "$(pwd)/.bats_deps/bats-core/bats-assert/load"
&lt;span class="gi"&gt;+  load "$(pwd)/.bats_deps/grayhemp/bats-mock/src/bats-mock"
&lt;/span&gt; }
&lt;span class="p"&gt;@@ -10 +11,14 @@&lt;/span&gt; setup() {
 }
&lt;span class="gi"&gt;+
+@test "advise to reinitialize devenv if pkgx is absent" {
+  export _WHICH="$(mock_create)"
+  mock_set_side_effect "$_WHICH" "case $1 in pkgx) exit 1;; esac"
+
+  run bin/doctor
+
+  assert_failure
+  assert_line "Suggested remedy: dev off; dev || source bin/bootstrap"
+   "$(mock_get_call_args $_WHICH)" = pkgx 
+}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And all that works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bats &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;test
&lt;/span&gt;bin/doctor.bats
 ✓ displays &lt;span class="nb"&gt;help &lt;/span&gt;message when &lt;span class="nt"&gt;--help&lt;/span&gt; is used
 ✓ advise to reinitialize devenv &lt;span class="k"&gt;if &lt;/span&gt;pkgx is absent

2 tests, 0 failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's pretty cool. Using the environment variable mechanism to inject commands is a bit more verbose, but it also makes the code nicely explicit. A worthwhile tradeoff.&lt;/p&gt;

&lt;p&gt;But we should stress-test this library further, and if we create tests for all the code-branches of &lt;code&gt;bin/doctor&lt;/code&gt; it can end up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bats &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;/bin/doctor.bats
doctor.bats
 ✓ displays &lt;span class="nb"&gt;help &lt;/span&gt;message when &lt;span class="nt"&gt;--help&lt;/span&gt; is used
 ✓ reports healthy system &lt;span class="o"&gt;(&lt;/span&gt;all stubs are by default configured &lt;span class="k"&gt;for &lt;/span&gt;success&lt;span class="o"&gt;)&lt;/span&gt;
 ✓ executes all stubs &lt;span class="k"&gt;in &lt;/span&gt;their expected order
 ✓ verifies Elixir is available before running mix
 ✓ advise to reinitialize devenv &lt;span class="k"&gt;if &lt;/span&gt;pkgx is absent
 ✓ advise to reinitialize devenv &lt;span class="k"&gt;if &lt;/span&gt;erl is absent
 ✓ advise to reinitialize devenv &lt;span class="k"&gt;if &lt;/span&gt;elixir is absent
 ✓ advise to toggle the devenv &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;stat &lt;/span&gt;is not provided by pkgx
 ✓ advise to start the database server &lt;span class="k"&gt;if &lt;/span&gt;pgrep cant connect
 ✓ advise to create a user when psql fails to connect
 ✓ advise to &lt;span class="nb"&gt;install &lt;/span&gt;hex &lt;span class="k"&gt;if &lt;/span&gt;hex is not installed locally
 ✓ advise to run mix setup &lt;span class="k"&gt;if &lt;/span&gt;mix dependencies are unavailable
 ✓ advise to run mix setup &lt;span class="k"&gt;if &lt;/span&gt;mix dependencies are outdated
 ✓ advise to run ecto &lt;span class="k"&gt;if &lt;/span&gt;the database is missing
 ✓ advise to advise download Bats helpers &lt;span class="k"&gt;if &lt;/span&gt;theyre missing

15 tests, 0 failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW I subsequently refactored the tests to bring down complexity, so each test is kept focused and simple, and delegate into helper-functions. Without this refactor it was grievously complex trying to keep an overview of e.g. the repeated calls to &lt;code&gt;mock_create&lt;/code&gt; and &lt;code&gt;mock_set_side_effect&lt;/code&gt;. This article drags on if we also cover those details but if you're interested &lt;a href="https://github.com/gaggle/perfect-elixir/blob/perfect-elixir-7-test-automating-shell-scripts/test/bin/doctor.bats" rel="noopener noreferrer"&gt;the full &lt;code&gt;/test/bin/doctor.bats&lt;/code&gt; is available here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Speeding Up Bats Tests
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6k7c1s7x6qcy8hkrhsof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6k7c1s7x6qcy8hkrhsof.png" width="800" height="56"&gt;&lt;/a&gt;We've made great progress on testing shell scripts, with an easy and simple approach to downloading the Bats framework and helper libraries.&lt;/p&gt;

&lt;p&gt;But tests have gotten slow to run.&lt;/p&gt;

&lt;p&gt;Here I've added a &lt;code&gt;bin/bats-test&lt;/code&gt; script to simplify running tests (see commit "&lt;a href="https://github.com/gaggle/perfect-elixir/commit/6b28999d1e188a47cb6f278a9f98ef48ba96f154" rel="noopener noreferrer"&gt;&lt;code&gt;Add initial bin/bats-test script&lt;/code&gt;&lt;/a&gt;" for details), and it shows our current performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bin/bats-test
Running all tests...
…
36 tests, 0 failures &lt;span class="k"&gt;in &lt;/span&gt;30 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;30 seconds&lt;/strong&gt;!? That's slow. Let's try some straightforward optimizations to see what we can do about this.&lt;/p&gt;

&lt;p&gt;Bats can run tests in parallel (to follow the code changes for this see commit "&lt;a href="https://github.com/gaggle/perfect-elixir/commit/e61791e9d8680bbb0139fdafa5cab0226a61357c" rel="noopener noreferrer"&gt;&lt;code&gt;Run bin/bats-test tests in parallel&lt;/code&gt;&lt;/a&gt;"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bin/bats-test
Running all tests &lt;span class="o"&gt;(&lt;/span&gt;via 14 parallel &lt;span class="nb"&gt;jobs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;...
…
36 tests, 0 failures &lt;span class="k"&gt;in &lt;/span&gt;19 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;19 seconds&lt;/strong&gt; is nearly a 40% improvement, so that did help. But it's still slow… &lt;/p&gt;

&lt;p&gt;Shell scripts tend to not change as often as application-code, could we skip tests until they actually change? We could persist a checksum when tests pass, and if a new run has the same checksum then skip the tests (to follow this change see commit "&lt;a href="https://github.com/gaggle/perfect-elixir/commit/413c9671193bf79208c76a1b8ce7052b5026aaf8" rel="noopener noreferrer"&gt;&lt;code&gt;Only run bin/bats-test tests if checksums indicate a change&lt;/code&gt;&lt;/a&gt;"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bin/bats-test
Running all tests (via 14 parallel jobs)...
…
38 tests, 0 failures in 22 seconds

$ bin/bats-test
No changes since last test run, skipping 38 tests.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;0 seconds&lt;/strong&gt; is certainly fast 😂, but of course doesn't alleviate the pain when tests do have to run. We could keep going to optimize even further, but despite our fingers itching to do more our time will be better spent moving on (for this article, at least), because all told we've successfully improved how we work with shell scripts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers can rely on &lt;code&gt;bin/bats-test&lt;/code&gt; to run shell tests &lt;strong&gt;as fast as possible&lt;/strong&gt; (even if that speed leaves something to desired, and can be improved incrementally over time by those who wish to optimize).&lt;/li&gt;
&lt;li&gt;For the cases where developers are not working on shell scripts the tests take no time, meaning &lt;strong&gt;friction is minimized for everyone that works elsewhere in the system&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Bats Into the Workflow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzldjpkn8dhghv11bldf8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzldjpkn8dhghv11bldf8.png" width="800" height="56"&gt;&lt;/a&gt;In the "&lt;a href="https://dev.to/jonlauridsen/perfect-elixir-development-workflows-26k6"&gt;Streamlining Development Workflows&lt;/a&gt;" article we introduced the core workflow of running &lt;code&gt;bin/shipit&lt;/code&gt; to quickly push changes, which internally runs &lt;code&gt;mix test&lt;/code&gt; to handle Elixir's test automation. We need to extend this mechanism to also run Bats tests. &lt;/p&gt;

&lt;p&gt;Adding more test-commands directly to &lt;code&gt;bin/shipit&lt;/code&gt; makes it crowded though; let’s create a dedicated &lt;code&gt;bin/test&lt;/code&gt; script instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;bin/test
&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.shhelpers"&lt;/span&gt;
step &lt;span class="nt"&gt;--with-output&lt;/span&gt; &lt;span class="s2"&gt;"Running Elixir tests"&lt;/span&gt; &lt;span class="s2"&gt;"mix test"&lt;/span&gt;
step &lt;span class="nt"&gt;--with-output&lt;/span&gt; &lt;span class="s2"&gt;"Running Shell tests"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/bats-test"&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;bin/test
• Running Elixir tests:
Running ExUnit with seed: 248176, max_cases: 28
.......
Finished &lt;span class="k"&gt;in &lt;/span&gt;0.05 seconds &lt;span class="o"&gt;(&lt;/span&gt;0.02s async, 0.03s &lt;span class="nb"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
7 tests, 0 failures
• Running Shell tests:
No changes since last &lt;span class="nb"&gt;test &lt;/span&gt;run, skipping 45 tests.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we rewire &lt;code&gt;bin/shipit&lt;/code&gt; to call our new script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U0 .
/bin/shipit
&lt;span class="p"&gt;@@ -6 +6 @@&lt;/span&gt;
&lt;span class="gd"&gt;-step --with-output "Running tests" "mix test"
&lt;/span&gt;&lt;span class="gi"&gt;+"$(dirname "$0")/test"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this we have easily integrated shell testing into our workflows, and team members won’t need extra setup; they’ll just automatically benefit from the added tests.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2i4eystojdrvyafm5pg5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2i4eystojdrvyafm5pg5.png" width="800" height="56"&gt;&lt;/a&gt;Shell scripts deserve testing just as much as our application code, yet they're often overlooked. In this article, we've shown it's straightforward to integrate robust shell testing into our structured workflows using &lt;strong&gt;Bats&lt;/strong&gt; and &lt;strong&gt;dependency injection&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Instead of accepting untested scripts as the status quo, we've chosen to uphold strong engineering principles and committed to simple, decoupled tools and techniques to accomplish that.&lt;/p&gt;

&lt;p&gt;Now we can write our shell scripts with the &lt;strong&gt;same sense of safety&lt;/strong&gt; ✅ as our Elixir applications, with &lt;strong&gt;isolated and reliable tests&lt;/strong&gt; ✅ fully integrated into our &lt;strong&gt;ultra-fast &lt;code&gt;bin/shipit&lt;/code&gt; workflow&lt;/strong&gt; ✅.&lt;/p&gt;

&lt;p&gt;Happy testing! 🚀&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>tutorial</category>
      <category>development</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Perfect Elixir: Automating Tests in Elixir Projects</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Wed, 05 Mar 2025 21:44:30 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/perfect-elixir-test-automation-58fd</link>
      <guid>https://forem.com/jonlauridsen/perfect-elixir-test-automation-58fd</guid>
      <description>&lt;p&gt;How do we write &lt;strong&gt;fast, reliable&lt;/strong&gt; tests in Elixir? How do we keep modules &lt;strong&gt;decoupled and easy to test&lt;/strong&gt;? Testing is a broad topic with many opinions, and today we’ll explore techniques that will let us have informed discussions about &lt;strong&gt;how we want to test&lt;/strong&gt;. Because good tests aren't just about passing — they’re about helping us &lt;strong&gt;move fast&lt;/strong&gt;, and &lt;strong&gt;deploy with confidence&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;In this guide, we’ll explore practical strategies for injecting dependencies and using test doubles in ways that keep our code &lt;strong&gt;modular&lt;/strong&gt;, our tests &lt;strong&gt;concurrent&lt;/strong&gt;, and our team &lt;strong&gt;productive&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why Test?&lt;/li&gt;
&lt;li&gt;Getting Started with &lt;strong&gt;ExUnit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
Dependency Injection Techniques

&lt;ul&gt;
&lt;li&gt;Mock (Global Overriding)&lt;/li&gt;
&lt;li&gt;Application Config Injection 🛑&lt;/li&gt;
&lt;li&gt;Scoped Injection with Mox&lt;/li&gt;
&lt;li&gt;Function Argument Injection&lt;/li&gt;
&lt;li&gt;Process Hierarchy Injection (via Process Tree)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Test Double Strategies

&lt;ul&gt;
&lt;li&gt;Manual Fakes via Behaviour&lt;/li&gt;
&lt;li&gt;Mox (Simplified Use) 🛑&lt;/li&gt;
&lt;li&gt;Hammox (Type-Safe Mocks)&lt;/li&gt;
&lt;li&gt;Double (Ad-Hoc Mocking)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Test?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvz19sjdpe1m7shcvhcc6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvz19sjdpe1m7shcvhcc6.png" width="800" height="56"&gt;&lt;/a&gt;Automated testing is foundational for &lt;strong&gt;continuous delivery&lt;/strong&gt; and &lt;strong&gt;high-performing teams&lt;/strong&gt;. The question isn’t &lt;em&gt;should&lt;/em&gt; we test — it’s &lt;strong&gt;how&lt;/strong&gt; we test in ways that keep code &lt;strong&gt;simple&lt;/strong&gt;, &lt;strong&gt;decoupled&lt;/strong&gt;, and &lt;strong&gt;easy to evolve&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The DevOps Research &amp;amp; Assessment (DORA) research shows the same thing again and again: teams with &lt;strong&gt;fast feedback loops&lt;/strong&gt; perform better. Testing well isn’t a nice-to-have — it’s a key capability.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW for more on DORA I've written about &lt;a href="https://dev.to/jonlauridsen/an-introduction-to-accelerate-its-dora-metrics-30lh"&gt;"Accelerate", the scientific analysis of software delivery&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A core challenge in test automation lies in managing dependencies between modules. Excessive coupling complicates testing and leads to brittle, intertwined systems. Decoupled modules allow for simpler and clearer systems, and often &lt;strong&gt;test doubles&lt;/strong&gt; are used to isolate dependencies.&lt;/p&gt;

&lt;p&gt;This topic can be contentious because some developers use test doubles sparingly (only at external interfaces like APIs or databases), while others inject them frequently, even between internal systems. Rather than taking sides, we'll focus purely on &lt;strong&gt;how&lt;/strong&gt; we can inject test doubles, as understanding these techniques lets teams make informed decisions about testing strategies.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW if you’re new to terms like "test double", Martin Fowler's &lt;a href="https://martinfowler.com/bliki/TestDouble.html" rel="noopener noreferrer"&gt;Test Doubles&lt;/a&gt; and &lt;a href="https://martinfowler.com/articles/mocksArentStubs.html" rel="noopener noreferrer"&gt;Mocks Aren't Stubs&lt;/a&gt; are excellent starting points.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But enough talk, let's get practical about this.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started with &lt;strong&gt;ExUnit&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwkz1l53f2lefyvgzuj3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwkz1l53f2lefyvgzuj3.png" width="800" height="55"&gt;&lt;/a&gt;&lt;strong&gt;ExUnit&lt;/strong&gt; is Elixir's built-in testing framework. It's simple and fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Math&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;do: &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;MathTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"multiplying two numbers returns their product"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;mix &lt;span class="nb"&gt;test&lt;/span&gt;
…
1 &lt;span class="nb"&gt;test&lt;/span&gt;, 0 failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://hexdocs.pm/ex_unit" rel="noopener noreferrer"&gt;ExUnit is also well-documented&lt;/a&gt; and works great out of the box — so let’s skip the basics and talk about the real challenges: &lt;strong&gt;how we develop simple, decoupled code and tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency Injection Techniques
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5vcxq6gu77lb8nfzkox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5vcxq6gu77lb8nfzkox.png" width="800" height="55"&gt;&lt;/a&gt;Dependency injection helps reduce tightly coupled code. Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;  
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="n"&gt;inventories&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;  
    &lt;span class="n"&gt;inventories&lt;/span&gt;  
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flat_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all_products&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
  &lt;span class="k"&gt;end&lt;/span&gt;  
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(assume both &lt;code&gt;Warehouse&lt;/code&gt; and &lt;code&gt;Inventory&lt;/code&gt; are complex domains in their own rights, backed by database- and API-calls)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here, &lt;code&gt;Warehouse&lt;/code&gt; directly depends on &lt;code&gt;Inventory&lt;/code&gt;, which creates a tight coupling that complicates testing. &lt;/p&gt;

&lt;p&gt;To isolate &lt;code&gt;Warehouse&lt;/code&gt; tests effectively we should look at how Elixir lets us inject test doubles — and where each approach shines (or stumbles).&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mock (Global Overriding)
&lt;/h3&gt;

&lt;p&gt;Elixir allows for sophisticated runtime module swapping, and that's what the &lt;a href="https://hexdocs.pm/mock" rel="noopener noreferrer"&gt;Mock&lt;/a&gt; library (built on Erlang’s &lt;code&gt;:meck&lt;/code&gt;)  enables.&lt;/p&gt;

&lt;p&gt;With Mock, the &lt;code&gt;Warehouse&lt;/code&gt; tests directly mock the &lt;code&gt;Inventory&lt;/code&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;WarehouseTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;async: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Mock&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"warehouse is empty when its inventories have no products"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;with_mock&lt;/span&gt; &lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;all_products: &lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
      &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;Inventory.all_products/1&lt;/code&gt; is mocked to always return an empty list, and so the warehouse is always empty regardless of how the &lt;code&gt;Inventory&lt;/code&gt; domain actually queries and counts its products.&lt;/p&gt;

&lt;p&gt;That Mock can just "reach in" and simply swap out a dependency at runtime is very powerful. But it also comes with drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🛑 &lt;strong&gt;It's Global&lt;/strong&gt;: Tests &lt;strong&gt;cannot run concurrently&lt;/strong&gt; because the module replacement affects the global module namespace. That's bad for performance, and it's a detail that's easy to forget and then the symptoms will be hard-to-debug issues where tests will fail seemingly at random.&lt;/li&gt;
&lt;li&gt;🛑 &lt;strong&gt;It's "Magic"&lt;/strong&gt;: Runtime code doesn't show that dependencies are being swapped, which can cause developer confusion. This is a debatable point that depends on the team, but often more explicit code is preferable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Application Config Injection 🛑
&lt;/h3&gt;

&lt;p&gt;This approach changes the runtime code to read which module to use from Elixir's configuration system (aka &lt;em&gt;Application environment&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;git-nice-diff -U1 HEAD~
&lt;/span&gt;/lib/warehouse.ex
&lt;span class="p"&gt;L#5:
&lt;/span&gt;     inventories
&lt;span class="gd"&gt;-    |&amp;gt; Enum.map(&amp;amp;Inventory.all_products/1)
&lt;/span&gt;&lt;span class="gi"&gt;+    |&amp;gt; Enum.map(&amp;amp;inventory_impl().all_products/1)
&lt;/span&gt;     |&amp;gt; List.flatten()
&lt;span class="p"&gt;L#9:
&lt;/span&gt;   end
&lt;span class="gi"&gt;+  defp inventory_impl(), do: Application.get_env(:myapp, :inventory, Inventory)
&lt;/span&gt; end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And tests can now override the module to use via &lt;code&gt;put_env/4&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;InventoryStub&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;all_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;do: &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;WarehouseTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;async: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"warehouse is empty when its inventories have no products"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:myapp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;InventoryStub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW you might have better ideas for creating a stub than this &lt;code&gt;InventoryStub&lt;/code&gt; module, but for now we're only focused on how stubs are &lt;strong&gt;injected&lt;/strong&gt;, we'll cover smarter ways to create stubs later.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This makes the override &lt;strong&gt;explicit&lt;/strong&gt; — which is good — but it comes with a difficult tradeoff:****&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🛑 &lt;strong&gt;It's (Still) Global:&lt;/strong&gt; The configuration system is global, so tests still cannot run concurrently.&lt;/li&gt;
&lt;li&gt;🛑 &lt;strong&gt;Maybe a Misuse?&lt;/strong&gt; Is it appropriate to use the application environment for dependency switching? It's primarily designed for &lt;strong&gt;user&lt;/strong&gt;-defined configuration of an application's features (e.g. database URLs, feature toggles), should it also expose internal dependencies that users are never intended to adjust?&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ _BTW the &lt;a href="https://hexdocs.pm/elixir/1.15/library-guidelines.html#avoid-application-configuration" rel="noopener noreferrer"&gt;Elixir documentation on library configuration&lt;/a&gt; advises &lt;strong&gt;against&lt;/strong&gt; using the application environment for anything other than truly global settings. While that guidance is for libraries and so not a perfect match for our context, I think it reinforces the idea that it's surprising to have configuration options that aren't meant to be user-configured.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoped Injection with Mox
&lt;/h3&gt;

&lt;p&gt;The widely used &lt;a href="https://hexdocs.pm/mox" rel="noopener noreferrer"&gt;Mox&lt;/a&gt; is Elixir's most widely used mocking library, and it &lt;a href="https://hexdocs.pm/mox/Mox.html#module-example" rel="noopener noreferrer"&gt;&lt;strong&gt;recommends&lt;/strong&gt; the Config-Based injection in its examples&lt;/a&gt;. And it has a smart feature that &lt;strong&gt;scopes mocks to the calling process&lt;/strong&gt; which makes Mox tests concurrent safe. That's a big improvement!&lt;/p&gt;

&lt;p&gt;First step is registering the mock (but note: Mox can only create mocks from behaviour-based modules):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;/test/test_helper.exs
&lt;span class="p"&gt;@@ -2 +2,4 @@&lt;/span&gt; ExUnit.start()
 Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)
&lt;span class="gi"&gt;+
+Mox.defmock(InventoryStub, for: Inventory)
+Application.put_env(:myapp, :inventory, InventoryStub)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes &lt;code&gt;InventoryStub&lt;/code&gt; globally available, and &lt;code&gt;Warehouse&lt;/code&gt; tests can now configure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;WarehouseTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;async: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Mox&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"warehouse is empty when its inventories have no products"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;InventoryStub&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:all_products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works, and these tests run asynchronously so performance is optimal.&lt;/p&gt;

&lt;p&gt;There's still a line being blurred by utilizing &lt;strong&gt;user config&lt;/strong&gt; for &lt;strong&gt;internal dependency switching&lt;/strong&gt;, but as long as a team can commit to doing things "the Mox way" it'll work fine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🛑 &lt;strong&gt;Mocks are globally registered:&lt;/strong&gt; Because of the configuration system we are forced to come up with names for our inventory keys. In our example the key &lt;code&gt;myapp.inventory&lt;/code&gt; is a small accidental complexity because it only exists to facilitate the internal injection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far, we’ve explored a few solid techniques. But Elixir gives us even more options — and there's still room for a more elegant solution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW Mox's is not too opinionated on how one should inject its mocks, I think its documentation suggests Config-Based because it's easy to explain and requires no additional tools. The blog that gave rise to Mox includes a hint that one does not have to use the config system: &lt;a href="https://dashbit.co/blog/mocks-and-explicit-contracts" rel="noopener noreferrer"&gt;José Valim's "Mocks and explicit contracts"&lt;/a&gt;. It's a good read whichever way you prefer injecting dependencies.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Function Argument Injection
&lt;/h3&gt;

&lt;p&gt;Injecting by argument is a classic technique:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;git-nice-diff -U1 HEAD~
&lt;/span&gt;/lib/warehouse.ex
&lt;span class="p"&gt;L#3:
&lt;/span&gt;&lt;span class="gd"&gt;-  def empty?(%Warehouse{inventories: inventories}) do
&lt;/span&gt;&lt;span class="gi"&gt;+  def empty?(%Warehouse{inventories: inventories}, inventory_impl) do
&lt;/span&gt;     inventories
&lt;span class="gd"&gt;-    |&amp;gt; Enum.map(&amp;amp;inventory_impl().all_products/1)
&lt;/span&gt;&lt;span class="gi"&gt;+    |&amp;gt; Enum.map(&amp;amp;inventory_impl.all_products/1)
&lt;/span&gt;     |&amp;gt; List.flatten()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way tests can easily pass a test double:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;InventoryStub&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;all_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;do: &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;WarehouseTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;async: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"warehouse is empty when its inventories have no products"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;InventoryStub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Argument injection can be especially useful for modules that are "orchestration-focused", meaning their &lt;strong&gt;purpose&lt;/strong&gt; is to orchestrate dependencies. But be weary of issues such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🛑 &lt;strong&gt;Argument pollution&lt;/strong&gt;: It's &lt;strong&gt;very problematic&lt;/strong&gt; if functions that are already doing a thing ends up polluted by also having to specify dependencies. Callers shouldn't have to wire up internal systems unless that's the point of that module.&lt;/li&gt;
&lt;li&gt;🛑 &lt;strong&gt;Challenging to Scale&lt;/strong&gt;: It can become cumbersome to keep multiple functions aligned in how they deal with dependencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Process Hierarchy Injection (via Process Tree)
&lt;/h3&gt;

&lt;p&gt;Elixir’s process model gives a unique superpower: hierarchical test isolation. Using the &lt;a href="https://github.com/jbsf2/process-tree" rel="noopener noreferrer"&gt;ProcessTree&lt;/a&gt; library, we can inject dependencies scoped to a test’s process &lt;strong&gt;and all its children&lt;/strong&gt; — without globals or polluting arguments.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW for more details about ProcessTree see JB Steadman's blog  &lt;a href="https://saltycrackers.dev/posts/bye-bye-async-false/" rel="noopener noreferrer"&gt;&lt;code&gt;async: false&lt;/code&gt; is the worst. Here's how to never need it again&lt;/a&gt;. Great blog, and my thanks to JB for making this library available 🙏.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, to make it simple and easy to use &lt;strong&gt;ProcessTree&lt;/strong&gt; we'll quickly wrap it in our own module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;InjectorTree&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="vi"&gt;@doc&lt;/span&gt; &lt;span class="s2"&gt;"""
  Injects the specified stub such that it gets registered as an override
  in the process hierarchy.
  """&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;injector_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:injector_tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sx"&gt;%{}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:injector_tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;injector_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="vi"&gt;@doc&lt;/span&gt; &lt;span class="s2"&gt;"""
  Provides (aka returns) the specified module such that if that module
  has been injected then that override is what gets returned.

  If the module has not been injected the specified module is returned.
  """&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;ProcessTree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:injector_tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="sx"&gt;%{}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we make the &lt;code&gt;Warehouse&lt;/code&gt; runtime code "&lt;strong&gt;provide&lt;/strong&gt;" an &lt;code&gt;Inventory&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;git-nice-diff -U1 HEAD~
&lt;/span&gt;/lib/warehouse.ex
&lt;span class="p"&gt;L#1:
&lt;/span&gt; defmodule Warehouse do
&lt;span class="gi"&gt;+  import InjectorTree, only: [provide: 1]
+
&lt;/span&gt;   defstruct [:inventories]
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-  def empty?(%Warehouse{inventories: inventories}, inventory_impl) do
&lt;/span&gt;&lt;span class="gi"&gt;+  def empty?(%Warehouse{inventories: inventories}) do
&lt;/span&gt;     inventories
&lt;span class="gd"&gt;-    |&amp;gt; Enum.map(&amp;amp;inventory_impl.all_products/1)
&lt;/span&gt;&lt;span class="gi"&gt;+    |&amp;gt; Enum.map(&amp;amp;provide(Inventory).all_products/1)
&lt;/span&gt;     |&amp;gt; List.flatten()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And tests can now "&lt;strong&gt;inject&lt;/strong&gt;" a stub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;WarehouseTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;async: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;InjectorTree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;inject: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"warehouse is empty when its inventories have no products"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;InventoryStub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ This technique offers clear, explicit, and isolated dependency injection, granting us &lt;strong&gt;concurrent-safe tests&lt;/strong&gt; while keeping runtime code &lt;strong&gt;clean and explicit&lt;/strong&gt;. Very cool!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW a more sophisticated implementation of &lt;code&gt;InjectorTree&lt;/code&gt; could probably compile itself out in production to avoid any theoretical overhead caused by traversing the process hierarchy. But such optimizations are beyond the scope of this article.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Double Strategies
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsc0v90kz19qtzdz6ydji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsc0v90kz19qtzdz6ydji.png" width="800" height="55"&gt;&lt;/a&gt;Now that we’ve seen how to &lt;em&gt;inject&lt;/em&gt; dependencies — let’s talk about &lt;strong&gt;how to build test doubles&lt;/strong&gt; that are simple, safe, and aligned with our code.&lt;/p&gt;

&lt;p&gt;The Elixir community offers several robust methods for defining test doubles, we'll examine them in more detail and consider their strengths and potential trade-offs.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual Fakes via Behaviour
&lt;/h3&gt;

&lt;p&gt;A straightforward yet powerful approach involves explicitly creating fake implementations using Elixir behaviours. This keeps the fake and the real implementation synchronized, enforced by compile-time checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;FakeInventory&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="vi"&gt;@behaviour&lt;/span&gt; &lt;span class="no"&gt;Inventory&lt;/span&gt;
  &lt;span class="vi"&gt;@impl&lt;/span&gt; &lt;span class="no"&gt;Inventory&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;all_products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;do: &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests then inject the fake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;WarehouseTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;async: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;InjectorTree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;inject: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"warehouse is empty when its inventories have no products"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RealInventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FakeInventory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method is powerful because it is explicit and straightforward, and require no external dependencies.&lt;/p&gt;

&lt;p&gt;However, the approach can be effort-intensive, as maintaining parallel implementations (real and fake) demands careful designing, and the fake can require its own tests if complexity grows significantly.&lt;/p&gt;

&lt;p&gt;Despite the maintenance cost, this is a highly effective and explicit approach for test doubles.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mox (Simplified Use) 🛑
&lt;/h3&gt;

&lt;p&gt;Yes we're covering &lt;a href="https://hexdocs.pm/mox" rel="noopener noreferrer"&gt;Mox&lt;/a&gt; again, but this we'll simplify its use by only using it to create a mock (we'll not rely on Mox's "per-process mock scoping" feature):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;WarehouseTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;async: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;InjectorTree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;inject: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Mox&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"warehouse is empty when its inventories have no products"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;RealInventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;defmock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;InventoryMock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;for: &lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:all_products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change may appear subtle, but this code is &lt;strong&gt;simpler&lt;/strong&gt;: Mox is used to create a test double, but nothing is hidden in how that test double gets injected. It's all nicely explicit code, where all the steps of creating, injecting, and configuring the mock is expressed right in the test file.&lt;/p&gt;

&lt;p&gt;And Mox has validation to ensure mocks align with the defined behaviour, preventing incorrect mocking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U0 .
/test/warehouse_test.exs
&lt;span class="p"&gt;@@ -10 +10 @@&lt;/span&gt; defmodule WarehouseTest do
&lt;span class="gd"&gt;-      |&amp;gt; stub(:all_products, fn _ -&amp;gt; [] end)
&lt;/span&gt;&lt;span class="gi"&gt;+      |&amp;gt; stub(:FOO, fn -&amp;gt; "bar" end)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;$ mix test
  1) test warehouse is empty when its inventories have no products (WarehouseTest)
     ** (ArgumentError) unknown function FOO/0 for mock InventoryMock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great! And the &lt;a href="https://hexdocs.pm/mox" rel="noopener noreferrer"&gt;Mox documentation is exceptionally good and well worth a read&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Mox, however, does not care about typespec enforcement, but the library &lt;strong&gt;Hammox&lt;/strong&gt; promises to fix that so lets give that a try next.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ _BTW this section marks Mox with a red "do not use" sign only because Hammox turns out to be a more powerful drop-in replacement. It's not a sign of criticism, Mox is awesome!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Hammox (Type-Safe Mocks)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://hexdocs.pm/hammox" rel="noopener noreferrer"&gt;Hammox&lt;/a&gt; extends Mox by adding typespec validation. It's a drop-in replacement, bringing greater safety and validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/test/warehouse_test.exs
&lt;span class="p"&gt;L#3:
&lt;/span&gt;   import InjectorTree, only: [inject: 2]
&lt;span class="gd"&gt;-  import Mox
&lt;/span&gt;&lt;span class="gi"&gt;+  import Hammox
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;$ mix test
…
&lt;span class="p"&gt;7 tests, 0 failures
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hammox ensures return values conform to typespecs, e.g. here the mock returns an invalid number-list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U0 .
/test/warehouse_test.exs
&lt;span class="p"&gt;@@ -10 +10 @@&lt;/span&gt; defmodule WarehouseTest do
&lt;span class="gd"&gt;-      |&amp;gt; stub(:all_products, fn _ -&amp;gt; [] end)
&lt;/span&gt;&lt;span class="gi"&gt;+      |&amp;gt; stub(:all_products, fn _ -&amp;gt; [1] end)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;$ mix test
  1) test warehouse is empty when its inventories have no products (WarehouseTest)
     test/warehouse_test.exs:6
     ** (Hammox.TypeMatchError) 
     Returned value [1] does not match type [String.t()].
&lt;span class="p"&gt;7 tests, 1 failure
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's very cool! This added safety makes &lt;strong&gt;Hammox&lt;/strong&gt; particularly recommended for behaviour-based mocking.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Double (Ad-Hoc Mocking)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://hexdocs.pm/double" rel="noopener noreferrer"&gt;Double&lt;/a&gt; addresses situations where behaviours feel like unnecessary overhead. It specifically allows ad-hoc mocking without behaviours, offering quick and flexible mocks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/test/test_helper.exs
&lt;span class="p"&gt;L#1:
&lt;/span&gt; ExUnit.start()
&lt;span class="gi"&gt;+Application.ensure_all_started(:double)
&lt;/span&gt; Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW a lesser known side-effect of behaviours is that implementations do not auto-complete in IEx. That's by design. It's a small extra friction that &lt;strong&gt;Double&lt;/strong&gt; avoids by allowing ad-hoc mocking.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The test can now stub &lt;code&gt;Inventory&lt;/code&gt;, even when it's just a plain old Elixir module (no behaviours, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;WarehouseTest&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ExUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;async: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;InjectorTree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;inject: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Double&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"warehouse is empty when its inventories have no products"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Inventory&lt;/span&gt; &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:all_products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;warehouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;inventories: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt; &lt;span class="no"&gt;Warehouse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;strong&gt;very&lt;/strong&gt; elegant. And Double also provides correctness-validation, e.g. we can't stub a function that doesn't exist on the target module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U0 .
/test/warehouse_test.exs
&lt;span class="p"&gt;@@ -7 +7 @@&lt;/span&gt; defmodule WarehouseTest do
&lt;span class="gd"&gt;-    inject(Inventory, Inventory |&amp;gt; stub(:all_products, fn _ -&amp;gt; [] end))
&lt;/span&gt;&lt;span class="gi"&gt;+    inject(Inventory, Inventory |&amp;gt; stub(:FOO, fn -&amp;gt; "BAR" end))
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;$ mix test
  1) test warehouse is empty when its inventories have no products (WarehouseTest)
     test/warehouse_test.exs:6
     ** (VerifyingDoubleError) The function 'FOO/0' is not defined in :InventoryDouble326
&lt;span class="p"&gt;7 tests, 1 failure
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, Double &lt;strong&gt;doesn't&lt;/strong&gt; verify stubbed return values against typespecs. If pragmatic flexibility and simplicity matter more to your team than strict typespec enforcement, Double is an excellent choice.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fse3jc9le3zad0p8xcijs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fse3jc9le3zad0p8xcijs.png" width="800" height="56"&gt;&lt;/a&gt;We’ve covered a lot — but the takeaway is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ Fast tests&lt;br&gt;
✅ Clear dependencies&lt;br&gt;
✅ Concurrent, isolated setups&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use &lt;strong&gt;Manual Fakes&lt;/strong&gt; for clarity, &lt;strong&gt;Hammox&lt;/strong&gt; for strictness, or &lt;strong&gt;Double&lt;/strong&gt; for pragmatic speed. And consider &lt;strong&gt;Process Hierarchy Injection&lt;/strong&gt; for clean, concurrent-safe test wiring.&lt;/p&gt;

&lt;p&gt;Ultimately there’s no single best way to test — the focus should be on what lets your team &lt;strong&gt;move fast&lt;/strong&gt; and &lt;strong&gt;ship with confidence&lt;/strong&gt;. Whatever combo you choose — stay explicit, keep things simple, and test fearlessly 💪.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW I’ve seen complex “Inversion of Control” frameworks that turned code into spaghetti. Be cautious of cleverness — &lt;strong&gt;simplicity scales best&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>elixir</category>
      <category>tutorial</category>
      <category>development</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Perfect Elixir: Simplifying Elixir Developer Onboarding</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Mon, 28 Oct 2024 13:00:00 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/perfect-elixir-onboarding-10o5</link>
      <guid>https://forem.com/jonlauridsen/perfect-elixir-onboarding-10o5</guid>
      <description>&lt;p&gt;Onboarding can be a costly but hidden drag on a team — not just in raw setup time, but also in how easily it becomes a process full of uncertainty and fragility. How developers get started reflects the standards of care a team chooses to uphold, and we should explore how simple and safe we can make this experience.&lt;/p&gt;

&lt;p&gt;This article will explore how to streamline onboarding through a simple, semi-automated script — striking a balance between ease of use and maintainability.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW I've started on projects where onboarding could take &lt;strong&gt;two weeks&lt;/strong&gt; 😱. Senior devs spent hours debugging others' setups — a time sink repeated with every new hire. We can do better!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Redefining Onboarding&lt;/li&gt;
&lt;li&gt;Beware Global System Changes&lt;/li&gt;
&lt;li&gt;
Implementing the Onboarding Script

&lt;ul&gt;
&lt;li&gt;Building Trust&lt;/li&gt;
&lt;li&gt;Step 1: Install pkgx&lt;/li&gt;
&lt;li&gt;Step 2: Activate Shell Integration&lt;/li&gt;
&lt;li&gt;Step 3: Clone the Repository&lt;/li&gt;
&lt;li&gt;Step 4: Activate the Development Environment&lt;/li&gt;
&lt;li&gt;Onboarding complete!&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Redefining Onboarding
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxs36zv70572ud6f28qr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxs36zv70572ud6f28qr.png" width="800" height="56"&gt;&lt;/a&gt;When we say onboarding, we mean everything a new developer needs to do to start working on our project.&lt;/p&gt;

&lt;p&gt;The traditional approach relies on written guides. But guides come with downsides:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No testability:&lt;/strong&gt; They can't be validated automatically, easily drifting out of date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity grows:&lt;/strong&gt; Old steps tend to stick around "just in case", with new ones piling on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard to maintain:&lt;/strong&gt; Updates happen under pressure, usually when someone's blocked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What if we reimagined onboarding as a &lt;strong&gt;single command&lt;/strong&gt; — no dependencies, no guesswork — that guides the developer interactively?&lt;/p&gt;

&lt;p&gt;On macOS and Linux systems, the simplest way to run a remote script is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-Ssf&lt;/span&gt; https://…/script | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's beautifully simple, but it comes with a limit: To onboard we'll want to inspect the user's environment to understand what tools they have configured, and that's not possible when piping to &lt;code&gt;sh&lt;/code&gt; (since it spawns a new shell that is not necessarily the same as their working shell).&lt;/p&gt;

&lt;p&gt;Instead, using &lt;code&gt;source&lt;/code&gt; allows the script to access the user's environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://…/script &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/script &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; /tmp/script
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's still incredibly simple, and with this approach, we can move toward a fully guided, low-friction setup. Next, let's look at one of the trickier parts: installing system tools.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Beware Global System Changes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffl1483kl7lo39k0pjabt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffl1483kl7lo39k0pjabt.png" width="800" height="56"&gt;&lt;/a&gt;For our project, the developer must have installed &lt;a href="https://pkgx.sh" rel="noopener noreferrer"&gt;pkgx&lt;/a&gt;, as introduced in the &lt;a href="https://dev.to/jonlauridsen/perfect-elixir-environment-setup-1145#pgkx"&gt;Setting Up an Elixir Dev Environment&lt;/a&gt; article. But how do we do that?&lt;/p&gt;

&lt;p&gt;First instinct could be to automatically install it, but installing system tools is a risky proposition — here's why we &lt;strong&gt;must not&lt;/strong&gt; do it automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's invasive:&lt;/strong&gt; Developers customize their environments. Global changes can break their system or other projects. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's brittle:&lt;/strong&gt; There are a &lt;strong&gt;lot&lt;/strong&gt; of environment variance out there, very difficult to write code that will work reliably across all of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's high-maintenance:&lt;/strong&gt; Continuously testing all the various supported permutations spirals out of control fast&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Luckily, we only need &lt;code&gt;pkgx&lt;/code&gt;, because it takes care of the rest of the tooling. And it's easy to install manually — no sudo needed. So we should &lt;strong&gt;ask&lt;/strong&gt; the developer to install it, and keep our script focused on guidance, not installation. That will also make our onboarding script much &lt;strong&gt;safer&lt;/strong&gt; and much easier to &lt;strong&gt;test&lt;/strong&gt; — win-win.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing the Onboarding Script
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fklagu96e6yvrvgjxbf20.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fklagu96e6yvrvgjxbf20.png" width="800" height="56"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building Trust
&lt;/h3&gt;

&lt;p&gt;Our first priority: earn the developer's trust.&lt;/p&gt;

&lt;p&gt;Instead of doing anything upfront, our script can clearly explain what it will do — and what it won't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ URL="https://raw.githubusercontent.com/gaggle/perfect-elixir/refs/heads/perfect-elixir-5-onboarding/bin/bootstrap"

$ curl -fsSL $URL &amp;gt; /tmp/boot &amp;amp;&amp;amp; source /tmp/boot
🚀 Welcome to the Perfect Elixir Onboarding Script! 🚀

This script will help you set up your development environment for our project. 

🔒 Trust and Transparency: 
  - This script NEVER makes changes to your system
  - It ONLY inspects your environment and makes suggestions

Ready to proceed? [y/N]:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4zy1s0t129pe8fup11dq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4zy1s0t129pe8fup11dq.gif" alt="Bootstrap script introducing itself and prompting the user to proceed" width="584" height="184"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Clear. Transparent. Opt-in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW I'm skipping the underlying code here (it just prints text), but &lt;a href="https://github.com/gaggle/perfect-elixir/blob/perfect-elixir-5-onboarding/bin/bootstrap" rel="noopener noreferrer"&gt;the full script is available on GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install pkgx
&lt;/h3&gt;

&lt;p&gt;We check if &lt;code&gt;pkgx&lt;/code&gt; is installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ok to proceed? [y/N]: y
• Checking for pkgx… x

📦 pkgx is not installed

User action required: Install pkgx
──────────────────────────────────
pkgx is our package manager for handling system dependencies. Learn more at https://pkgx.sh.

Here are quick ways to install it:
1. Via Homebrew:
   $ brew install pkgxdev/made/pkgx
2. Via cURL:
   $ curl -Ssf https://pkgx.sh | sh
Other installation methods at https://docs.pkgx.sh/run-anywhere/terminals.

ℹ️ pkgx installation is simple, and installing via Homebrew does not require sudo.

After pkgx has been installed please source this script again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now the power is in the user's hands, and they can choose to install it according to their preferences. After installation, the user re-sources the script and continues.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Activate Shell Integration
&lt;/h3&gt;

&lt;p&gt;Next, we check if &lt;code&gt;pkgx&lt;/code&gt;'s &lt;strong&gt;shell integration&lt;/strong&gt; is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ok to proceed? [y/N]: y
• Checking for pkgx… ✓
• Checking pkgx shell integration… x

🔧 pkgx shell integration is not active

User action required: Activate pkgx shell integration
─────────────────────────────────────────────────────
To activate pkgx shell integration, run the following command:

  $ pkgx dev integrate

Integration writes one line to your .shellrc.

ℹ️ For more information about shell integration see: https://docs.pkgx.sh/using-pkgx/shell-integration.

After shell integration has been activated please source this script again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell integration unlocks &lt;code&gt;pkgx&lt;/code&gt;'s &lt;code&gt;dev&lt;/code&gt; command — our tool for managing project-specific dependencies.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Clone the Repository
&lt;/h3&gt;

&lt;p&gt;Now we're ready to check for our repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Ok to proceed? [y/n]: y
• Checking for pkgx… ✓
• Checking pkgx shell integration… ✓
• Checking repository is cloned… x

🌿 Repository not found

User action required: Clone the repository
──────────────────────────────────────────
Please clone the repository using your preferred method:

1. Via GitHub CLI:
&lt;/span&gt;&lt;span class="gp"&gt;   $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pkgx gh repo clone gaggle/perfect-elixir
&lt;span class="go"&gt;2. Via SSH:
&lt;/span&gt;&lt;span class="gp"&gt;   $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone git@github.com:gaggle/perfect-elixir.git
&lt;span class="go"&gt;3. Via HTTPS:
&lt;/span&gt;&lt;span class="gp"&gt;   $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/gaggle/perfect-elixir.git
&lt;span class="go"&gt;
After cloning, navigate to the project directory and source this script again.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW the first suggestion leverages &lt;code&gt;pkgx&lt;/code&gt;'s ability to run arbitrary tools, in this case the command would ephemerally run GitHub's CLI. Quite simple!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Activate the Development Environment
&lt;/h3&gt;

&lt;p&gt;Finally, we ensure the &lt;code&gt;pkgx&lt;/code&gt; &lt;strong&gt;development environment&lt;/strong&gt; is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Ok to proceed? [y/n]: y
• Checking for pkgx… ✓
• Checking pkgx shell integration… ✓
• Checking repository is cloned… ✓
• Checking development environment is active… x

🔧 Development environment is not active

User action required: Activate the development environment
──────────────────────────────────────────────────────────
To activate the development environment, run:

&lt;/span&gt;&lt;span class="gp"&gt;  $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dev
&lt;span class="go"&gt;
This will make project-specific versions of dependencies available within the repository.

After activating, please source this script again.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Onboarding complete!
&lt;/h3&gt;

&lt;p&gt;When all checks pass, the script confirms completion and guides the user to start using our development workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Ok to proceed? [y/n]: y
• Checking for pkgx… ✓
• Checking pkgx shell integration… ✓
• Checking repository is cloned… ✓
• Checking development environment is active… ✓

✅ Onboarding complete!

- To start working on the project, run:

&lt;/span&gt;&lt;span class="gp"&gt;    $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bin/doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;br&gt;
Here is the full flow from a factory-reset machine to being ready to work on the project:



&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xuio9tpnv4g8kexivd9a.gif" rel="noopener noreferrer"&gt;Animated gif showing "&lt;strong&gt;Running bootstrap script on a fresh machine, pasting in each of the suggested commands until onboarding is complete&lt;/strong&gt;"&lt;/a&gt;&lt;br&gt;
(dev.to fails to inline this .gif, so it's a hyperlink instead)&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This clear, step-by-step approach ensures the developer can get fully set up in a few minutes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW when extending onboarding to cover more steps, it's useful to note that past the first &lt;code&gt;pkgx&lt;/code&gt; step the entire &lt;code&gt;pkgx&lt;/code&gt; ecosystem is available. That means subsequent steps can easily leverage powerful tools such as GitHub CLI, or specific programming languages, and lots more. It's very powerful.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fioqfasxbgrieypjffnyc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fioqfasxbgrieypjffnyc.png" width="800" height="56"&gt;&lt;/a&gt;We set out to rethink onboarding, and landed on something radically simple: A single command that guides developers through a safe, low-friction setup. The entire onboarding instruction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Run, and follow the steps: &lt;code&gt;$ curl -fsSL https://…/script &amp;gt; /tmp/script &amp;amp;&amp;amp; source /tmp/script&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's minimal, extensible, and respectful of the developer’s environment. By avoiding invasive changes, the script stays safe and is easy to test — qualities that matter for teams who care about maintainability.&lt;/p&gt;

&lt;p&gt;In our case setup is especially simple because the repository is public, but it works just as well to host the script behind a VPN, on a secure static server — the principles still hold.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW when implementing script-based onboarding, keep these security practices in mind:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Host on trusted infrastructure&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Consider checksums for verification&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Inspect scripts before running them&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The real strength of this approach is its flexibility. Once &lt;code&gt;pkgx&lt;/code&gt; is available, the script can evolve to include anything — repo access checks, CLI authentication, project prerequisites, even ticketing workflows.&lt;/p&gt;

&lt;p&gt;This small investment pays off quickly. A smooth first experience sends a clear message: this is a team that values quality — from first command to first commit.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>tutorial</category>
      <category>development</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Perfect Elixir: Streamlining Development Workflows</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Mon, 17 Jun 2024 12:25:00 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/perfect-elixir-development-workflows-26k6</link>
      <guid>https://forem.com/jonlauridsen/perfect-elixir-development-workflows-26k6</guid>
      <description>&lt;p&gt;Today we'll define and implement our daily development workflows. We'll begin by identifying what makes a workflow truly effective — drawing from cutting-edge research on software delivery practices. Then we'll establish &lt;strong&gt;really simple&lt;/strong&gt; mechanisms for managing code changes so we can move quickly and accurately as a team. And importantly, these workflows must be flexible enough to scale as our team and product grows. Let's dive in!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
A Reflection on Workflows

&lt;ul&gt;
&lt;li&gt;Don't Use Branches&lt;/li&gt;
&lt;li&gt;In Defense of Shell Scripting&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Doctor&lt;/li&gt;

&lt;li&gt;Update&lt;/li&gt;

&lt;li&gt;Shipit&lt;/li&gt;

&lt;li&gt;Continuous Code Reviewing&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Reflection on Workflows
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2rdk3gxvoz9obsue5cf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2rdk3gxvoz9obsue5cf.png" width="800" height="56"&gt;&lt;/a&gt;Let's begin with a simple question: &lt;strong&gt;What exactly is a "workflow"?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most teams have some loose process — &lt;em&gt;clone the repo&lt;/em&gt;, &lt;em&gt;open a pull request&lt;/em&gt;, &lt;em&gt;get it reviewed&lt;/em&gt;. That's a workflow, technically. But we can do better by going back to first principles.&lt;/p&gt;

&lt;p&gt;Are some ways of working &lt;strong&gt;objectively better&lt;/strong&gt; than others? According to the &lt;a href="https://dora.dev" rel="noopener noreferrer"&gt;DevOps Research and Assessment&lt;/a&gt; (DORA) project — absolutely.&lt;/p&gt;

&lt;p&gt;DORA has spent nearly a decade studying thousands of engineering teams, using rigorous, peer-reviewed methods. Their research doesn't just find patterns — it finds &lt;strong&gt;causal relationships&lt;/strong&gt;. That means: improving your DORA capabilities is statistically likely to &lt;strong&gt;make your team more effective&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two of the most powerful predictors of performance are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Minimal lead time&lt;/strong&gt; — code should go from commit to production in under an hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High deployment frequency&lt;/strong&gt; — ideally, each commit is a deploy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These aren't abstract ideals — they're backed by real-world data. And they tell us something important:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To improve how we build software, we must build **workflows that support continuous code flow&lt;/em&gt;* — fast, safe, and always moving forward.*&lt;/p&gt;

&lt;p&gt;Let's find out what that can actually look like.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW want to dive deeper into the research? Check out my &lt;a href="https://dev.to/jonlauridsen/an-introduction-to-accelerate-its-dora-metrics-30lh"&gt;intro to Accelerate and DORA metrics&lt;/a&gt;, or &lt;a href="https://dev.to/jonlauridsen/the-software-delivery-performance-model-dora-metrics-2nf4"&gt;explore the full Software Delivery Performance Model&lt;/a&gt;. And if you haven't yet, I can't recommend their &lt;a href="https://www.amazon.com/Accelerate-Software-Performing-Technology-Organizations/dp/1942788339" rel="noopener noreferrer"&gt;&lt;strong&gt;Accelerate&lt;/strong&gt;&lt;/a&gt; book enough — it's essential reading.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't Use Branches
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmyfdtkc2us95f9v91ns4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmyfdtkc2us95f9v91ns4.png" width="800" height="56"&gt;&lt;/a&gt;The DORA metrics forces us to a key realization:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branches introduce delays in continuously integrating and delivering code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's why:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Branches add time between writing code and running it in production. The fastest path are small changes pushed continuously to &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Branches often accumulate multiple commits, reducing deployment frequency. The highest-performing teams push small changes straight to &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To some, that may sound unsafe. But what we're describing is a well-established practice: &lt;strong&gt;Trunk-Based Development&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW if that sounds unfamiliar or intimidating, I've written a &lt;a href="https://dev.to/jonlauridsen/beginners-intro-to-trunk-based-development-3158"&gt;Beginner's Intro to Trunk-Based Development&lt;/a&gt;. Also, the &lt;a href="https://dora.dev/devops-capabilities/technical/trunk-based-development/" rel="noopener noreferrer"&gt;DORA research&lt;/a&gt; explores this topic in detail.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Still, we need to answer some important questions:&lt;/p&gt;

&lt;p&gt;If we don't use branches, we're also not using pull requests. So how do we protect code quality? Where do tests run? What about linting, security checks, and other automations?&lt;/p&gt;

&lt;p&gt;The answer: &lt;strong&gt;Shift the workflows left&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Move checks to the developer's machine. Run tests, linters, and verifications &lt;strong&gt;before&lt;/strong&gt; pushing to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In other words, we need fast, &lt;strong&gt;local&lt;/strong&gt; workflows that feel lightweight — but offer strong safety guarantees. And they need to be simple enough to evolve with the team.&lt;/p&gt;

&lt;p&gt;So how do we do that? How do we make sure code is safe to ship &lt;strong&gt;without&lt;/strong&gt; branches or PRs?&lt;/p&gt;

&lt;p&gt;That's exactly what this article will explore. Let's get into it.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  In Defense of Shell Scripting
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fan8dbfhkmveg75uwzdku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fan8dbfhkmveg75uwzdku.png" width="800" height="56"&gt;&lt;/a&gt;Now that we've established the need for fast, reliable workflows, we need need to dig into how we can safely and continuously pull and push code. Thanks to &lt;code&gt;pkgx&lt;/code&gt; we can use any language to write our workflows in — so what should we choose?&lt;/p&gt;

&lt;p&gt;I suggest we start with humble &lt;strong&gt;Shell Scripting&lt;/strong&gt;. Here's why:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Industry Standard&lt;/strong&gt; – Shell is the default choice for scripting across virtually all engineering teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least Surprising&lt;/strong&gt; – Most developers already know it, or can at least read it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical &amp;amp; Low-Maintenance&lt;/strong&gt; – It's not the prettiest, but it gets the job done with minimal overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable &amp;amp; Consistent&lt;/strong&gt; – With &lt;code&gt;pkgx&lt;/code&gt;, we ensure everyone runs the same version of &lt;code&gt;bash&lt;/code&gt;, so there won't be "works on my machine"-issues.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep in mind we're not here to build beautiful workflow code — we're here to ship products. Scripts can still be &lt;strong&gt;important&lt;/strong&gt;, but they're not where we should do exciting new things that create exciting new problems. So it makes sense to choose the simplest, most boring tool for the job: shell scripts.&lt;/p&gt;

&lt;p&gt;This mindset reflects a core principle for any product team: minimize complexity, stay pragmatic, and focus on what matters — shipping value, fast and safely.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Doctor
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6m5e4e1cfs5kuuds735.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6m5e4e1cfs5kuuds735.png" width="800" height="56"&gt;&lt;/a&gt;Let's kick off our workflows with a script to keep development environments healthy across the team by checking vital preconditions like whether the database is running and dependencies are installed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW I like calling this script &lt;strong&gt;doctor&lt;/strong&gt; because it verifies the health of our environment — but you can pick any name that fits your team.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Back in the "&lt;a href="https://dev.to/jonlauridsen/perfect-elixir-environment-setup-1145"&gt;Setting Up an Elixir Dev Environment&lt;/a&gt;" article, we picked &lt;code&gt;pkgx&lt;/code&gt; to manage our dev environment. So first, let's verify it's installed and active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;bin/doctor
&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.shhelpers"&lt;/span&gt;

check &lt;span class="s2"&gt;"pkgx installed?"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"which pkgx"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"brew install pkgxdev/made/pkgx"&lt;/span&gt;

check &lt;span class="s2"&gt;"Developer environment active?"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"which erl &amp;amp;&amp;amp; which elixir"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"dev"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW this sources &lt;code&gt;.shhelpers&lt;/code&gt; for utility functions such as &lt;code&gt;check&lt;/code&gt;. &lt;a href="https://github.com/gaggle/perfect-elixir/blob/perfect-elixir-4-development-workflows/bin/.shhelpers" rel="noopener noreferrer"&gt;The full &lt;code&gt;.shhelpers&lt;/code&gt; file is available here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Running &lt;code&gt;doctor&lt;/code&gt; now gives us a clean bill of health:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bin/doctor
• pkgx installed? ✓
• Developer environment active? ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcs6ifki6hasoqysnpb3i.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcs6ifki6hasoqysnpb3i.gif" alt="Running bin/doctor, showing initial checks passing with green checkmark" width="584" height="88"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, let's build toward starting our Phoenix app (chosen back in "&lt;a href="https://dev.to/jonlauridsen/perfect-elixir-foundations-of-a-web-app-95l"&gt;Building a Basic Elixir Web App&lt;/a&gt;"). First up: Make sure the local database is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/bin/doctor
&lt;span class="p"&gt;@@ -12 +12,5 @@&lt;/span&gt; check "Developer environment active?" \
   "dev"
&lt;span class="gi"&gt;+
+check "PostgreSQL server running?" \
+  "pgrep -f bin/postgres" \
+  "bin/db start"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW this references &lt;code&gt;bin/db&lt;/code&gt;, a small script to easily start and stop the database. &lt;a href="https://github.com/gaggle/perfect-elixir/blob/perfect-elixir-4-development-workflows/bin/db" rel="noopener noreferrer"&gt;You can find it here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the database isn't running &lt;code&gt;doctor&lt;/code&gt; now fails and suggests a fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bin/doctor
• pkgx installed? ✓
• Developer environment active? ✓
• PostgreSQL server running? x
&amp;gt; Executed: pgrep -f bin/postgres

Suggested remedy: bin/db start
(Copied to clipboard)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And running the suggestion works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bin/db start
• Creating /Users/cloud/perfect-elixir/priv/db ✓
• Initializing database ✓
• Database started:
waiting for server to start.... done
server started
↳ Database started ✓

$ bin/doctor
• pkgx installed? ✓
• Developer environment active? ✓
• PostgreSQL server running? ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9af5f0vbb3gudpc1zhb0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9af5f0vbb3gudpc1zhb0.gif" alt="Running bin/doctor which now fails because server is not running. Doctor's suggested remedy is run, and then doctor is rerun and now passes with all green checkmarks" width="584" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the &lt;strong&gt;doctor&lt;/strong&gt; pattern: check for issues, suggest a fix. It's easy to understand, easy to extend.&lt;/p&gt;

&lt;p&gt;Let's jump ahead to a more complete version — with checks covering everything needed to start the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bin/doctor

Running doctor checks…
• pkgx installed? ✓
• Developer environment active? ✓
• PostgreSQL server running? ✓
• PostgreSQL server has required user? ✓
• Hex package manager installed? ✓
• Mix dependencies installed &amp;amp; compiled? ✓
• PostgreSQL database exists? ✓

✓ All checks passed, system is healthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2kwwlck8ue0kxafz3d45.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2kwwlck8ue0kxafz3d45.gif" alt="Running  raw `bin/doctor` endraw  showing all green checkmarks, reporting the system is healthy and ready" width="584" height="232"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;

&lt;p&gt;ℹ️ &lt;em&gt;BTW the full &lt;a href="https://github.com/gaggle/perfect-elixir/blob/perfect-elixir-4-development-workflows/bin/doctor" rel="noopener noreferrer"&gt;&lt;code&gt;doctor&lt;/code&gt; script is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And since everything is passing, we can launch our app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ iex -S mix phx.server
[info] Running MyAppWeb.Endpoint with Bandit 1.4.2 at 127.0.0.1:4000 (http)
…
Done in 260ms.
iex(1)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it — &lt;code&gt;bin/doctor&lt;/code&gt; now ensures all the critical preconditions are met before development begins. It's simple, extendable, and safe.&lt;/p&gt;

&lt;p&gt;But… how do we make sure developers remember to run it? Let's cover that next.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Update
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4czp4696o0h96c5quy9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4czp4696o0h96c5quy9n.png" width="800" height="56"&gt;&lt;/a&gt;Now let's create a script to get the latest code — a replacement for &lt;code&gt;git pull&lt;/code&gt; that also ensures any new code is properly applied.&lt;/p&gt;

&lt;p&gt;We'll start simple: check we're on &lt;code&gt;main&lt;/code&gt; and pull latest code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;bin/update
&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.shhelpers"&lt;/span&gt;
check &lt;span class="s2"&gt;"Branch is main?"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"[ &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--abbrev-ref&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; = &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;main&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"git checkout 'main'"&lt;/span&gt;
step &lt;span class="s2"&gt;"Pulling latest code"&lt;/span&gt; &lt;span class="s2"&gt;"git pull origin 'main' --rebase"&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;bin/update
• Branch is main? ✓
• Pulling latest code ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhjjwizkxv29k2zt0smw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhjjwizkxv29k2zt0smw.gif" alt="Running bin/update, it checks branch is main and then pulls latest code, both get a green checkmark" width="584" height="88"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After pulling, we need to handle any follow-up steps — like updating dependencies if &lt;code&gt;mix.exs&lt;/code&gt; changed and ensuring the local development environment remains valid. So let's extend the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;$ git-nice-diff -U1 .
/bin/update
&lt;span class="p"&gt;@@ -8 +8,4 @@&lt;/span&gt; check "Branch is main?" \
 step "Pulling latest code" "git pull origin 'main' --rebase"
&lt;span class="gi"&gt;+step "Getting dependencies" "mix deps.get"
+step "Compiling dependencies" "mix deps.compile"
+"$(dirname "$0")/doctor"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, running &lt;code&gt;bin/update&lt;/code&gt; does more than pull code — it sets up our environment and ensures it's fully healthy, so we're ready to keep working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bin/update
• Branch is main? ✓
• Pulling latest code ✓
• Getting dependencies ✓
• Compiling dependencies ✓

Running doctor checks…
• pkgx installed? ✓
• Developer environment active? ✓
• PostgreSQL server running? ✓
• PostgreSQL server has required user? ✓
• Hex package manager installed? ✓
• Mix dependencies installed &amp;amp; compiled? ✓
• PostgreSQL database exists? ✓

✓ All checks passed, system is healthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ib28ytu4y8puujczp5f.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ib28ytu4y8puujczp5f.gif" alt="Running bin/update, resulting in latest changes being pulled down, dependencies installed and compiled, and the system checked" width="584" height="296"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where our scripts begin to interlock. &lt;code&gt;bin/update&lt;/code&gt; becomes our go-to command to get the latest code and ensure our environment is sync, fully replacing &lt;code&gt;git pull&lt;/code&gt; — a small habit shift that quickly becomes second nature.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW &lt;code&gt;bin/update&lt;/code&gt; is also where we should apply database migrations, but since we don't have those yet that step doesn't exist for now.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8m1vg49pfhu8oaqrps9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz8m1vg49pfhu8oaqrps9.png" width="800" height="56"&gt;&lt;/a&gt;Our final workflow script, &lt;code&gt;shipit&lt;/code&gt;, is the cornerstone of our Continuous Integration and Delivery (CI/CD) process. It replaces &lt;code&gt;git push&lt;/code&gt; by only pushing code after verifying it's in a shippable state by running tests and quality checks.&lt;/p&gt;

&lt;p&gt;Let's take a look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;bin/shipit
&lt;span class="c"&gt;#!/usr/bin/env bash  &lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail  
&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.shhelpers"&lt;/span&gt;  
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/update"&lt;/span&gt;  
step &lt;span class="nt"&gt;--with-output&lt;/span&gt; &lt;span class="s2"&gt;"Running tests"&lt;/span&gt; &lt;span class="s2"&gt;"mix test"&lt;/span&gt;  
check &lt;span class="s2"&gt;"Files formatted?"&lt;/span&gt; &lt;span class="s2"&gt;"mix format --check-formatted"&lt;/span&gt; &lt;span class="s2"&gt;"mix format"&lt;/span&gt;  
step &lt;span class="s2"&gt;"Pushing changes to main"&lt;/span&gt; &lt;span class="s2"&gt;"git push origin &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;main&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  
cecho &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-bB&lt;/span&gt; &lt;span class="nt"&gt;--green&lt;/span&gt; &lt;span class="s2"&gt;"✓ Shipped! 🚢💨"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;shipit&lt;/code&gt; calls &lt;code&gt;update&lt;/code&gt; first, to ensure we're testing against the latest version of &lt;code&gt;main&lt;/code&gt;. That's how we continuously integrate our changes with the rest of the team's work.&lt;/p&gt;

&lt;p&gt;When we run &lt;code&gt;shipit&lt;/code&gt;, here's what it looks like in action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bin/shipit
• Branch is main? ✓
• Pulling latest code ✓
• Getting dependencies ✓
• Compiling dependencies ✓

Running doctor checks…
• pkgx installed? ✓
• Developer environment active? ✓
• PostgreSQL server running? ✓
• PostgreSQL server has required user? ✓
• Hex package manager installed? ✓
• Mix dependencies installed &amp;amp; compiled? ✓
• PostgreSQL database exists? ✓

✓ All checks passed, system is healthy
• Running tests:
.....
Finished in 0.07 seconds (0.03s async, 0.04s sync)
5 tests, 0 failures

Randomized with seed 579539
↳ Running tests ✓
• Files formatted? ✓
• Pushing changes to main ✓

✓ Shipped! 🚢💨
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevxdw67te1k0117dmksp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevxdw67te1k0117dmksp.gif" alt="Shipit script running, showing all checks passing and ending up pushing the code" width="584" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This gives us a simple but powerful daily rhythm: run &lt;code&gt;bin/update&lt;/code&gt; when starting the day, and &lt;code&gt;bin/shipit&lt;/code&gt; whenever a commit is ready. A lightweight but robust CI/CD flow that minimizes delays and maximizes confidence.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW like the other scripts, this &lt;code&gt;shipit&lt;/code&gt; is intentionally basic,  because this article is about paving a &lt;strong&gt;direction&lt;/strong&gt; for our workflows. An initial simplicity is a good thing, as it makes it easier to build initial trust and encourage team-wide adoption and iteration.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As the project matures, &lt;code&gt;shipit&lt;/code&gt; can evolve to include additional quality gates — like linters, security checks, and even performance testing. But the most important thing for now is: &lt;strong&gt;build the habit of shipping frequently&lt;/strong&gt;. It's how we learn fast and deliver real value.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous Code Reviewing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmg5jzstlcqlbzvrozsha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmg5jzstlcqlbzvrozsha.png" width="800" height="56"&gt;&lt;/a&gt;We've established simple but powerful local workflows: &lt;code&gt;bin/update&lt;/code&gt; to &lt;strong&gt;continuously integrate&lt;/strong&gt;, and &lt;code&gt;bin/shipit&lt;/code&gt; to &lt;strong&gt;continuously push&lt;/strong&gt; — effectively replacing &lt;code&gt;git pull&lt;/code&gt; and &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But by eliminating branches, we've also removed pull requests. So, &lt;strong&gt;what about code reviews?&lt;/strong&gt; Our scripts automate local flow, but what replaces the second set of eyes?&lt;/p&gt;

&lt;p&gt;The answer: &lt;strong&gt;Code reviewing must also be continuous.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That may sound radical, but it's backed by research: Asynchronous reviews add delay — often hours or days — as code sits waiting for attention. In a fast-moving team, that latency is a dealbreaker. Instead, we need &lt;strong&gt;reviewing to happen immediately&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This shift requires a cultural change:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a commit is ready, &lt;strong&gt;review it right away&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Don't move on — &lt;strong&gt;wait for it to ship&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Remember: &lt;strong&gt;Code only delivers value in production.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;How?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call a colleague over and review it together.&lt;/li&gt;
&lt;li&gt;Or better yet, &lt;strong&gt;write the code together from the start&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is a fast, safe, collaborative flow to production — and frequent, small commits are key. When your team ships dozens of changes per hour, &lt;strong&gt;that's true continuous integration and delivery&lt;/strong&gt; 🤩&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW there are ways of working like this that are as old as programming itself. Pair programming and team programming (or "mobbing") are examples that naturally support continuous reviewing. While poor pairing can be draining, great pairing is joyful and productive 😊.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Further reading:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://martinfowler.com/articles/on-pair-programming.html" rel="noopener noreferrer"&gt;&lt;em&gt;Pair Programming by Martin Fowler&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=aItVJprLYkg" rel="noopener noreferrer"&gt;&lt;em&gt;Dave Farley on Continuous Delivery&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=SHOVVnRB4h0" rel="noopener noreferrer"&gt;&lt;em&gt;Woody Zuill: Mob Programming&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.geepawhill.org/2021/09/29/many-more-much-smaller-steps-first-sketch/" rel="noopener noreferrer"&gt;&lt;em&gt;GeePaw Hill: Many More Much Smaller Steps&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fioqfasxbgrieypjffnyc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fioqfasxbgrieypjffnyc.png" width="800" height="56"&gt;&lt;/a&gt;We began by outlining the principles behind fast, low-friction workflows, and landed on something deceptively simple: a few composable shell scripts that help us pull and push changes quickly, safely, and together.&lt;/p&gt;

&lt;p&gt;By stripping away latency — like branches and pull requests — we've created a workflow optimized for high-trust teams that value speed, clarity, and continuous improvement. The simplicity of the scripts is not a limitation; it's an invitation to the team to evolve them as the team grows.&lt;/p&gt;

&lt;p&gt;And yes — it's a little ironic that in an article series about &lt;em&gt;Perfect Elixir&lt;/em&gt;, we've barely touched Elixir. But that's the point: perfect Elixir isn't just about Elixir. It's about designing the environment that lets great Elixir happen. The workflows we've explored today are broadly applicable, grounded in research, and shaped for the realities of modern, high-velocity product teams.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>tutorial</category>
      <category>development</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Perfect Elixir: Building a Basic Elixir Web App</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Sun, 14 Apr 2024 22:00:00 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/perfect-elixir-foundations-of-a-web-app-95l</link>
      <guid>https://forem.com/jonlauridsen/perfect-elixir-foundations-of-a-web-app-95l</guid>
      <description>&lt;p&gt;Today we'll change gears a bit: It's time to make a web app. We'll need a project to work on for later articles in order to explore how to work with Elixir code, and so for this article we'll dive into some of the different ways to build a web app in Elixir. Our goal will be a simple example of a web app, and ideally it should make a data database call to be representative of real-world projects. Let's see what solutions we can find.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nothing At All&lt;/li&gt;
&lt;li&gt;Cowboy&lt;/li&gt;
&lt;li&gt;Plugged Cowboy&lt;/li&gt;
&lt;li&gt;
Bandit

&lt;ul&gt;
&lt;li&gt;Connecting To Database&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Phoenix Framework

&lt;ul&gt;
&lt;li&gt;What Phoenix Gives Us&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing At All
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fan8dbfhkmveg75uwzdku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fan8dbfhkmveg75uwzdku.png" width="800" height="56"&gt;&lt;/a&gt; What if we don't choose a solution? It's technically possible to serve HTML directly using the &lt;a href="https://www.erlang.org/doc/man/gen_tcp.html" rel="noopener noreferrer"&gt;Erlang TCP/IP socket module&lt;/a&gt;, and although this isn't a viable solution it might be illuminating to see what it takes to work without abstractions.&lt;/p&gt;

&lt;p&gt;The basic idea is to first listen on a TCP socket and wait for traffic, then send data when a request arrives. Here's a script that does that, full of comments to explain the details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;SimpleServer&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Listen on a TCP socket on the specified port&lt;/span&gt;
    &lt;span class="c1"&gt;#   :binary       - Treat data as raw binary, instead of&lt;/span&gt;
    &lt;span class="c1"&gt;#                   being automatically converted into&lt;/span&gt;
    &lt;span class="c1"&gt;#                   Elixir strings (which are UTF-8 encoded).&lt;/span&gt;
    &lt;span class="c1"&gt;#                   It'd be unnecessary to convert, as the&lt;/span&gt;
    &lt;span class="c1"&gt;#                   HTTP protocol uses raw bytes.&lt;/span&gt;
    &lt;span class="c1"&gt;#   packet: :line - Frame messages using newline delimiters,&lt;/span&gt;
    &lt;span class="c1"&gt;#                   which is the expected shape of HTTP-data&lt;/span&gt;
    &lt;span class="c1"&gt;#   active: false - Require manual fetching of messages. In&lt;/span&gt;
    &lt;span class="c1"&gt;#                   Erlang, active mode controls the&lt;/span&gt;
    &lt;span class="c1"&gt;#                   automatic sending of messages to the&lt;/span&gt;
    &lt;span class="c1"&gt;#                   socket's controlling process. We disable&lt;/span&gt;
    &lt;span class="c1"&gt;#                   this behavior, so our server can control&lt;/span&gt;
    &lt;span class="c1"&gt;#                   when and how it reads data&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:gen_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="ss"&gt;:binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;packet:&lt;/span&gt; &lt;span class="ss"&gt;:line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;active:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Listening on port &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;loop_handle_client_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;loop_handle_client_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Wait for a new client connection. This is a blocking call&lt;/span&gt;
    &lt;span class="c1"&gt;# that waits until a new connection arrives.&lt;/span&gt;
    &lt;span class="c1"&gt;# A connection returns a `client_socket` which is connected&lt;/span&gt;
    &lt;span class="c1"&gt;# to the client, so we can send a reply back.&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:gen_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;send_hello_world_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="ss"&gt;:gen_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Recursively wait for the next client connection&lt;/span&gt;
    &lt;span class="n"&gt;loop_handle_client_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;defp&lt;/span&gt; &lt;span class="n"&gt;send_hello_world_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Simple HTML content for the response.&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Hello, World!&amp;lt;/h1&amp;gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Generate the entire raw HTTP response, which includes &lt;/span&gt;
    &lt;span class="c1"&gt;# calculating content-length header.&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sd"&gt;"""
    HTTP/1.1 200 OK
    content-length: #{byte_size(content)}
    content-type: text/html

    #{content}
    """&lt;/span&gt;
    &lt;span class="ss"&gt;:gen_tcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;SimpleServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now start the script in one terminal and probe it with &lt;code&gt;curl&lt;/code&gt; in another to see HTML being returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ elixir simple_server.exs    | $ curl http://localhost:8080
Listening on port 8080        | &amp;lt;h1&amp;gt;Hello, World!&amp;lt;/h1&amp;gt;%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26jsmrkeejnvraygll2p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26jsmrkeejnvraygll2p.png" alt="A very basic Hello world" width="686" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This works, if by "works" we mean it &lt;strong&gt;technically&lt;/strong&gt; returns HTML.&lt;/p&gt;

&lt;p&gt;Is this actually useful? No, not in any practical sense, but it gives a small hint at what happens underneath all the abstractions that the libraries we'll explore today provides.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW the &lt;a href="https://github.com/gaggle/perfect-elixir/tree/perfect-elixir-3-foundations-of-a-web-app/nothing_at_all" rel="noopener noreferrer"&gt;full code to this section is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cowboy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu64tqxji10d2srce1co4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu64tqxji10d2srce1co4.png" width="800" height="56"&gt;&lt;/a&gt; &lt;a href="https://hex.pm/packages/cowboy" rel="noopener noreferrer"&gt;Cowboy&lt;/a&gt; is a minimalist and popular HTTP server, written in Erlang and used in many Elixir projects.&lt;/p&gt;

&lt;p&gt;To try Cowboy, we scaffold an Elixir project with &lt;code&gt;mix&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix new cowboy_example
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/cowboy_example.ex
* creating test
* creating test/test_helper.exs
* creating test/cowboy_example_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd cowboy_example
    mix test

Run "mix help" for more commands.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we add Cowboy as a dependency and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cd cowboy_example

$ git-nice-diff -U1
cowboy_example/mix.exs
L#23:
     [
+      {:cowboy, "~&amp;gt; 2.11"}
       # {:dep_from_hexpm, "~&amp;gt; 0.3.0"},

$ mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.043s
New:
  cowboy 2.12.0
  cowlib 2.13.0
  ranch 1.8.0
* Getting cowboy (Hex package)
* Getting cowlib (Hex package)
* Getting ranch (Hex package)
You have added/upgraded packages you could sponsor, run 
`mix hex.sponsor` to learn more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW &lt;code&gt;git-nice-diff&lt;/code&gt; is just a small script that works like &lt;code&gt;git diff&lt;/code&gt; but simplifies the output to make it easier to show diffs in this article. &lt;a href="https://github.com/gaggle/perfect-elixir/blob/perfect-elixir-3-foundations-of-a-web-app/git_diff.exs" rel="noopener noreferrer"&gt;You can find it here&lt;/a&gt; if you're curious.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then we use Cowboy in a module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;CowboyExample&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;start_server&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Set up the routing table for the Cowboy server, so root &lt;/span&gt;
    &lt;span class="c1"&gt;#requests ("/") direct to our handler.&lt;/span&gt;
    &lt;span class="n"&gt;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:cowboy_router&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;&lt;span class="ss"&gt;:_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CowboyExample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;HelloWorldHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]}&lt;/span&gt;
    &lt;span class="p"&gt;]}])&lt;/span&gt;

    &lt;span class="c1"&gt;# Start the Cowboy server in "clear mode" aka plain HTTP&lt;/span&gt;
    &lt;span class="c1"&gt;#   options - Configuration options for the server itself&lt;/span&gt;
    &lt;span class="c1"&gt;#             (this also supports which IP to bind to,&lt;/span&gt;
    &lt;span class="c1"&gt;#             SSL details, etc.)&lt;/span&gt;
    &lt;span class="c1"&gt;#   `env`   - Configuration map for how the server&lt;/span&gt;
    &lt;span class="c1"&gt;#             handles HTTP requests&lt;/span&gt;
    &lt;span class="c1"&gt;#             (this also allows configuring timeouts,&lt;/span&gt;
    &lt;span class="c1"&gt;#             compression settings, etc.)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="ss"&gt;:cowboy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;:my_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;env:&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;dispatch:&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cowboy server started on port 8080"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;CowboyExample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;HelloWorldHandler&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# `init/2` is the entry point for handling a new HTTP request&lt;/span&gt;
  &lt;span class="c1"&gt;# in Cowboy&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:cowboy_req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;
      &lt;span class="s2"&gt;"content-type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"text/html"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Hello World!&amp;lt;/h1&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Return `{:ok, req, state}` where `state` is&lt;/span&gt;
    &lt;span class="c1"&gt;# handler-specific state data; here, it's `:nostate`&lt;/span&gt;
    &lt;span class="c1"&gt;# as we do not maintain any state between requests.&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:nostate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we can get an HTML response from the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ iex -S mix                  | $ curl http://localhost:8080
Generated cowboy_example app. | &amp;lt;h1&amp;gt;Hello World!&amp;lt;/h1&amp;gt;%
Erlang/OTP 26 [erts-14.2.2] [ |
source] [64-bit] [smp:12:12]  |
[ds:12:12:10] [async-threads: |
1] [dtrace]                   |
                              |
Interactive Elixir (1.16.1) - |
 press Ctrl+C to exit (type h |
() ENTER for help)            |
iex(1)&amp;gt; CowboyExample.start_s |
erver                         |
Cowboy server started on port |
 8080                         |
:ok                           |
iex(2)&amp;gt;                       |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have moved up an abstraction level compared to before, with Cowboy now handling the sockets. The code to make Cowboy work is perhaps a bit low-level in how we end up writing raw HTML responses code, and how we have to learn some Erlang-specific syntax and patterns. But it works, which is certainly pleasing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW the &lt;a href="https://github.com/gaggle/perfect-elixir/tree/perfect-elixir-3-foundations-of-a-web-app/cowboy_example" rel="noopener noreferrer"&gt;full code to this section is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugged Cowboy
&lt;/h2&gt;

&lt;p&gt;Actually, Cowboy is most commonly used with &lt;a href="https://hexdocs.pm/plug/readme.html" rel="noopener noreferrer"&gt;Plug&lt;/a&gt;, which is an Elixir library to make it easy to write HTML-responding functions. In fact, the two are put together so often there's a library just for that purpose: &lt;a href="https://hexdocs.pm/plug_cowboy" rel="noopener noreferrer"&gt;plug_cowboy&lt;/a&gt;, which combines both Cowboy and Plug dependencies.&lt;/p&gt;

&lt;p&gt;To try it out we generate a new project, but this time we'll also generate a supervisor (the &lt;code&gt;--sup&lt;/code&gt; flag) because Plug manages Cowboy servers in sub-processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix new --sup plugged_cowboy
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/plugged_cowboy.ex
* creating lib/plugged_cowboy/application.ex
* creating test
* creating test/test_helper.exs
* creating test/plugged_cowboy_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd plugged_cowboy
    mix test

Run "mix help" for more commands.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, add the dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cd plugged_cowboy

$ git-nice-diff -U1
/plugged_cowboy/mix.exs
L#24:
     [
+      {:plug_cowboy, "~&amp;gt; 2.0"}
       # {:dep_from_hexpm, "~&amp;gt; 0.3.0"},

$ mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.104s
New:
  cowboy 2.10.0
  cowboy_telemetry 0.4.0
  cowlib 2.12.1
  mime 2.0.5
  plug 1.15.3
  plug_cowboy 2.7.0
  plug_crypto 2.0.0
  ranch 1.8.0
  telemetry 1.2.1
* Getting plug_cowboy (Hex package)
* Getting cowboy (Hex package)
* Getting cowboy_telemetry (Hex package)
* Getting plug (Hex package)
* Getting mime (Hex package)
* Getting plug_crypto (Hex package)
* Getting telemetry (Hex package)
* Getting cowlib (Hex package)
* Getting ranch (Hex package)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write a request-handling plug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;PluggedCowboy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MyPlug&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Conn&lt;/span&gt;

  &lt;span class="c1"&gt;# Plugs must define `init/1`, but we have nothing to configure so it's just a no-op implementation&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;

  &lt;span class="c1"&gt;# `call/2` is the main function of a Plug, and is expected to process the request and generate a response&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;
    &lt;span class="c1"&gt;# Both functions below are part of `Plug.Conn`s functions, they're available because we imported it&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;put_resp_content_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"text/plain"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;send_resp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Hello, World!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure and register a &lt;code&gt;Plug.Cowboy&lt;/code&gt; child process, which will take care of the underlying Cowboy server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;nice&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;U1&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugged_cowboy&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugged_cowboy&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;
&lt;span class="no"&gt;L&lt;/span&gt;&lt;span class="c1"&gt;#10:&lt;/span&gt;
     &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;      &lt;span class="c1"&gt;# Connect  Plug.Cowboy plug handler&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Cowboy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;plug:&lt;/span&gt; &lt;span class="no"&gt;PluggedCowboy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MyPlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scheme:&lt;/span&gt; &lt;span class="ss"&gt;:http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;options:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;port:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
       &lt;span class="c1"&gt;# Starts a worker by calling: PluggedCowboy.Worker.start_link(arg)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And… that all just works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ iex -S mix                  | $ curl http://localhost:8080
Erlang/OTP 26 [erts-14.2.2] [ | Hello, World!%
source] [64-bit] [smp:12:12]  | 
[ds:12:12:10] [async-threads: |
1] [dtrace]                   |
Interactive Elixir (1.16.1) - |
 press Ctrl+C to exit (type h |
() ENTER for help)            |
iex(1)&amp;gt;                       |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plug definitely offers us several useful simplifications, by bringing our code into pure Elixir and abstracting over both the Cowboy server details and the raw HTML details in the response function.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW the &lt;a href="https://github.com/gaggle/perfect-elixir/tree/perfect-elixir-3-foundations-of-a-web-app/plugged_cowboy" rel="noopener noreferrer"&gt;full code to this section is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bandit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8vnbnhst2elnhx3l306.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8vnbnhst2elnhx3l306.png" width="800" height="56"&gt;&lt;/a&gt; &lt;a href="https://hexdocs.pm/bandit/Bandit.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Bandit&lt;/strong&gt;&lt;/a&gt; is a modern Elixir web server that integrates well with Elixir's concurrency model, offering great performance.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW there's humor in the naming: &lt;strong&gt;Bandit&lt;/strong&gt; is an alternative to &lt;strong&gt;Cowboy&lt;/strong&gt;, and internally Cowboy uses a dependency called &lt;strong&gt;Ranch&lt;/strong&gt; to orchestrate sockets, and Bandit decided to call their socket-wrangling dependency &lt;strong&gt;Thousand Island&lt;/strong&gt; 😂.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's generate a new project and add dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix new --sup bandit_example &amp;gt; /dev/null
$ cd bandit_example
$ git-nice-diff -U1
/bandit_example/mix.exs
L#24:
     [
+      {:bandit, "~&amp;gt; 1.0"},
+      {:plug, "~&amp;gt; 1.0"}
       # {:dep_from_hexpm, "~&amp;gt; 0.3.0"},

$ mix deps.get
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW the &lt;code&gt;&amp;gt; /dev/null&lt;/code&gt; just silences the &lt;code&gt;mix new&lt;/code&gt; command.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then write the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;nice&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;U1&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bandit_example&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bandit_example&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;
&lt;span class="no"&gt;L&lt;/span&gt;&lt;span class="c1"&gt;#10:&lt;/span&gt;
     &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Bandit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;plug:&lt;/span&gt; &lt;span class="no"&gt;BanditExample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MyPlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;port:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="c1"&gt;# Starts a worker by calling: BanditExample.Worker.start_link(arg)&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bandit_example&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bandit_example&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;my_plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;
&lt;span class="no"&gt;L&lt;/span&gt;&lt;span class="c1"&gt;#1:&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;BanditExample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MyPlug&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Conn&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;    &lt;span class="n"&gt;conn&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;put_resp_content_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"text/plain"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;send_resp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Hello, World!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that just works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ iex -S mix                  | $ curl http://localhost:8080    
                              | Hello, World!%
Erlang/OTP 26 [erts-14.2.2] [ |
source] [64-bit] [smp:12:12]  |
[ds:12:12:10] [async-threads: |
1] [dtrace]                   |
                              |
Interactive Elixir (1.16.1) - |
 press Ctrl+C to exit (type h |
() ENTER for help)            |
iex(1)&amp;gt;                       |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turned out very similar to Plugged Cowboy, and that's no accident: Bandit is designed as a drop-in replacement to Cowboy. So, since this wasn't a challenge, how about we move ahead and connect to our database?&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting To Database
&lt;/h3&gt;

&lt;p&gt;Add the Postgres adapter &lt;a href="https://hexdocs.pm/postgrex/Postgrex.html" rel="noopener noreferrer"&gt;&lt;code&gt;Postgrex&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git-nice-diff -U1
bandit_example/mix.exs
L#25:
       {:bandit, "~&amp;gt; 1.0"},
-      {:plug, "~&amp;gt; 1.0"}
+      {:plug, "~&amp;gt; 1.0"},
+      {:postgrex, "&amp;gt;= 0.0.0"}
       # {:dep_from_hexpm, "~&amp;gt; 0.3.0"},

$ mix deps.get &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mkdir -p priv/db 
$ initdb -D priv/db
$ pg_ctl -D priv/db -l logfile start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need a user and a database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ createuser -d bandit 
$ createdb -O bandit bandit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And register the Postgrex process in our application's supervisor tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;nice&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;U1&lt;/span&gt;
&lt;span class="n"&gt;bandit_example&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;
&lt;span class="no"&gt;L&lt;/span&gt;&lt;span class="c1"&gt;#10:&lt;/span&gt;
     &lt;span class="n"&gt;children&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Bandit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;plug:&lt;/span&gt; &lt;span class="no"&gt;BanditExample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MyPlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;port:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Bandit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;plug:&lt;/span&gt; &lt;span class="no"&gt;BanditExample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;MyPlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;port:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;Postgrex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;        &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;          &lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="ss"&gt;:bandit_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;          &lt;span class="ss"&gt;hostname:&lt;/span&gt; &lt;span class="s2"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;          &lt;span class="ss"&gt;username:&lt;/span&gt; &lt;span class="s2"&gt;"bandit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;          &lt;span class="ss"&gt;password:&lt;/span&gt; &lt;span class="s2"&gt;"bandit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;          &lt;span class="ss"&gt;database:&lt;/span&gt; &lt;span class="s2"&gt;"bandit"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;        &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;      &lt;span class="p"&gt;}&lt;/span&gt;
     &lt;span class="c1"&gt;# Starts a worker by calling: BanditExample.Worker.start_link(arg)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can add a database query to our HTML response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;nice&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;U1&lt;/span&gt;
&lt;span class="n"&gt;bandit_example&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;my_plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;
&lt;span class="no"&gt;L&lt;/span&gt;&lt;span class="c1"&gt;#6:&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;    &lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="no"&gt;Postgrex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;rows:&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;      &lt;span class="no"&gt;Postgrex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:bandit_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"SELECT NOW() as current_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;
     &lt;span class="n"&gt;conn&lt;/span&gt;
     &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;put_resp_content_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"text/plain"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt;    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;send_resp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Hello, World!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;send_resp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Hello, World! It's &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;current_time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response we get reflects the database query the server performs when generating the response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20:04:22.598 [info] Running B | $ curl http://localhost:8080
anditExample.MyPlug with Band | Hello, World! It's 2024-03-17
it 1.3.0 at 0.0.0.0:8080 (htt |  19:04:24.547992Z%
p)                            | 
Erlang/OTP 26 [erts-14.2.2] [ |
source] [64-bit] [smp:12:12]  |
[ds:12:12:10] [async-threads: |
1] [dtrace]                   |
                              |
Interactive Elixir (1.16.1) - |
 press Ctrl+C to exit (type h |
() ENTER for help)            |
iex(1)&amp;gt;                       |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've climbed up from brutal raw socket handling to using powerful high-level web- and SQL-abstractions, so we can once again claim everything works 🎉&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW the &lt;a href="https://github.com/gaggle/perfect-elixir/tree/perfect-elixir-3-foundations-of-a-web-app/bandit_example" rel="noopener noreferrer"&gt;full code to this section is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Phoenix Framework
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywcj5xohz4z5e4hl8rew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywcj5xohz4z5e4hl8rew.png" width="800" height="56"&gt;&lt;/a&gt; We can't skip &lt;a href="https://www.phoenixframework.org" rel="noopener noreferrer"&gt;&lt;strong&gt;Phoenix&lt;/strong&gt;&lt;/a&gt;, the dominant framework for Elixir web development. Phoenix is a powerful, flexible, and highly ergonomic framework for writing very scalable web applications.&lt;/p&gt;

&lt;p&gt;Generate and bootstrap a Phoenix sample app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix local.hex --force &amp;amp;&amp;amp; mix archive.install hex phx_new --force
$ mix phx.new my_app  # answer yes when prompted
$ cd my_app
$ mkdir -p priv/db &amp;amp;&amp;amp; initdb -D priv/db &amp;amp;&amp;amp; pg_ctl -D priv/db -l logfile start &amp;amp;&amp;amp; createuser -d postgres
$ mix deps.get &amp;amp;&amp;amp; mix ecto.create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW if you get database errors, you might need to reset Postgres:&lt;/em&gt;&lt;/p&gt;


&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;lsof &lt;span class="nt"&gt;-ti&lt;/span&gt; :5432 | xargs &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; priv/db &lt;span class="c"&gt;# Kill all processes on Postgres' default port&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; priv/db &lt;span class="c"&gt;# Delete the local DB data folder&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;Now start the app with &lt;code&gt;iex -S mix phx.server&lt;/code&gt; and visit &lt;code&gt;http://localhost:4000&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbt9uwml80k619n8663w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbt9uwml80k619n8663w.png" alt="Screenshot of Phoenix Framework's default page" width="686" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What Phoenix Gives Us
&lt;/h3&gt;

&lt;p&gt;The Phoenix generator connects to Postgres using &lt;a href="https://hexdocs.pm/ecto/getting-started.html" rel="noopener noreferrer"&gt;&lt;code&gt;Ecto&lt;/code&gt;&lt;/a&gt; which is a library designed to efficiently create database queries.&lt;/p&gt;

&lt;p&gt;And Ecto actually uses Postgrex under the hood; if we read &lt;code&gt;config/dev.exs&lt;/code&gt; we see the same pattern of hardcoded database credentials that we saw in the Bandit section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat config/dev.exs
…
# Configure your database
config :my_app, MyApp.Repo,
  username: "postgres",
  password: "postgres",
  hostname: "localhost",
  database: "my_app_dev",
…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's also make our Phoenix example use the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git-nice-diff -U1
my_app/lib/my_app/query.ex
L#1:
+defmodule MyApp.Query do
+  import Ecto.Query
+
+  alias MyApp.Repo
+
+  def get_db_time do
+    # The SELECT 1 is a dummy table to perform a query without a table
+    query = from u in fragment("SELECT 1"), select: fragment("NOW()")
+    query |&amp;gt; Repo.all() |&amp;gt; List.first()
+  end
+end
my_app/lib/my_app_web/controllers/page_controller.ex
L#6:
     # so skip the default app layout.
-    render(conn, :home, layout: false)
+    db_time = MyApp.Query.get_db_time()
+    render(conn, :home, layout: false, db_time: db_time)
   end
my_app/lib/my_app_web/controllers/page_html/home.html.heex
L#42:
   &amp;lt;div class="mx-auto max-w-xl lg:mx-0"&amp;gt;
+    &amp;lt;h1 class="text-lg"&amp;gt;Database Time: &amp;lt;%= @db_time %&amp;gt;&amp;lt;/h1&amp;gt;
     &amp;lt;svg viewBox="0 0 71 48" class="h-12" aria-hidden="true"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now display the database query result on the page:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywxfnzlbqwk7ea20t3a9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fywxfnzlbqwk7ea20t3a9.png" alt="Screenshot of Phoenix Framework's default page with database query result" width="686" height="435"&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW the &lt;a href="https://github.com/gaggle/perfect-elixir/tree/perfect-elixir-3-foundations-of-a-web-app/my_app" rel="noopener noreferrer"&gt;full code to this section is here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But there is much more to Phoenix than just getting it working: Phoenix comes with a lot of powerful tooling to create advanced live-updating server-side rendered pages that match or exceed the experience of fully client-side rendered web apps. But it doesn't seem effective to dive into those things here as Phoenix has &lt;a href="https://hexdocs.pm/phoenix/up_and_running.html" rel="noopener noreferrer"&gt;many&lt;/a&gt; &lt;a href="https://hexdocs.pm/phoenix_live_view/welcome.html" rel="noopener noreferrer"&gt;amazing&lt;/a&gt; &lt;a href="https://hexdocs.pm/phoenix/presence.html" rel="noopener noreferrer"&gt;guides&lt;/a&gt; that introduce its capabilities better than what this article can.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbxlwxbstwtl9rzck3oa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbxlwxbstwtl9rzck3oa.png" width="800" height="56"&gt;&lt;/a&gt; It's been a lot of fun trying out various solutions, but there's no escaping &lt;strong&gt;Phoenix&lt;/strong&gt; ends up as the most realistic choice for our needs: It's a simple and supremely scalable framework with impressive industry-leading features, and it comes with really great documentation and lots of articles and forum posts to draw inspiration from.&lt;/p&gt;

&lt;p&gt;That's not to discount the alternatives, e.g. Bandit: For simple or very specialized needs there wouldn't be anything wrong with choosing Bandit, as it does a great job at providing the essential web server. That is, after all, why Phoenix &lt;strong&gt;uses&lt;/strong&gt; Bandit, and it is delightful how these libraries manages to interact and support each other via Elixir's mature and welcoming community.&lt;/p&gt;

&lt;p&gt;Phoenix is the obvious fit for this article series , and I will continue to use it in future articles.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>tutorial</category>
      <category>development</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Perfect Elixir: Setting Up an Elixir Dev Environment</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Mon, 18 Mar 2024 13:05:25 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/perfect-elixir-environment-setup-1145</link>
      <guid>https://forem.com/jonlauridsen/perfect-elixir-environment-setup-1145</guid>
      <description>&lt;p&gt;We need &lt;strong&gt;Erlang&lt;/strong&gt; and &lt;strong&gt;Elixir&lt;/strong&gt; installed, which might sound simple, but there are trade-offs to consider for a shared team environment. We'll also add a &lt;strong&gt;PostgreSQL&lt;/strong&gt; database to keep our explorations relevant to real-world scenarios.&lt;/p&gt;

&lt;p&gt;Let's explore different approaches and discuss their pros and cons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Just… Install the Dependencies?&lt;/li&gt;
&lt;li&gt;asdf&lt;/li&gt;
&lt;li&gt;mise&lt;/li&gt;
&lt;li&gt;Nix&lt;/li&gt;
&lt;li&gt;pkgx&lt;/li&gt;
&lt;li&gt;In Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Just… Install the Dependencies?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwckqpbq3ipaoyjh5h2ar.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwckqpbq3ipaoyjh5h2ar.png" width="800" height="56"&gt;&lt;/a&gt;Why not just install the dependencies as suggested by each tool's website?&lt;/p&gt;

&lt;p&gt;I’m on macOS and &lt;a href="https://erlang.org" rel="noopener noreferrer"&gt;erlang.org&lt;/a&gt;, &lt;a href="https://elixir-lang.org" rel="noopener noreferrer"&gt;elixir-lang.org&lt;/a&gt;, and &lt;a href="https://postgresql.org" rel="noopener noreferrer"&gt;postgresql.org&lt;/a&gt; all recommend installing via &lt;a href="https://brew.sh" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, first we install Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;/bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flb19xkyhr9zp54f6w75v.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flb19xkyhr9zp54f6w75v.gif" alt="Install Homebrew" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then install our tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;erlang elixir postgresql@15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fflmb3rzn57ofl1w74bqc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fflmb3rzn57ofl1w74bqc.gif" alt="Brew install erlang, elixir, and postgresql" width="480" height="286"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And… we’re done!?&lt;/p&gt;

&lt;p&gt;But there are critical issues with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No versioning&lt;/strong&gt; - There's no control over what versions we just installed, because Homebrew is not designed for versioning. It leads to a totally unpredictable mix of versions as different developers will install their tools at different times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Globally installed&lt;/strong&gt; - Homebrew tools are globally installed, meaning they affect all other projects. That leads to unpredictable behavior as one project requires an upgrade that ruins another project, and Homebrew doesn't offer a way to switch between versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So no, “just installing” isn’t viable at all, we must find a solution that installs exactly the right versions just for our project, across all developer machines and environments. Let's go explore some tools that are designed to do that.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  asdf
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9ynjn9gz71i3er12st3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9ynjn9gz71i3er12st3.png" width="800" height="56"&gt;&lt;/a&gt;&lt;a href="https://asdf-vm.com" rel="noopener noreferrer"&gt;asdf is a version manager&lt;/a&gt; with plugins for &lt;a href="https://github.com/asdf-vm/asdf-erlang" rel="noopener noreferrer"&gt;Erlang&lt;/a&gt;, &lt;a href="https://github.com/asdf-vm/asdf-elixir" rel="noopener noreferrer"&gt;Elixir&lt;/a&gt;, and &lt;a href="https://github.com/smashedtoatoms/asdf-postgres" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt;. The installation guide suggests first installing some system dependencies via Homebrew and then &lt;strong&gt;cloning&lt;/strong&gt; the asdf repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;coreutils curl git
…
&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/asdf-vm/asdf.git ~/.asdf &lt;span class="nt"&gt;--branch&lt;/span&gt; v0.13.1
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'. "$HOME/.asdf/asdf.sh"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frisa5e72umf5msntpcfg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frisa5e72umf5msntpcfg.gif" alt="Brew install coreutils, curl, and git" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;

&lt;p&gt;ℹ️ &lt;em&gt;BTW it's quite odd to install asdf via &lt;code&gt;git clone&lt;/code&gt;. Although it &lt;strong&gt;can&lt;/strong&gt; be installed via Homebrew the asdf guide recommends using Git so &lt;code&gt;git clone&lt;/code&gt; it is…&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, add plugins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
&lt;span class="nv"&gt;$ &lt;/span&gt;asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git
&lt;span class="nv"&gt;$ &lt;/span&gt;asdf plugin add postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv7svqp05bn3rocshccdl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv7svqp05bn3rocshccdl.gif" alt="Add plugins to asdf" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then we need to go through each plugin's GitHub repository's documentation to derive a list of additional dependencies that are needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;autoconf openssl@1.1 openssl libxslt fop gcc readline zlib curl ossp-uuid
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export KERL_CONFIGURE_OPTIONS="--without-javac --with-ssl=$(brew --prefix openssl@1.1)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6j8l02c8aukzntmq5lcg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6j8l02c8aukzntmq5lcg.gif" alt="Add plugin dependencies" width="80" height="47"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ℹ️ &lt;em&gt;BTW it's really unnerving how each plugin requires their own set of Homebrew-installed system dependencies, because that seems to throw all the versioning right out the window! But let's keep going…&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then, create a &lt;code&gt;.tool-versions&lt;/code&gt; file to specify the tools and versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; &amp;gt;&amp;gt; .tool-versions
erlang 26.2.1
elixir 1.16.0
postgres 15.5
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then the last command is to install the specified tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;asdf &lt;span class="nb"&gt;install&lt;/span&gt;
…
&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
/Users/cloud/.asdf/shims/erl
&lt;span class="nv"&gt;$ &lt;/span&gt;which elixir
/Users/cloud/.asdf/shims/elixir
&lt;span class="nv"&gt;$ &lt;/span&gt;which psql
/Users/cloud/.asdf/shims/psql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy03wy5vjydiflf0147f9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy03wy5vjydiflf0147f9.gif" alt="Run asdf install" width="480" height="286"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We now have all our tools installed 🎉&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;direnv&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;But just having the tools installed isn't quite enough: Developers will have to manually run &lt;code&gt;asdf install&lt;/code&gt; to stay in sync with the specified versions, it's not taken care of automatically. Can we automate this part?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://direnv.net" rel="noopener noreferrer"&gt;direnv&lt;/a&gt; is a common tool for keeping developer environments in sync, because it can trigger commands upon entering a folder. So let's install and configure direnv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;asdf plugin add direnv
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;direnv 2.30.0 &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .tool-versions 
&lt;span class="nv"&gt;$ &lt;/span&gt;asdf &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;asdf direnv setup &lt;span class="nt"&gt;--shell&lt;/span&gt; zsh &lt;span class="nt"&gt;--version&lt;/span&gt; 2.30.0
&lt;span class="nv"&gt;$ &lt;/span&gt;asdf direnv &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"use asdf"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .envrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjntubso4wem25kyrd5n1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjntubso4wem25kyrd5n1.gif" alt="Add asdf direnv" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, if we simulate a change to &lt;code&gt;.tools-versions&lt;/code&gt; by updating the Erlang version, we'll see direnv automatically prompts to re-install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;perfect-elixir/
direnv: loading ~/perfect-elixir/.envrc
direnv: using asdf
direnv: Creating &lt;span class="nb"&gt;env &lt;/span&gt;file /Users/cloud/.cache/asdf-direnv/env/1510633598-1931737049-390094659-716574907
direnv: erlang 26.2.2 not installed. Run &lt;span class="s1"&gt;'asdf direnv install'&lt;/span&gt; to install.
direnv: referenced  does not exist
&lt;span class="nv"&gt;$ &lt;/span&gt;asdf direnv &lt;span class="nb"&gt;install
&lt;/span&gt;Downloading 26.2.2 to /Users/cloud/.asdf/downloads/erlang/26.2.2...
...
&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
/Users/cloud/.asdf/installs/erlang/26.2.2/bin/erl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp6mrtnhxq2wyluy5n2m7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp6mrtnhxq2wyluy5n2m7.gif" alt="Show how asdf direnv automatically checks for dependencies on entering the folder" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We now have an workflow backed by asdf that automatically keep our environments in sync, even as our team upgrades tool versions 🎉&lt;/p&gt;

&lt;p&gt;We can compare tools later, for now let's move on to try the next tool to see how it's different.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  mise
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuz2j1ub8mwmh63mg93ce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuz2j1ub8mwmh63mg93ce.png" width="800" height="56"&gt;&lt;/a&gt;&lt;strong&gt;&lt;a href="https://mise.jdx.dev" rel="noopener noreferrer"&gt;Mise&lt;/a&gt;&lt;/strong&gt; is a recent replacement for asdf, leveraging all the existing asdf plugins but promising to dramatically simplify the steps to get everything work. So let's check it out.&lt;/p&gt;

&lt;p&gt;Install Mise via Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;mise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate it for your shell (assuming zsh):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(mise activate zsh)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ZDOTDIR&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.zshrc"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;.mise.toml&lt;/code&gt; file to specify dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .mise.toml
&lt;span class="o"&gt;[&lt;/span&gt;tools]
erlang &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'26.2.1'&lt;/span&gt;
elixir &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1.16.0'&lt;/span&gt;
postgres &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'15.5'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;mise &lt;span class="nb"&gt;install
&lt;/span&gt;mise ⚠️ postgres is a community-developed plugin – https://github.com/smashedtoatoms/asdf-postgres
Would you like to &lt;span class="nb"&gt;install &lt;/span&gt;postgres? Yes
…
mise elixir@1.16.0 ✓ installed  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcj2nekuwxm5bpuv0zyx3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcj2nekuwxm5bpuv0zyx3.gif" alt="Running mise install command" width="584" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And just like that every tool is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
/Users/cloud/.local/share/mise/installs/erlang/26.2.1/bin/erl

&lt;span class="nv"&gt;$ &lt;/span&gt;which elixir
/Users/cloud/.local/share/mise/installs/elixir/1.16.0/bin/elixir

&lt;span class="nv"&gt;$ &lt;/span&gt;which psql
/Users/cloud/.local/share/mise/installs/postgres/15.5/bin/psql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the tools are automatically only activated inside the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
erl not found

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;perfect-elixir
&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
/Users/cloud/.local/share/mise/installs/erlang/26.2.1/bin/erl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's slick! 🎉&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feondla6o7l8p1r1sz5n5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feondla6o7l8p1r1sz5n5.png" width="800" height="56"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nix is a tool &lt;a href="https://nixos.org" rel="noopener noreferrer"&gt;"for reproducible and declarative configuration management"&lt;/a&gt;, available for macOS and Linux. Let’s give it a try!&lt;/p&gt;

&lt;p&gt;First, install Nix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;sh &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://nixos.org/nix/install&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3apwbxd05h08n9uwk5w9.gif" rel="noopener noreferrer"&gt;Installing nix.gif&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;

&lt;p&gt;ℹ️ &lt;em&gt;BTW The installer requires &lt;code&gt;sudo&lt;/code&gt;, and it creates a new “Nix Store” drive-volume and &lt;strong&gt;32 hidden new users&lt;/strong&gt;. I immediately find that really intrusive, it sure feels like a lot just to install some system tools…&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nix uses a custom pseudo programming language for specifying dependencies. I've struggled greatly to understand any available Nix guides and tutorials but I think we have to enable some experimental features and create a &lt;code&gt;flake.nix&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/nix &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"experimental-features = nix-command flakes"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/nix/nix.conf

&lt;span class="nv"&gt;$ &lt;/span&gt;nix flake new &lt;span class="nb"&gt;.&lt;/span&gt;
wrote: /Users/cloud/Documents/nix/flake.nix

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;flake.nix
&lt;span class="o"&gt;{&lt;/span&gt;
  description &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A very basic flake"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  outputs &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; self, nixpkgs &lt;span class="o"&gt;}&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    packages.x86_64-linux.hello &lt;span class="o"&gt;=&lt;/span&gt; nixpkgs.legacyPackages.x86_64-linux.hello&lt;span class="p"&gt;;&lt;/span&gt;
    packages.x86_64-linux.default &lt;span class="o"&gt;=&lt;/span&gt; self.packages.x86_64-linux.hello&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ℹ️ &lt;em&gt;BTW I don't understand why a new flake references "legacy" packages, or why they point to Linux packages when I run this on a Mac… but these are just minor confusions in the journey to get Nix working properly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We then edit &lt;code&gt;flake.nix&lt;/code&gt; to specify our (Mac) dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;flake.nix
&lt;span class="o"&gt;{&lt;/span&gt;
  description &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A flake"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  outputs &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; self, nixpkgs &lt;span class="o"&gt;}&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    devShells.x86_64-darwin &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      default &lt;span class="o"&gt;=&lt;/span&gt; nixpkgs.legacyPackages.x86_64-darwin.mkShell &lt;span class="o"&gt;{&lt;/span&gt;
        buildInputs &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
          nixpkgs.legacyPackages.x86_64-darwin.erlangR26
          nixpkgs.legacyPackages.x86_64-darwin.elixir_1_16
          nixpkgs.legacyPackages.x86_64-darwin.postgresql_15
        &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can activate the flake by running &lt;code&gt;nix develop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;nix develop
...
&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
/nix/store/49qw7cw30wszrfn3sa23qnlskyvbnbhi-erlang-26.2.2/bin/erl
&lt;span class="nv"&gt;$ &lt;/span&gt;which elixir
/nix/store/rr6immch9mp8dphv1jvgxym35za4b7jy-elixir-1.16.1/bin/elixir
&lt;span class="nv"&gt;$ &lt;/span&gt;which psql
/nix/store/v5ym92k3kss1af7n1788653vis1d6qsc-postgresql-15.5/bin/psql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmt6sntwpwwbo8wzsjj93.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmt6sntwpwwbo8wzsjj93.gif" alt="Run nix develop" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And if we exit the shell, the tools are no longer available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;macOS-14:perfect-elixir cloud&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
/nix/store/49qw7cw30wszrfn3sa23qnlskyvbnbhi-erlang-26.2.2/bin/erl
macOS-14:perfect-elixir cloud&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;exit
exit&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
erl not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5zbkhcaafayjr5f52n6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw5zbkhcaafayjr5f52n6.gif" alt="Exiting the Nix shell makes tools unavailable again" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We now have a reproducible specification of our environment!&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;direnv&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;But just as with #asdf we would like the tools to automatically be available upon entering the folder. Let's once again automate it with &lt;code&gt;direnv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, install &lt;code&gt;direnv&lt;/code&gt; via Nix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;nix-env &lt;span class="nt"&gt;-iA&lt;/span&gt; nixpkgs.direnv&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(direnv hook zsh)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe5f85jb5srtzsde5wnl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe5f85jb5srtzsde5wnl.gif" alt="Install direnv via Nix" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And activate the flake with &lt;code&gt;direnv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"use flake"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .envrc
&lt;span class="nv"&gt;$ &lt;/span&gt;direnv allow
direnv: loading ~/Documents/nix/.envrc
direnv: using flake
…
&lt;span class="nv"&gt;$ &lt;/span&gt;which erl
/nix/store/rp1c50s0w039grl22q086h0dyrygk0p2-erlang-26.2.1/bin/erl
&lt;span class="nv"&gt;$ &lt;/span&gt;which elixir
/nix/store/66f9b1d1c4fmhz6bd3fpcny6brjm0fk7-elixir-1.16.0/bin/elixir
&lt;span class="nv"&gt;$ &lt;/span&gt;which psql
/nix/store/zhk6mf2y5c07zqf519zjkm3fm2nazmvj-postgresql-15.5/bin/psql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;
  🖥️ Terminal
  &lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbn4z3j2xjrhdxtm3yoei.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbn4z3j2xjrhdxtm3yoei.gif" alt="Activate Nix Flake via Direnv" width="577" height="344"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, our Nix environment automatically activates when we enter the folder 🎉&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;BTW &lt;code&gt;direnv&lt;/code&gt; requires running &lt;code&gt;direnv allow&lt;/code&gt; whenever the &lt;code&gt;.envrc&lt;/code&gt; file changes to prevent malicious code from executing. Always review &lt;code&gt;.envrc&lt;/code&gt; before allowing.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  pkgx
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2fzig8dwbgjaee8ko6ov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2fzig8dwbgjaee8ko6ov.png" width="800" height="56"&gt;&lt;/a&gt;&lt;a href="https://pkgx.sh" rel="noopener noreferrer"&gt;&lt;code&gt;pkgx&lt;/code&gt; is a new package manager&lt;/a&gt;, and is actually actually a collection of small composable tools that can be used and composed to solve a variety of dependency-related use cases. Our needs is &lt;a href="https://github.com/pkgxdev/dev" rel="noopener noreferrer"&gt;covered by their  &lt;code&gt;dev&lt;/code&gt; tool&lt;/a&gt;, which describes itself like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;dev&lt;/code&gt; uses &lt;code&gt;pkgx&lt;/code&gt; to create “virtual environments” consisting of the specific versions of tools and their dependencies you need for your projects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's give it a go!&lt;/p&gt;

&lt;p&gt;First, we install and activate the &lt;code&gt;dev&lt;/code&gt; tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;pkgxdev/made/dev
&lt;span class="nv"&gt;$ &lt;/span&gt;dev integrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;a href="https://docs.pkgx.sh/using-dev/dev#pkgx.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;.pkgx.yml&lt;/code&gt;&lt;/a&gt; file to specify dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .pkgx.yml
dependencies:
  erlang.org: &lt;span class="o"&gt;=&lt;/span&gt;27.3.4.2  
  elixir-lang.org: &lt;span class="o"&gt;=&lt;/span&gt;1.18.4
  postgresql.org: &lt;span class="o"&gt;=&lt;/span&gt;17.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;which elixir
elixir not found

&lt;span class="nv"&gt;$ &lt;/span&gt;dev

&lt;span class="nv"&gt;$ &lt;/span&gt;which elixir
/Users/cloud/.pkgx/elixir-lang.org/v1.18.4/bin/elixir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And… err, that’s it?! The tools are automatically only available when inside the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;span class="nt"&gt;-erlang&lt;/span&gt;.org&lt;span class="o"&gt;=&lt;/span&gt;27.3.4.2 &lt;span class="nt"&gt;-elixir-lang&lt;/span&gt;.org&lt;span class="o"&gt;=&lt;/span&gt;1.18.4 &lt;span class="nt"&gt;-postgresql&lt;/span&gt;.org&lt;span class="o"&gt;=&lt;/span&gt;17.2.0
no devenv detected

&lt;span class="nv"&gt;$ &lt;/span&gt;which elixir
elixir not found

&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;perfect-elixir

&lt;span class="nv"&gt;$ &lt;/span&gt;which elixir
/Users/cloud/.pkgx/elixir-lang.org/v1.18.4/bin/elixir
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hard to get any simpler than that! And worth noting &lt;code&gt;pkgx&lt;/code&gt; also supports all manner of core system tools such as &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, etc., which is super useful.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;asdf&lt;/strong&gt; seems to be a popular choice judging from an abundance of articles mentioning it, but I found it &lt;strong&gt;quite&lt;/strong&gt; cumbersome and old-fashioned to use. I don't mean to be rude, and I'm sure asdf has helped developers for decades, but I think asdf is probably popular more for historical reasons than for how it compares to its present-day peers.&lt;/p&gt;

&lt;p&gt;I would not recommend using asdf for a new project.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Mise&lt;/strong&gt; dramatically simplifies the asdf experience, removing the major ergonomic painpoints of asdf. It's really remarkably simple to use, and it deserves praise for that. But also worth mentioning: Mise does not support system tools such as &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, etc., and having different versions of those are &lt;strong&gt;very common sources of errors&lt;/strong&gt; because e.g. MacOS' &lt;code&gt;grep&lt;/code&gt; is very different from GNU &lt;code&gt;grep&lt;/code&gt; and some projects often end up requiring one or the other.&lt;/p&gt;

&lt;p&gt;As a result Mise-based projects are highly likely to end up also maintaining a list of Homebrew dependencies that developers must install, which causes the issues of lack of versioning we saw in the Just... Install the Dependencies section.&lt;/p&gt;

&lt;p&gt;Ultimately Mise provides a nice experience, but cannot offer versioning of a wide enough set of tools.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Nix&lt;/strong&gt; is clearly powerful, but also very hard to learn. Like, &lt;strong&gt;way over the top hard&lt;/strong&gt;, complete with lacking documentation and hard to grasp jargon. It offers unmatched control over dependencies, but it also requires such a significant learning curve it stands in strong contrast to our needs of just wanting a handful of system tools installed, and it ends up being really intrusive compared to the task we're looking to solve.&lt;/p&gt;

&lt;p&gt;I'm sure Nix is a great tool for sophisticated needs such as specifying all dependencies for an entire operating system, but for installing Elixir and Bash? Nix is a huge overkill for that purpose.&lt;/p&gt;

&lt;p&gt;You should carefully consider its learning curve and reach into everyone's systems before adopting it, including how it will impact every person who will ever work on this project, including future hires.&lt;/p&gt;




&lt;p&gt;🥇 &lt;code&gt;pkgx&lt;/code&gt;'s &lt;code&gt;dev&lt;/code&gt; tool is &lt;strong&gt;impressively simple&lt;/strong&gt;, and very easy to use: Easy to install, easy to configure, and easy to use. It's crazy simple all the way: No need for &lt;code&gt;sudo&lt;/code&gt;, the installer automatically handles integrating with the shell, and it's just enormously convenient how &lt;code&gt;dev&lt;/code&gt; just works correctly right out of the box.&lt;/p&gt;

&lt;p&gt;Furthermore, &lt;a href="https://pkgx.dev" rel="noopener noreferrer"&gt;the &lt;code&gt;pkgx&lt;/code&gt; registry&lt;/a&gt; includes all manner of packages, including system-tools such as &lt;code&gt;bash&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt;. Don't underestimate the value of that, because projects tend to accumulate Bash scripts and it's a common source of errors to have Bash scripts fail in various ways because the team has not synced to the same versions.&lt;/p&gt;




&lt;p&gt;Ultimately the best tool must depend on your specific needs and preferences; only you can choose based on the requirements you face. To me &lt;strong&gt;pkgx&lt;/strong&gt; is by far the most user-friendly and comprehensive option available so it's an easy recommendation, and it is the tool I'll use going forward in this article series.&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>tutorial</category>
      <category>development</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Exploring the Perfect Elixir Setup</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Mon, 18 Mar 2024 13:00:00 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/exploring-the-perfect-elixir-setup-3dek</link>
      <guid>https://forem.com/jonlauridsen/exploring-the-perfect-elixir-setup-3dek</guid>
      <description>&lt;p&gt;In this series, we'll explore &lt;strong&gt;the most effective ways of working with Elixir projects&lt;/strong&gt;, examining the various tools, techniques, and processes that are available in Elixir. There are several topics to explore, and each of them has various trade-offs we'll need to navigate and discuss. Our ultimate goal is to identify specific answers and practical tools that unlock the most effective and simplest ways of working, such that the solutions can &lt;strong&gt;start simple and stay useful&lt;/strong&gt; as a team and their product grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Elixir: A Brief Overview&lt;/li&gt;
&lt;li&gt;Exploring Elixir?&lt;/li&gt;
&lt;li&gt;Topics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Elixir: A Brief Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fehh7gjmwil347q05l2rj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fehh7gjmwil347q05l2rj.png" width="800" height="56"&gt;&lt;/a&gt;For those new to the language, Elixir is a dynamic, functional programming language created in 2011, with syntax inspired by Ruby. Similar to how Java uses the Java Virtual Machine, Elixir runs on the &lt;em&gt;Erlang Abstract Machine&lt;/em&gt; (BEAM). The BEAM, originating in the mid-80s, is known for powering critical and highly fault-tolerant distributed systems at scale.&lt;/p&gt;

&lt;p&gt;It's a very interesting language because it manages to be simultaneously easy to pick up, has a standard-library with superpowers that are just out of this world, and its community is friendly and welcoming. But this isn't the series to deep-dive into Elixir itself, as there are great resources for that already. For example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://elixir-lang.org/" rel="noopener noreferrer"&gt;Official Elixir Website and Documentation&lt;/a&gt;: The official Elixir guide is a great starting point, with thorough documentation and great getting started guides.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://podcast.thinkingelixir.com" rel="noopener noreferrer"&gt;Thinking Elixir podcast&lt;/a&gt;: An informative and friendly podcast covering recent developments and community insights.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://elixirforum.com" rel="noopener noreferrer"&gt;The Elixir Forum&lt;/a&gt;: An excellent place to ask questions and engage with the community. It's known for being welcoming to newcomers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring Elixir?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18v402fiy7e4xvhnoyc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18v402fiy7e4xvhnoyc2.png" width="800" height="56"&gt;&lt;/a&gt;Now back to exploring. This series is an open learning journey: I am myself skilling up in Elixir, so this is genuinely a dive into the unknown to explore what's possible in Elixir projects. We are very explicitly not rushing in to write Elixir application code, rather our focus will be &lt;strong&gt;the many details and root-decisions that surrounds the application&lt;/strong&gt;. We'll focus in particular on the many decisions that are often made in a hurry when projects are just getting started, that can end up scaling horribly and cause all manner of irritations and slowdowns as a project grows.&lt;/p&gt;

&lt;p&gt; &lt;br&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Topics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/jonlauridsen/perfect-elixir-environment-setup-1145"&gt;2. Setting Up an Elixir Dev Environment&lt;/a&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;We need &lt;strong&gt;Erlang&lt;/strong&gt; and &lt;strong&gt;Elixir&lt;/strong&gt; installed, which might sound simple, but there are trade-offs to consider for a shared team environment. We'll also add a &lt;strong&gt;PostgreSQL&lt;/strong&gt; database to keep our explorations relevant to real-world scenarios.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article explores &lt;strong&gt;asdf&lt;/strong&gt;, &lt;strong&gt;mise&lt;/strong&gt;, &lt;strong&gt;Nix&lt;/strong&gt;, and &lt;strong&gt;pkgx&lt;/strong&gt;, to set up a reproducible development environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/jonlauridsen/perfect-elixir-foundations-of-a-web-app-95l"&gt;3. Building a Basic Elixir Web App&lt;/a&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Today we'll change gears a bit: It's time to make a web app. We'll need a project to work on for later articles in order to explore how to work with Elixir code, and so for this article we'll dive into some of the different ways to build a web app in Elixir. Our goal will be a simple example of a web app, and ideally it should make a data database call to be representative of real-world projects. Let's see what solutions we can find.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article explores &lt;strong&gt;Cowboy&lt;/strong&gt;, &lt;strong&gt;Plug&lt;/strong&gt;, &lt;strong&gt;Bandit&lt;/strong&gt;, and &lt;strong&gt;Phoenix&lt;/strong&gt; to set up a web app.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/jonlauridsen/perfect-elixir-development-workflows-26k6"&gt;4. Streamlining Development Workflows&lt;/a&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Today we'll define and implement our daily development workflows. We'll begin by identifying what makes a workflow truly effective — drawing from cutting-edge research on software delivery practices. Then we'll establish &lt;strong&gt;really simple&lt;/strong&gt; mechanisms for managing code changes so we can move quickly and accurately as a team. And importantly, these workflows must be flexible enough to scale as our team and product grows. Let's dive in!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article explores how to write &lt;strong&gt;shell scripts&lt;/strong&gt; to set up effective and efficient workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/jonlauridsen/perfect-elixir-onboarding-10o5"&gt;5. Simplifying Elixir Developer Onboarding&lt;/a&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Onboarding can be a costly but hidden drag on a team — not just in raw setup time, but also in how easily it becomes a process full of uncertainty and fragility. How developers get started reflects the standards of care a team chooses to uphold, and we should explore how simple and safe we can make this experience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article explores streamlining developer onboarding via &lt;strong&gt;semi-automated scripting&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/jonlauridsen/perfect-elixir-test-automation-58fd"&gt;6. Automating Tests in Elixir Projects&lt;/a&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we write &lt;strong&gt;fast, reliable&lt;/strong&gt; tests in Elixir? How do we keep modules &lt;strong&gt;decoupled and easy to test&lt;/strong&gt;? Testing is a broad topic with many opinions, and today we’ll explore techniques that will let us have informed discussions about &lt;strong&gt;how we want to test&lt;/strong&gt;. Because good tests aren't just about passing — they’re about helping us &lt;strong&gt;move fast&lt;/strong&gt;, and &lt;strong&gt;deploy with confidence&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, we'll set up &lt;strong&gt;automated tests in Elixir&lt;/strong&gt;, explore &lt;strong&gt;dependency injection&lt;/strong&gt;, and compare tools like &lt;strong&gt;Mox, Hammox, and Double&lt;/strong&gt; to keep tests clean and scalable.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://dev.to/jonlauridsen/perfect-elixir-test-automating-shell-scripts-18ii"&gt;7. Testing Shell Scripts with Bats&lt;/a&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Automated testing is &lt;strong&gt;crucial&lt;/strong&gt; for reliable software development, yet shell scripts are frequently left untested, despite managing critical tasks like deployments, database migrations, and CI pipelines. Neglecting tests here can lead to subtle bugs and fragile workflows. Today, let's make sure &lt;strong&gt;all our code is testable&lt;/strong&gt;, treating shell scripts with the same discipline we apply to application code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article explores the &lt;strong&gt;Bash Automated Testing System&lt;/strong&gt; (Bats) to properly integrate shell scripts into our codebase.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;…and more articles to come as I find time to write them. If you have suggestions for topics please leave a comment.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>elixir</category>
      <category>tutorial</category>
      <category>development</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Event-Driven Architecture: A Focused Primer</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Wed, 10 Jan 2024 18:00:00 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/event-driven-architecture-a-quick-primer-f44</link>
      <guid>https://forem.com/jonlauridsen/event-driven-architecture-a-quick-primer-f44</guid>
      <description>&lt;p&gt;Event-Driven Architecture (EDA) is a design pattern that helps systems remain resilient and scalable under complexity and high load. It works by decoupling inputs from side effects and letting operations run asynchronously. EDA naturally supports fault tolerance, parallel processing, and graceful handling of service degradations. This primer covers some essential strengths of EDA and when it can make sense to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands and Events
&lt;/h2&gt;

&lt;p&gt;EDA systems accept &lt;strong&gt;commands&lt;/strong&gt;: requests to perform an action, such as "&lt;em&gt;cancel an order&lt;/em&gt;", "&lt;em&gt;change item quantity&lt;/em&gt;" or "&lt;em&gt;play a song&lt;/em&gt;". A command represents &lt;em&gt;intent&lt;/em&gt; — it expresses what a user or system wants to happen, but does not guarantee that it will.&lt;/p&gt;

&lt;p&gt;Processing a command means validating it, applying business rules, and then deciding what &lt;strong&gt;events&lt;/strong&gt; (if any) to emit in response. &lt;strong&gt;Events&lt;/strong&gt; are the true &lt;strong&gt;core&lt;/strong&gt; of EDA — they are immutable, durable records of something that happened. Once emitted, they form the historical record of the system.&lt;/p&gt;

&lt;p&gt;Commands are not limited to any protocols, they may be ingested via HTTP, queues, or any transport, but what makes a system event-driven is that events are the durable, traceable outcome of the processing of commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoupling and Resilience
&lt;/h2&gt;

&lt;p&gt;Unlike synchronous APIs that return results immediately, EDA systems can balance which parts to handle inline and which to defer. For example, suppose a "&lt;em&gt;change item quantity&lt;/em&gt;" command includes a business rule that requires a geo-IP lookup. That lookup might be slow or unreliable. Instead of stalling the entire command, an EDA system can emit an event like &lt;code&gt;ItemQuantityChangeRequested&lt;/code&gt;, and let another subsystem handle the lookup and emit &lt;code&gt;QuantityChangeRejected&lt;/code&gt; or &lt;code&gt;QuantityChanged&lt;/code&gt; later.&lt;/p&gt;

&lt;p&gt;This separation improves throughput and robustness. Even if the downstream geo service is offline or slow, command ingestion continues. Systems like this are naturally resilient to back-pressure and failure, since only the minimal ingestion surface needs to stay online.&lt;/p&gt;

&lt;p&gt;In very high-throughput cases, command ingestion can become entirely asynchronous: just schema validation and queueing. That allows for immense scale and high availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling and Graceful Degradation
&lt;/h2&gt;

&lt;p&gt;Systems built with EDA can also handle transient failures more gracefully. If a "&lt;em&gt;cancel order&lt;/em&gt;" command is received but the underlying order system is temporarily offline, the command handler can emit a &lt;code&gt;CancelOrderRequested&lt;/code&gt; event, and retry later when the system is available.&lt;/p&gt;

&lt;p&gt;Crucially, the event provides an audit trail. Even if that order becomes ineligible for cancellation by the time the cancellation event is processed, the timestamp on the event proves the user acted in time. This enables fair outcomes — for example, issuing a refund even though the cancellation was delayed.&lt;/p&gt;

&lt;p&gt;This pattern generalizes well to many domains: decoupling validation from processing lets systems make forward progress even in degraded states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event Replay and Correctness
&lt;/h2&gt;

&lt;p&gt;Durable events also make it possible to recover from logic bugs. Suppose a page is showing only cancelled orders due to a filtering bug. If events are stored durably — e.g. in an append-only log or a multi-region database — the page's read model can be reset, the bug fixed, and the full stream of past events replayed to rebuild correct state.&lt;/p&gt;

&lt;p&gt;This pattern provides a very powerful safety net for evolving business logic without losing historical context.&lt;/p&gt;

&lt;p&gt;And replay isn't limited to debugging — it also supports migrations, rebuilding read models with new fields, or syncing newly onboarded services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observing Outcomes
&lt;/h2&gt;

&lt;p&gt;When a command is processed, the emitted events become the system's response — not just to the caller, but to any interested party. Clients may subscribe to event streams, poll read models, or observe real-time updates via web-sockets.&lt;/p&gt;

&lt;p&gt;This model enables loose coupling not only internally, but &lt;strong&gt;across systems&lt;/strong&gt;. For example, one service might emit &lt;code&gt;OrderConfirmed&lt;/code&gt;, and many others can listen — fraud detection, shipping prep, analytics — all triggered without tight integration.&lt;/p&gt;

&lt;p&gt;EDA enables systems to react and evolve independently, along well-defined and stable events. That makes the systems more adaptable and maintainable over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;EDA is a powerful architectural model for building systems that can handle scale, failure, and change. By treating &lt;strong&gt;commands as intent&lt;/strong&gt;, &lt;strong&gt;events as facts&lt;/strong&gt;, and &lt;strong&gt;work as asynchronous and composable&lt;/strong&gt;, it enables systems to be more resilient and observable.&lt;/p&gt;

&lt;p&gt;It’s not a silver bullet, it does require more moving parts, and success can depend on careful event design, durable infrastructure, and clear failure strategies. The simplicity of traditional REST-style APIs are still valid and should not be discarded, but EDA gives more leverage for building systems that evolve and survive in the real world — where slowness, downtime, and complexity are the norm.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>ElixirScript, a Github Action to run Elixir code in workflow steps</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Wed, 10 Jan 2024 15:00:00 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/elixirscript-a-github-action-to-run-elixir-code-in-workflow-steps-41ob</link>
      <guid>https://forem.com/jonlauridsen/elixirscript-a-github-action-to-run-elixir-code-in-workflow-steps-41ob</guid>
      <description>&lt;p&gt;Hi everyone,&lt;/p&gt;

&lt;p&gt;I’ve released this new GitHub Actions called &lt;a href="https://github.com/marketplace/actions/elixir-script" rel="noopener noreferrer"&gt;&lt;strong&gt;Elixir Script&lt;/strong&gt;&lt;/a&gt;, because I wanted a way to script various tasks in my GitHub Actions workflows and wished for a way to use Elixir to do it.&lt;/p&gt;

&lt;p&gt;Thus: &lt;strong&gt;ElixirScript, a Github Action to run Elixir code in workflow steps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It works by taking a script as input, which gets run, and then the script’s return is available in &lt;code&gt;outputs.result&lt;/code&gt; for later steps to use. Like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/example.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gaggle/elixir_script@v0&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;script&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;defmodule Greeter do&lt;/span&gt;
        &lt;span class="s"&gt;def greet(name), do: "Oh hi #{name}!"&lt;/span&gt;
      &lt;span class="s"&gt;end&lt;/span&gt;
      &lt;span class="s"&gt;Greeter.greet("Mark")&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get result&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "${{steps.script.outputs.result}}"&lt;/span&gt;
  &lt;span class="c1"&gt;# Echos "Oh hi Mark!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the script is run with bindings to the GitHub Actions context that caused the run, so it’s easy to react to run-specific information. For example, :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;"🚀 Pushed to #{context.payload.repository.name} by @#{context.actor}!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I think it’s a pretty easy, very low-friction way of getting to write some of the devopsy glue-code automations in Elixir. And it was a lot of fun to write as I got to learn about Elixir and GitHub Actions 🙂.&lt;/p&gt;

&lt;p&gt;It also comes with a pre-authenticated GitHub client via &lt;a href="https://github.com/edgurgel/tentacat" rel="noopener noreferrer"&gt;Tentacat&lt;/a&gt;, so it’s also easy to write scripts that interact with the GitHub API. Here I get a list of stargazers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gaggle/elixir_script@v0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;{200, stargazers, _} = Tentacat.Users.Starring.stargazers(client, "gaggle", "elixir_script")&lt;/span&gt;
    &lt;span class="s"&gt;IO.inspect(Enum.map_join(stargazers, ", ", &amp;amp; &amp;amp;1["login"]), label: "Stargazers")&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or I can can cause changes too, here I star a repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gaggle/elixir_script@v0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;{204, _, _} = Tentacat.Users.Starring.star(client, "gaggle", "elixir_script")&lt;/span&gt;
    &lt;span class="s"&gt;:ok&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy to hear any thoughts, feedback, suggestions, etc.&lt;/p&gt;

&lt;p&gt;The actual marketplace page is here: &lt;a href="https://github.com/marketplace/actions/elixir-script" rel="noopener noreferrer"&gt;Elixir Script · Actions · GitHub Marketplace · GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>elixir</category>
      <category>githubactions</category>
      <category>devops</category>
    </item>
    <item>
      <title>Effectively Handling MacOS Screenshots</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Tue, 09 Jan 2024 14:00:00 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/effectively-handling-macos-screenshots-4f60</link>
      <guid>https://forem.com/jonlauridsen/effectively-handling-macos-screenshots-4f60</guid>
      <description>&lt;p&gt;&lt;em&gt;(cover image: midjourney)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Screenshots can get messy: If you copy to clipboard then you first have to hope where you want to put it accepts pasting, otherwise you’ll need to somehow get the clipboard saved to a file. And if you want to take multiple screenshots? You’ll have to grab, paste, grab, paste, switching context a bunch of time. And if you suddenly have need for a screenshot again 10 minutes later, then, well too bad it’s gone. &lt;/p&gt;

&lt;p&gt;To simplify and streamline all that, here's a small tweak that’s easy to set up so screenshots fall into an easy and simple workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Open &lt;code&gt;screenshot.app&lt;/code&gt; (e.g. &lt;code&gt;SHIFT-CMD-5&lt;/code&gt; shortcut, or via Spotlight/Launchpad)&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq899eauq11gssydiplew.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq899eauq11gssydiplew.jpg" alt="Image description" width="512" height="68"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Options, then “&lt;strong&gt;Other Location&lt;/strong&gt;”&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwi8ur55b8xdvy8fipl56.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwi8ur55b8xdvy8fipl56.jpg" alt="Image description" width="258" height="512"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And then go to your &lt;strong&gt;Documents folder&lt;/strong&gt; and create a new folder called “&lt;strong&gt;Screenshots&lt;/strong&gt;” and select that as your screenshot location&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Then close &lt;code&gt;screenshot.app&lt;/code&gt; (hit ESC)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open Finder, go to Documents, and &lt;strong&gt;drag the new Screenshots folder to your dock&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzj2ad4qxkx6fu6ay0p4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzj2ad4qxkx6fu6ay0p4.png" alt="Image description" width="151" height="256"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Then right-click the folder in the dock and select &lt;strong&gt;Sort by Date Added&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F33xuuz1y3084x2jq9mgk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F33xuuz1y3084x2jq9mgk.png" alt="Image description" width="371" height="512"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now all new screenshots will instantly appear at the top of that folder, which is easy to click on from any app and thus easy to drag screenshots in no matter which app you're in. Now it’s easy to take several screenshots in one go and trust they’ll all end up in that folder where they can be easily accessed later. And now they also persist, so you can refer back to them later. And they’re easy to modify by just clicking on them, since that opens Preview.&lt;/p&gt;

&lt;p&gt;That's it. Just a tiny trick to reduce a small friction.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Don’t Make Conditional GitHub Actions Jobs</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Tue, 09 Jan 2024 13:00:00 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/dont-make-conditional-github-actions-jobs-3f01</link>
      <guid>https://forem.com/jonlauridsen/dont-make-conditional-github-actions-jobs-3f01</guid>
      <description>&lt;p&gt;&lt;em&gt;(cover image: midjourney)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A conditional GitHub Actions job can look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy-o-matic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there’s a better way. &lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions what?
&lt;/h2&gt;

&lt;p&gt;So, GitHub Actions are based around &lt;strong&gt;workflows&lt;/strong&gt; which are yaml files with GitHub Actions syntax. And workflows consist of &lt;strong&gt;jobs&lt;/strong&gt; which gets assigned to (virtual) GitHub Actions machines. And jobs consists of &lt;strong&gt;steps&lt;/strong&gt; which that machine runs one after another. &lt;/p&gt;

&lt;p&gt;And it is a very common pattern to see jobs that are conditional, meaning they are only run under certain conditions and are otherwise skipped. This is often done to have a single workflow that runs both in branches and on commits to the main branch, but only the main branch should deploy or create a release.&lt;/p&gt;

&lt;p&gt;That is a sensible requirement, but it does not need to be implemented as a conditional job, because that has several negative implications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All that conditional code isn't run as regularly, and so can accrue mistakes and rot until a deployment happens, and only then does everything explode.&lt;/li&gt;
&lt;li&gt;They're inherently very difficult to debug, precisely because the code only gets run on main commits. That makes them difficult to maintain, and that causes them quickly become legacy code no-one dares touch. It's a real problem!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let's not make jobs conditional.&lt;/p&gt;

&lt;p&gt;Instead, here are three alternative patterns to consider:&lt;/p&gt;

&lt;h2&gt;
  
  
  Call Simple Scripts
&lt;/h2&gt;

&lt;p&gt;This is a generally applicable tip for all pipelines, but it's quite important as much pipeline logic as possible is extracted into simple scripts that can also be run locally. I put this first because some pipelines are absolutely full of various nested logic, where complex inline scripts form a web of behavior that can be very difficult to change or even make sense of. That means no-one has confidence changing it.&lt;/p&gt;

&lt;p&gt;So step one is to ensure simplicity: Workflows should ideally just be high-level orchestrators of scripts, not themselves expressions of complex logic. Take as much complexity into scripts, because those can be tested and debugged locally, which makes them much simpler to change and iterate on compared to pipelines that can only run on CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conditionally &lt;strong&gt;dryrun&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;With a pipeline optimized for simplicity that calls out to a number of scripts, now we should identify the actual &lt;strong&gt;steps&lt;/strong&gt; that must only be run on commits to the primary branch. Which exact step is the one that does the deploying? Can that script get invoked conditionally with something like a &lt;code&gt;—dryrun&lt;/code&gt; argument? E.g. like this:&lt;br&gt;
&lt;br&gt;
 &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run deploy script&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
&lt;span class="s"&gt;       if [ "${{ github.ref }}" = "refs/heads/main" ]; then &lt;/span&gt;
        &lt;span class="s"&gt;./deploy.sh&lt;/span&gt;
&lt;span class="s"&gt;       else&lt;/span&gt;
&lt;span class="s"&gt;         ./deploy.sh --dryrun&lt;/span&gt;
&lt;span class="s"&gt;       fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This way all pipeline-code gets fully exercised even in branches, but with &lt;code&gt;—dryrun&lt;/code&gt; passed when the commit isn’t on the main branch which the script could implement as outputting useful information about what it &lt;strong&gt;would’ve&lt;/strong&gt; done but without causing any actual side-effects.&lt;/p&gt;

&lt;p&gt;This is a lot better, because now there isn’t any code that lies dormant, all the code is always exercised!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ I do find it can look a little “dangerous” to have a job named “deploy” that now shows up in the workflow overview, and then only by looking into its details can one see it actually dryruns. It can cause people who test in a branch to fear if they’re suddenly about to actually deploy 😱&lt;/p&gt;

&lt;p&gt;To remedy this, and to more clearly express that the job is actually running in dryrun mode, I like to set the job name dynamically:&lt;/p&gt;


&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.ref == 'refs/heads/main' &amp;amp;&amp;amp; 'Deploy' || &amp;gt; 'Deploy (dryrun)' }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;It’s just a small quality-of-life improvement, but it takes away anxiety because now the job will clearly show up as  &lt;code&gt;Deploy (dryrun)&lt;/code&gt; in the overview.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Conditionally run a step
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;—dryrun&lt;/code&gt; isn’t really an option, then at least the conditional check should only be applied to the specific steps that mustn’t get run rather than the whole job. Like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare deploy&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./deploy.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way at last most of the pipeline still runs all the time, even though yes it does leave the script itself vulnerable to rot. But certainly better than making the entire job conditional.&lt;/p&gt;

&lt;p&gt;And if there are any complex data-transformations that prepares any data that a conditionally run step ingests, then strongly consider extracting those transformations into a separate step so the transformations can be easily tested in branches. And then pass the transformed data directly into the step that must be conditionally run. Like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate release description&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;echo "BODY=**Full Changelog**: …" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;softprops/action-gh-release@v1&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.BODY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here I have a separate step that calculates the “release description”, which makes it easily debuggable in branches because it always runs. And then its result is passed into the subsequent conditional step.&lt;/p&gt;

&lt;h2&gt;
  
  
  In conclusion
&lt;/h2&gt;

&lt;p&gt;There are many good patterns to follow that minimizes risk of ending up with an unmaintainable and complex pipeline. If you have any to share I'd be happy to hear 'em in the comments.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>devops</category>
      <category>productivity</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Are You Listening to Your Code? Here's Why You Should Start</title>
      <dc:creator>Jon Lauridsen</dc:creator>
      <pubDate>Sat, 06 Jan 2024 13:30:54 +0000</pubDate>
      <link>https://forem.com/jonlauridsen/listen-to-your-code-2990</link>
      <guid>https://forem.com/jonlauridsen/listen-to-your-code-2990</guid>
      <description>&lt;p&gt;In the rush to deliver features and meet tight deadlines, many programmers move from one story to the next without pausing. But there's great value in injecting moments of tranquility before rushing off to do the next ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Art of Listening to Code&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Just like a book or a piece of music, code has its own rhythm and flow. By setting aside some unstructured time to &lt;strong&gt;listen&lt;/strong&gt; to it, you can uncover insights that might otherwise go unnoticed.&lt;/p&gt;

&lt;p&gt;Taking a leisurely stroll through your codebase means setting aside a responsible amount of time to sense the code at all its levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace the Logic:&lt;/strong&gt; Follow the pathways of your functions and methods. Do they lead logically from one to the next? Would a new-joiner find them understandable, and see how they all fit together cohesively?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify the Structure:&lt;/strong&gt; Are there areas where files and modules feel cluttered or are overly complex? Do multiple concerns clump together, making it difficult to maintain an overview? Imagine explaining this to someone new to the code, would it be a natural story?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imagine the Alternatives:&lt;/strong&gt; What could certain systems look like if they were reimagined? Could they be expressed more ideally? What's a small investment that would move the code towards that more ideal state?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By doing this, perhaps you'll find tests that are overly complex and it's time to clean them up. Maybe you'll notice abstractions that are leaking their implementation details. Or you might discover solutions that are difficult to maintain and would benefit from being reimagined in a way that better solves their problem. Most codebases are in need of various kinds of improvement work, and only by taking the time to do it can they be transformed from a tangled mess into a harmonious system.&lt;/p&gt;

&lt;p&gt;Personally, I find that playing some of my favorite music helps soften my focus and brings the "feel" of the code to the forefront. I'll allocate a bit of time, sometimes just half an hour, and try to dive in without external distractions to just explore and experiment with code structures.&lt;/p&gt;

&lt;p&gt;The outcomes might be a refactor, but just as valuable can be sketching out how certain components interact to surface hidden complexities, or bringing alternative designs back to the team for them to react to. It's even positive just trying out some ideas and then reverting them in the end, because you walk away with more knowledge of how the code actually functions.&lt;/p&gt;

&lt;p&gt;These acts of pruning and refining are very valuable, because it lets you focus on the architecture and robustness of the code. This doesn't just make the current code better — it lays a stronger foundation for future development, clarifying and unifying existing patterns. Clean, well-organized code is more maintainable, scalable, and a pleasure to work with.&lt;/p&gt;

&lt;p&gt;So next time you wrap up a story, resist the urge to jump straight into the next task. Instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explore:&lt;/strong&gt; Wander through the new code you've added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reflect:&lt;/strong&gt; Consider how it integrates with existing systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize:&lt;/strong&gt; Look for opportunities to enhance clarity and efficiency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By investing just a small amount of time, you can significantly improve code quality, enhance maintainability, and find greater satisfaction in your work. Try to purposefully set aside the rigid flow of tickets for a moment, and just… listen to the code, imagine what could be, and let your mind wander as you take a leisurely stroll.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>development</category>
      <category>agile</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
