<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.production-ready.de/feed/en.xml" rel="self" type="application/atom+xml" /><link href="https://www.production-ready.de/" rel="alternate" type="text/html" /><updated>2026-05-16T08:02:22+02:00</updated><id>https://www.production-ready.de/feed/en.xml</id><title type="html">Production Ready | Blog</title><subtitle>Ein persönlicher Dev-Blog</subtitle><author><name>David Ullrich</name></author><entry xml:lang="en"><title type="html">AI: Coding is not the Bottleneck</title><link href="https://www.production-ready.de/2025/12/17/coding-is-not-the-bottleneck-en.html" rel="alternate" type="text/html" title="AI: Coding is not the Bottleneck" /><published>2025-12-17T12:45:00+01:00</published><updated>2025-12-17T12:45:00+01:00</updated><id>https://www.production-ready.de/2025/12/17/coding-is-not-the-bottleneck-en</id><content type="html" xml:base="https://www.production-ready.de/2025/12/17/coding-is-not-the-bottleneck-en.html"><![CDATA[<p>Artificial intelligence experienced a massive and still ongoing hype with the release of ChatGPT in November 2022. Especially in the field of software development, it has been attributed significant potential. However, when engaging in pure <code class="language-plaintext highlighter-rouge">vibe coding</code>, the limitations quickly become apparent, and it becomes clear that sustainable solutions, particularly in enterprise environments, require more than just generated source code. Where <abbr title="Artificial Intelligence">AI</abbr> truly adds value and where it does not, especially in the day-to-day work of senior developers, is what the following experience report aims to illustrate.</p>

<!--more-->

<h2 id="ai-assisted-coding"><abbr title="Artificial Intelligence">AI</abbr> Assisted Coding</h2>

<p>This is explicitly not about <a href="https://en.wikipedia.org/wiki/Vibe_coding">vibe coding</a>. <code class="language-plaintext highlighter-rouge">AI-assisted coding</code> focuses on automating programming tasks and offloading work from the developer. The developer remains in the lead, precisely defining the expected outcome and the way it should be achieved.</p>

<h3 id="code-completion">Code Completion</h3>

<p>A simple form of <abbr title="Artificial Intelligence">AI</abbr>-assisted development is code completion. This goes far beyond the previously established variants of auto-completion limited to individual lines. Because the <abbr title="Artificial Intelligence">AI</abbr> model has access to contextual information about the codebase, it can automatically generate entire blocks of code without difficulty. For repetitive tasks such as writing mapping methods or CRUD operations, this significantly reduces the amount of manual typing required. The more generic the requirement, the better the results tend to be, as large amounts of recurring code are well represented in the training data of <abbr title="Artificial Intelligence">AI</abbr> models.</p>

<p>Implementing well-known algorithms also works very effectively due to the available training data. In many cases, it is sufficient to write a method signature and immediately receive a suitable auto-completion suggestion.</p>

<h3 id="repetitive-tasks">Repetitive tasks</h3>

<p>Tasks that recur frequently within a codebase are well suited for <abbr title="Artificial Intelligence">AI</abbr>-assisted development. For example, code for CRUD operations and database access can be generated with little effort. The initial implementation, however, should be done manually. This is where all technical details required for the specific use case are worked out. For subsequent tasks, this implementation can be provided to the <abbr title="Large Language Model">LLM</abbr> as a blueprint that it should follow as a reference.</p>

<blockquote>
  <p>Prompt: Implement a database repository “RolesRepository” for the entity “userrole”. Use the existing implementation of “UsersRepository” as a reference.</p>
</blockquote>

<p>Class and method names can also be explicitly defined in the prompt. The more constraints and guidance you provide to an <abbr title="Large Language Model">LLM</abbr>, the lower the likelihood of hallucinations.</p>

<h3 id="refactorings">Refactorings</h3>

<p>For refactorings that go beyond simple renaming, <abbr title="Artificial Intelligence">AI</abbr> can take over many tasks. With clear instructions on what should be changed and what should remain untouched, and with a well-defined scope, the desired result can be achieved very quickly. A high level of test coverage ensures that functionality is not broken after the refactoring.</p>

<h2 id="approach">Approach</h2>

<p>To successfully delegate work to an <abbr title="Artificial Intelligence">AI</abbr> agent, requirements must be clearly formulated. In addition to the functional requirements often captured, for example, in a user story, technical details also need to be described. What may previously have been implicit knowledge shared among everyone involved in a software project now has to be made explicit, written down, and made accessible to the <abbr title="Large Language Model">LLM</abbr>.</p>

<p>Developers therefore need to invest more time in writing tasks in a way that allows them to be meaningfully executed by an <abbr title="Artificial Intelligence">AI</abbr>. Here as well, it is advisable to proceed in very small, incremental steps. User stories are broken down into many small technical tasks with a very limited scope. These tasks can then be handed over to the <abbr title="Artificial Intelligence">AI</abbr> for execution. Breaking work down into small tasks also reduces the blast radius an <abbr title="Artificial Intelligence">AI</abbr> can create when it starts making changes across dozens of files. After the <abbr title="Artificial Intelligence">AI</abbr> has implemented all tasks one by one and written dedicated tests for each task, it can open a pull request. The development team must then review the <abbr title="Pull Requests">PRs</abbr> and ensure the quality of the generated code. The requirement still applies that code must not only be correct, but also maintainable. Going forward, we will continue to read more source code than we write. Perhaps to an even greater extent than today.</p>

<p>In addition to individual tasks, technical and domain-specific information about a software system can be provided to an <abbr title="Artificial Intelligence">AI</abbr> in an <a href="https://agents.md">AGENTS.md</a> file. This file is implicitly read with every prompt and taken into account when generating responses.</p>

<h2 id="limits-of-ai-coding">Limits of <abbr title="Artificial Intelligence">AI</abbr> Coding</h2>

<p>What does not work are one-shot attempts to implement entire features or even to generate complete systems. Even if such approaches produce results, they are usually far away from the desired outcome, the intended architecture, or even the coding guidelines followed by the team.</p>

<p>In some cases, using an <abbr title="Artificial Intelligence">AI</abbr> is not even a time advantage. In agentic mode, an <abbr title="Artificial Intelligence">AI</abbr> can easily spend an hour on a single task that an experienced developer with sufficient project knowledge could have implemented in just a few minutes. The time required to formulate high-quality prompts should also not be underestimated.</p>

<h3 id="coding-is-not-the-bottleneck">Coding is not the Bottleneck</h3>

<p>Writing source code itself is not the limiting factor in software development. Even without <abbr title="Artificial Intelligence">AI</abbr>, the actual coding time of senior developers typically accounts for only 30–40% of their work. For this reason, the use of artificial intelligence is not the silver bullet that some may see in it. However, it does shift developers’ work even further away from coding and more toward planning, preparation and review.</p>

<p>For experienced developers, <abbr title="Artificial Intelligence">AI</abbr> tools can be a valuable way to automate simpler and repetitive tasks. For junior developers, this makes it even more important not to rely solely on prompting and vibe coding, but instead to build the experience and expertise required to develop software effectively with the help of <abbr title="Artificial Intelligence">AI</abbr>.</p>]]></content><author><name>David</name></author><category term="ai" /><category term="ki" /><category term="coding" /><category term="development" /><category term="en" /><summary type="html"><![CDATA[Artificial intelligence experienced a massive and still ongoing hype with the release of ChatGPT in November 2022. Especially in the field of software development, it has been attributed significant potential. However, when engaging in pure vibe coding, the limitations quickly become apparent, and it becomes clear that sustainable solutions, particularly in enterprise environments, require more than just generated source code. Where AI truly adds value and where it does not, especially in the day-to-day work of senior developers, is what the following experience report aims to illustrate.]]></summary></entry><entry xml:lang="en"><title type="html">Snapshot Testing in C#</title><link href="https://www.production-ready.de/2025/12/01/snapshot-testing-in-csharp-en.html" rel="alternate" type="text/html" title="Snapshot Testing in C#" /><published>2025-12-01T18:00:00+01:00</published><updated>2025-12-01T18:00:00+01:00</updated><id>https://www.production-ready.de/2025/12/01/snapshot-testing-in-csharp-en</id><content type="html" xml:base="https://www.production-ready.de/2025/12/01/snapshot-testing-in-csharp-en.html"><![CDATA[<p><code class="language-plaintext highlighter-rouge">Testing</code> has already been a recurring topic on this blog. In addition to <a href="/2023/06/10/property-based-testing-in-csharp-en.html">property-based testing</a>, I have also covered <a href="/2024/04/27/integration-testing-with-testcontainers-en.html">integration testing in .NET with Testcontainers</a> and <a href="/2023/12/10/architecture-refactoring-with-archunitnet-en.html">architecture testing with ArchUnitNET</a>.</p>

<p>Another useful tool in the testing toolbox is <code class="language-plaintext highlighter-rouge">snapshot testing</code>. <code class="language-plaintext highlighter-rouge">Snapshot tests</code> do exactly what the name suggests: they create a <code class="language-plaintext highlighter-rouge">snapshot</code> of a test result. This makes it possible to freeze the behavior of a system and protect it against unwanted changes and regressions.</p>

<!--more-->

<blockquote>
  <p>The example code for this blog post is available on Github:</p>

  <p><a href="https://github.com/davull/demo-snapshot-testing-in-csharp">https://github.com/davull/demo-snapshot-testing-in-csharp</a></p>
</blockquote>

<p>The examples shown here use the NuGet package <a href="https://swisslife-oss.github.io/snapshooter/">Snapshooter</a>. However, the approach can easily be applied to other libraries as well.</p>

<h2 id="why-snapshot-testing">Why Snapshot Testing?</h2>

<p>Snapshot tests freeze the behavior of a system at the moment the test is executed. When a snapshot test is run for the first time, the result of the functionality under test is written to a file and then serves as a reference for future test runs. If the result deviates from the content of the reference file, the test is considered to have failed.</p>

<p>The differences can be easily inspected visually in a diff tool, allowing you to quickly decide whether the changes are expected or unexpected. Once you have reviewed the changes and consider them valid, you can accept the updated file as the new reference.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "Date": "2025-12-01",
  "TemperatureC": 18,
<span class="gd">--  "TemperatureF": 59,
</span><span class="gi">++  "TemperatureF": 64,
</span><span class="gd">--  "Summary": ""
</span><span class="gi">++  "Summary": "Mild"
</span>}
</code></pre></div></div>

<p>Another advantage is that you can easily inspect the contents of large data objects visually. If you want to verify multiple properties of a return object, you would traditionally write several <code class="language-plaintext highlighter-rouge">Assert</code> statements:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">GetForecast_Should_ReturnCorrectValues</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">tomorrow</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">DateOnly</span><span class="p">(</span><span class="m">2025</span><span class="p">,</span> <span class="m">12</span><span class="p">,</span> <span class="m">02</span><span class="p">);</span>
    <span class="k">const</span> <span class="kt">int</span> <span class="n">temperatureC</span> <span class="p">=</span> <span class="m">18</span><span class="p">;</span>
    
    <span class="kt">var</span> <span class="n">forecast</span> <span class="p">=</span> <span class="n">WeatherForecastProvider</span><span class="p">.</span><span class="nf">GetForecast</span><span class="p">(</span><span class="n">tomorrow</span> <span class="p">,</span> <span class="n">temperatureC</span><span class="p">);</span>
    
    <span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">forecast</span><span class="p">.</span><span class="n">Date</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="nf">EqualTo</span><span class="p">(</span><span class="n">tomorrow</span><span class="p">));</span>
    <span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">forecast</span><span class="p">.</span><span class="n">Summary</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="nf">EqualTo</span><span class="p">(</span><span class="s">"Mild"</span><span class="p">));</span>
    <span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">forecast</span><span class="p">.</span><span class="n">TemperatureC</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="nf">EqualTo</span><span class="p">(</span><span class="m">18</span><span class="p">));</span>
    <span class="n">Assert</span><span class="p">.</span><span class="nf">That</span><span class="p">(</span><span class="n">forecast</span><span class="p">.</span><span class="n">TemperatureF</span><span class="p">,</span> <span class="n">Is</span><span class="p">.</span><span class="nf">EqualTo</span><span class="p">(</span><span class="m">64</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In contrast, when using a snapshot test, a single call to MatchSnapshot() allows you to capture all properties of the object at once.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">GetForecast_Should_MatchSnapshot</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">tomorrow</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">DateOnly</span><span class="p">(</span><span class="m">2025</span><span class="p">,</span> <span class="m">12</span><span class="p">,</span> <span class="m">02</span><span class="p">);</span>
    <span class="k">const</span> <span class="kt">int</span> <span class="n">temperatureC</span> <span class="p">=</span> <span class="m">18</span><span class="p">;</span>
    
    <span class="kt">var</span> <span class="n">forecast</span> <span class="p">=</span> <span class="n">WeatherForecastProvider</span><span class="p">.</span><span class="nf">GetForecast</span><span class="p">(</span><span class="n">tomorrow</span> <span class="p">,</span> <span class="n">temperatureC</span><span class="p">);</span>
    <span class="n">forecast</span><span class="p">.</span><span class="nf">MatchSnapshot</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In the corresponding snapshot file <code class="language-plaintext highlighter-rouge">__snapshots__/WeatherForecastProviderTests.GetForecast_Should_MatchSnapshot.snap</code>, you’ll find the JSON representation of the object and can immediately see whether the properties contain the expected values.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2025-12-02"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"TemperatureC"</span><span class="p">:</span><span class="w"> </span><span class="mi">18</span><span class="p">,</span><span class="w">
  </span><span class="nl">"TemperatureF"</span><span class="p">:</span><span class="w"> </span><span class="mi">64</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Mild"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="snapshot-testing-on-the-backend">Snapshot Testing on the backend</h2>

<p>In the world of <a href="https://jestjs.io/docs/snapshot-testing">JavaScript frontend frameworks</a>, snapshot testing has been common for quite some time, for example to verify the HTML output of render functions.</p>

<p>But snapshot testing can also be used effectively to verify <abbr title="Server Side Rendering">SSR</abbr>-based websites or API endpoints.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">readonly</span> <span class="n">CustomWebApplicationFactory</span> <span class="n">_factory</span> <span class="p">=</span> <span class="k">new</span><span class="p">();</span>

<span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">GetWeatherForecast_Should_MatchSnapshot</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="n">_factory</span><span class="p">.</span><span class="nf">CreateClient</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"/WeatherForecast"</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">content</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>

    <span class="n">content</span><span class="p">.</span><span class="nf">MatchSnapshot</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If an API call is not a <a href="/2023/09/28/refactor-to-purity-en.html">pure function</a>, some return values may change independently of the given input. For example, the current date. If you cannot control such values in your test setup, for instance by using the <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.time.testing.faketimeprovider">FakeTimeProvider</a>, you can filter them out of the snapshot result.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">content</span><span class="p">.</span><span class="nf">MatchSnapshot</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span> <span class="n">o</span><span class="p">.</span><span class="nf">ExcludeField</span><span class="p">(</span><span class="s">"$[*].date"</span><span class="p">));</span>
</code></pre></div></div>

<h3 id="html-snapshots">HTML Snapshots</h3>

<p>HTML pages can be tested in the same way. It’s often useful to clean up the HTML output before performing the snapshot comparison so that changes of random values are ignored. This includes things like dynamic IDs, <a href="https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery">anti-forgery tokens</a>, or automatically generated CSS classes.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="kt">string</span> <span class="nf">PrepareMarkup</span><span class="p">(</span><span class="kt">string</span> <span class="n">markup</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">replacements</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="p">{</span>
          <span class="s">"""
</span>          <span class="p">&lt;</span><span class="n">input</span> <span class="n">name</span><span class="p">=</span><span class="s">"__RequestVerificationToken"</span> <span class="n">type</span><span class="p">=</span><span class="s">"hidden"</span> <span class="k">value</span><span class="p">=</span><span class="s">"[\d\w-]+"</span> <span class="err">\</span><span class="p">/&gt;</span>
          <span class="s">""",
</span>          <span class="s">"""
</span>          <span class="p">&lt;</span><span class="n">input</span> <span class="n">name</span><span class="p">=</span><span class="s">"__RequestVerificationToken"</span> <span class="n">type</span><span class="p">=</span><span class="s">"hidden"</span> <span class="k">value</span><span class="p">=</span><span class="s">"&lt;replaced&gt;"</span> <span class="p">/&gt;</span>
          <span class="s">"""
</span>        <span class="p">},</span>
        <span class="p">{</span>
          <span class="s">@"\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b"</span><span class="p">,</span>
          <span class="s">"00000000-0000-0000-0000-000000000000"</span>
        <span class="p">},</span>
        <span class="c1">// ...</span>
    <span class="p">};</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="p">(</span><span class="n">pattern</span><span class="p">,</span> <span class="n">replacement</span><span class="p">)</span> <span class="k">in</span> <span class="n">replacements</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">markup</span> <span class="p">=</span> <span class="n">Regex</span><span class="p">.</span><span class="nf">Replace</span><span class="p">(</span><span class="n">markup</span><span class="p">,</span> <span class="n">pattern</span><span class="p">,</span> <span class="n">replacement</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">markup</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>By creating snapshots of entire web pages at this level, you can reliably detect unwanted changes in the generated output and gain effective regression tests.</p>

<h2 id="working-with-legacy-code">Working with legacy code</h2>

<p>It’s well known that code without tests is considered <code class="language-plaintext highlighter-rouge">legacy code</code>. To work quickly and effectively with a legacy codebase, snapshot tests are an excellent way. They allow you to capture the application’s existing behavior, providing a safety net that makes it possible to modify the codebase while detecting unexpected changes.</p>

<p>For web applications, testing HTTP and API endpoints is straightforward. For desktop applications or native mobile apps, you need to work one layer below the UI. For example, you can create a snapshot of a <a href="https://learn.microsoft.com/de-de/dotnet/architecture/maui/mvvm">ViewModel</a> before and after changing the code.</p>

<p>If you have the ability to create repeatable integration tests with external resources such as a database, then the contents of that database can also become the target of a snapshot test.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">Table_User_Should_MatchSnapshot</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Prepare database</span>

    <span class="c1">// Do testing operation</span>

    <span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="n">Repository</span><span class="p">.</span><span class="nf">GetUsers</span><span class="p">();</span>
    <span class="n">users</span><span class="p">.</span><span class="nf">MatchSnapshot</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>David</name></author><category term="c#" /><category term="testing" /><category term="snapshot" /><category term="en" /><summary type="html"><![CDATA[Testing has already been a recurring topic on this blog. In addition to property-based testing, I have also covered integration testing in .NET with Testcontainers and architecture testing with ArchUnitNET. Another useful tool in the testing toolbox is snapshot testing. Snapshot tests do exactly what the name suggests: they create a snapshot of a test result. This makes it possible to freeze the behavior of a system and protect it against unwanted changes and regressions.]]></summary></entry><entry xml:lang="en"><title type="html">Walk the Board: Effective Work with Task Boards in Software Projects</title><link href="https://www.production-ready.de/2025/10/24/walk-the-board-en.html" rel="alternate" type="text/html" title="Walk the Board: Effective Work with Task Boards in Software Projects" /><published>2025-10-24T22:15:00+02:00</published><updated>2025-10-24T22:15:00+02:00</updated><id>https://www.production-ready.de/2025/10/24/walk-the-board-en</id><content type="html" xml:base="https://www.production-ready.de/2025/10/24/walk-the-board-en.html"><![CDATA[<p>In software projects managed with tools such as <a href="https://www.atlassian.com/software/jira">Jira</a> or <a href="https://azure.microsoft.com/en-us/products/devops/">Azure DevOps</a>, it is common practice to visualize tasks and user stories on a <em>board</em>. This board helps the entire team to track the status of the work and identify potential bottlenecks in the workflow. During daily stand-up meetings, the progress of the current development iteration is discussed based on the board.</p>

<p>How to work with such a task board in an agile project environment in order to deliver not just <a href="https://mooncamp.com/blog/output-vs-outcome"><em>output</em> but <em>outcome</em></a> and to avoid falling into mere status reporting is what I’ll demonstrate in this blog post.</p>

<!--more-->

<p>Most software teams that work in iterations use some form of task board. These boards consist of several columns representing the current status of a task. In the simplest case, three columns <code class="language-plaintext highlighter-rouge">TODO</code>, <code class="language-plaintext highlighter-rouge">DOING</code>, and <code class="language-plaintext highlighter-rouge">DONE</code> are sufficient. Depending on the project setup, additional columns may appear at the beginning for design and refinement phases. On the other end, columns for testing or approval by the product owner or other stakeholders can be added. Furthermore, it is possible to assign each item on the board to the person currently working on the corresponding task.</p>

<picture><source srcset="/generated/2025-10-24-walk-the-board/board-3-columns-400-07152d807.webp 400w, /generated/2025-10-24-walk-the-board/board-3-columns-600-07152d807.webp 600w, /generated/2025-10-24-walk-the-board/board-3-columns-800-07152d807.webp 800w, /generated/2025-10-24-walk-the-board/board-3-columns-1000-07152d807.webp 1000w" type="image/webp" /><source srcset="/generated/2025-10-24-walk-the-board/board-3-columns-400-ef3720b77.png 400w, /generated/2025-10-24-walk-the-board/board-3-columns-600-ef3720b77.png 600w, /generated/2025-10-24-walk-the-board/board-3-columns-800-ef3720b77.png 800w, /generated/2025-10-24-walk-the-board/board-3-columns-1000-ef3720b77.png 1000w" type="image/png" /><img src="/generated/2025-10-24-walk-the-board/board-3-columns-800-ef3720b77.png" alt="Task Board with 3 Columns" /></picture>

<h2 id="shared-ownership">Shared Ownership</h2>

<p>The entire team is responsible for completing the work items. Therefore, user stories (or bugs or issues) are not assigned to individual people. At the beginning of an iteration, the team commits to a workload it believes it can handle. When a new work item is started, it is broken down into concrete tasks that need to be completed. During implementation, additional tasks may be added as it becomes clear that further work is required. These sub-tasks are then handled by individual team members and assigned to them on the board. In this way, multiple people can work on the same user story in parallel, allowing the team to keep track of which tasks are in progress and how many people are involved in each.</p>

<picture><source srcset="/generated/2025-10-24-walk-the-board/board-shared-400-1261ae3bb.webp 400w, /generated/2025-10-24-walk-the-board/board-shared-600-1261ae3bb.webp 600w, /generated/2025-10-24-walk-the-board/board-shared-800-1261ae3bb.webp 800w, /generated/2025-10-24-walk-the-board/board-shared-1000-1261ae3bb.webp 1000w" type="image/webp" /><source srcset="/generated/2025-10-24-walk-the-board/board-shared-400-e82f9f1e8.png 400w, /generated/2025-10-24-walk-the-board/board-shared-600-e82f9f1e8.png 600w, /generated/2025-10-24-walk-the-board/board-shared-800-e82f9f1e8.png 800w, /generated/2025-10-24-walk-the-board/board-shared-1000-e82f9f1e8.png 1000w" type="image/png" /><img src="/generated/2025-10-24-walk-the-board/board-shared-800-e82f9f1e8.png" alt="Shared Ownership Board" /></picture>

<p>This means that everyone on the team is responsible for completing the currently active work items. Throughout the entire process, there are no “my” or “your” stories. Once someone has finished their current task, they can join another story already in progress. Only when no further parallel work is reasonably possible does the team begin implementing a new story.</p>

<h2 id="prioritize">Prioritize</h2>

<p>How do I decide which task to tackle next? Because of the <em>shared ownership</em> of the entire iteration scope, I don’t have “my own” stories to work through. To avoid starting the next story at random, it’s important to prioritize the work currently represented on the board. Stories located further to the right side on the board are more important than those on the left. They always have a higher priority. If a story is in the <code class="language-plaintext highlighter-rouge">TESTING</code> column, I pick it up and test it. Instead of having a dedicated <code class="language-plaintext highlighter-rouge">TESTING</code> column, you can also simply create a sub-task for testing on the board. If a story is in the <code class="language-plaintext highlighter-rouge">DOING</code> column, I contribute to that story. Only when there are no more stories remaining on the right side of the board and no parallel work on active stories is possible does the team begin working on a new task.</p>

<p>In the Kanban framework, this concept is known as the <a href="https://www.atlassian.com/agile/kanban/wip-limits">Work in Progress (WIP) Limit</a>.</p>

<p>The reasoning behind this approach is simple: user stories that are not finished — that is, not in the <code class="language-plaintext highlighter-rouge">DONE</code> status — provide no value to the product. One completed story is more valuable than five that are only partially done. After all, “almost finished” is not “finished”.</p>

<blockquote>
  <p>Stop Starting, Start Finishing</p>
</blockquote>

<h2 id="walk-the-board">Walk the Board</h2>

<p>As we’ve seen above, we walk through our task board from the right to the left and the same principle applies to our daily sync meetings with the product owner. We go through the board from right to left, clarifying which items have been completed and can be accepted. It’s sufficient for one person to guide the discussion through the board; there’s no need for everyone to report on their individual activities. The purpose is not to justify how one spent the previous workday or to showcase progress on a personal story. When issues arise, the team discusses them collectively to find a solution.</p>

<p>By following this approach, teams can maintain focus even in stressful situations and ensure the continuous improvement and evolution of the product.</p>]]></content><author><name>David</name></author><category term="board" /><category term="jira" /><category term="azure" /><category term="task" /><category term="agile" /><category term="scrum" /><category term="kanban" /><category term="project" /><category term="en" /><summary type="html"><![CDATA[In software projects managed with tools such as Jira or Azure DevOps, it is common practice to visualize tasks and user stories on a board. This board helps the entire team to track the status of the work and identify potential bottlenecks in the workflow. During daily stand-up meetings, the progress of the current development iteration is discussed based on the board. How to work with such a task board in an agile project environment in order to deliver not just output but outcome and to avoid falling into mere status reporting is what I’ll demonstrate in this blog post.]]></summary></entry><entry xml:lang="en"><title type="html">Automatically publish RSS feeds on Mastodon</title><link href="https://www.production-ready.de/2024/11/24/feed-to-mastodon-en.html" rel="alternate" type="text/html" title="Automatically publish RSS feeds on Mastodon" /><published>2024-11-24T18:00:00+01:00</published><updated>2024-11-24T18:00:00+01:00</updated><id>https://www.production-ready.de/2024/11/24/feed-to-mastodon-en</id><content type="html" xml:base="https://www.production-ready.de/2024/11/24/feed-to-mastodon-en.html"><![CDATA[<p>Many blogs and news websites share information about new articles and posts on various social media platforms like Twitter, Facebook, or Threads. Unfortunately, Mastodon is often not yet a target for these cross-postings.</p>

<p>Since I personally enjoy using Mastodon as a news source and aggregator for various blogs, I developed an application that automatically publishes RSS feeds in the Fediverse.</p>

<!--more-->

<blockquote>
  <p>The source code and instructions for use are available on GitHub: <a href="https://github.com/davull/FeedToMastodon">https://github.com/davull/FeedToMastodon</a></p>
</blockquote>

<blockquote>
  <p>A running instance of the application publishes posts from several news feeds on <a href="https://feedmirror.social/public/local">https://feedmirror.social</a></p>
</blockquote>

<h2 id="feed-to-mastodon">Feed To Mastodon</h2>

<p><code class="language-plaintext highlighter-rouge">Feed To Mastodon</code> is a .NET application that automatically posts new feed entries to Mastodon. It supports <a href="https://en.wikipedia.org/wiki/RSS">RSS</a>, <a href="https://en.wikipedia.org/wiki/Atom_(web_standard)">Atom</a> and <a href="https://en.wikipedia.org/wiki/Resource_Description_Framework">RDF</a> feeds. The application can be compiled and run locally or used as a pre-configured <a href="https://hub.docker.com/r/davidullrich/feed-to-mastodon">Docker image</a>.</p>

<p>The application can handle any number of feeds. A separate Mastodon account can be used for each feed.</p>

<p>To prevent the respective Mastodon account from being flooded with old feed entries, previous entries are skipped during the first synchronization of a new feed, and only new entries are published.</p>

<h3 id="configuration">Configuration</h3>

<p>The configuration of the feeds to be monitored is done in a <code class="language-plaintext highlighter-rouge">.ini</code> file.</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[heise.de]</span><span class="w">
</span><span class="py">feed_url</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">https://www.heise.de/rss/heise-atom.xml</span>
<span class="py">summary_separator</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">[...]</span>
<span class="py">mastodon_server</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">https://mastodon.social/</span>
<span class="py">mastodon_access_token</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">AWWHkaIB_...</span>
<span class="w">
</span><span class="nn">[wired.com]</span><span class="w">
</span><span class="py">feed_url</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">https://www.wired.com/feed/rss</span>
<span class="py">mastodon_server</span><span class="w"> </span><span class="p">=</span><span class="w">  </span><span class="s">https://mastodon.social/</span>
<span class="py">mastodon_access_token</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">ABC...</span>
<span class="w">
</span><span class="na">...</span><span class="w">
</span></code></pre></div></div>

<p>In addition to the URLs for the feed and the Mastodon server where the target account is registered, an access token is required.</p>

<p>The <code class="language-plaintext highlighter-rouge">summary_separator</code> parameter is optional and can be specified to shorten feed entries. The entry will be truncated at the first occurrence of the separator.</p>

<p>For example, if a post contains <code class="language-plaintext highlighter-rouge">[…]</code>, the entry can be shortened here:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Earlier today, we reported that […]
To the post &lt;a rel="nofollow" ...&gt;http://news.com/123&lt;/a&gt;
</code></pre></div></div>

<p>Configuring <code class="language-plaintext highlighter-rouge">summary_separator = […]</code> results in the shortened entry:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Earlier today, we reported that ...
</code></pre></div></div>

<p>Posted entries are stored in an SQLite database to prevent duplicate posts.</p>

<h4 id="mastodon-access-token">Mastodon access token</h4>

<p>An access token can be generated via the Mastodon website under <code class="language-plaintext highlighter-rouge">Preferences</code> -&gt; <code class="language-plaintext highlighter-rouge">Development</code> -&gt; <code class="language-plaintext highlighter-rouge">New application</code>. In the <code class="language-plaintext highlighter-rouge">Name</code> field, you can enter any text, such as <code class="language-plaintext highlighter-rouge">Feed to Mastodon</code>. For the <code class="language-plaintext highlighter-rouge">Redirect URI</code>, the default value <code class="language-plaintext highlighter-rouge">urn:ietf:wg:oauth:2.0:oob</code> can be kept. Under <code class="language-plaintext highlighter-rouge">Scopes</code>, only <code class="language-plaintext highlighter-rouge">write:statuses</code> needs to be selected, and <code class="language-plaintext highlighter-rouge">profile</code> can be deselected.</p>

<picture><source srcset="/generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-400-0f621878c.webp 400w, /generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-600-0f621878c.webp 600w, /generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-800-0f621878c.webp 800w, /generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-1000-0f621878c.webp 1000w" type="image/webp" /><source srcset="/generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-400-58823c5d4.png 400w, /generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-600-58823c5d4.png 600w, /generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-800-58823c5d4.png 800w, /generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-1000-58823c5d4.png 1000w" type="image/png" /><img src="/generated/2024-11-24-feed-to-mastodon/mastodon-your-applications-800-58823c5d4.png" alt="Your Applications" /></picture>

<p>After saving, you can click on the newly created application again and copy the access token under <code class="language-plaintext highlighter-rouge">Your access token</code>.</p>

<picture><source srcset="/generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-400-c17ab9f21.webp 400w, /generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-600-c17ab9f21.webp 600w, /generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-800-c17ab9f21.webp 800w, /generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-1000-c17ab9f21.webp 1000w" type="image/webp" /><source srcset="/generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-400-9af0ed109.png 400w, /generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-600-9af0ed109.png 600w, /generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-800-9af0ed109.png 800w, /generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-1000-9af0ed109.png 1000w" type="image/png" /><img src="/generated/2024-11-24-feed-to-mastodon/mastodon-application-feed-to-mastodon-800-9af0ed109.png" alt="Application Feed to Mastodon" /></picture>

<h4 id="parameters">Parameters</h4>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>feed_url</td>
      <td>URL of the feed, required</td>
    </tr>
    <tr>
      <td>summary_separator</td>
      <td>Separator where the summary is cut off, see below, optional</td>
    </tr>
    <tr>
      <td>mastodon_server</td>
      <td>URL of the Mastodon server, required</td>
    </tr>
    <tr>
      <td>mastodon_access_token</td>
      <td>Access token for the Mastodon account, required</td>
    </tr>
  </tbody>
</table>

<p>The file paths for the configuration and database files are stored as environment variables.</p>

<table>
  <thead>
    <tr>
      <th>Environment Variable</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FTM_CONFIG_FILE_NAME</td>
      <td>Absolute or relative path to the configuration file</td>
    </tr>
    <tr>
      <td>FTM_DATABASE_NAME</td>
      <td>Absolute or relative path to the sqlite database</td>
    </tr>
  </tbody>
</table>

<h3 id="run-feed-to-mastodon">Run Feed to Mastodon</h3>

<p>To run <code class="language-plaintext highlighter-rouge">Feed to Mastodon</code> via <a href="https://www.docker.com">Docker</a>, run the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">PWD</span><span class="k">}</span><span class="s2">/ftm-feed-config.ini:/app/ftm-feed-config.ini"</span> <span class="se">\</span>
  <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">PWD</span><span class="k">}</span><span class="s2">/ftm.sqlite:/app/ftm.sqlite"</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="s2">"FTM_CONFIG_FILE_NAME=/app/ftm-feed-config.ini"</span> <span class="se">\</span>
  <span class="nt">-e</span> <span class="s2">"FTM_DATABASE_NAME=/app/ftm.sqlite"</span> <span class="se">\</span>
  davidullrich/feed-to-mastodon:latest
</code></pre></div></div>

<p>A <a href="https://docs.docker.com/compose/">Docker Compose</a> configuration looks like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">ftm</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">feed-to-mastodon</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">davidullrich/feed-to-mastodon:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./data:/app/data</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">FTM_DATABASE_NAME=/app/data/ftm.sqlite</span>
      <span class="pi">-</span> <span class="s">FTM_CONFIG_FILE_NAME=/app/data/ftm.ini</span>
      <span class="pi">-</span> <span class="s">TZ=Europe/Berlin</span>
</code></pre></div></div>

<h4 id="statistics">Statistics</h4>

<p>For those interested, <code class="language-plaintext highlighter-rouge">Feed to Mastodon</code> outputs a daily statistic on the console. This shows the number of feeds posted in the last 24 hours and the last seven days.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Statistics[0] ============================================================
Statistics[0] Total Posts per feed:    2024-11-18 00:00 - 2024-11-25 00:00
Statistics[0] ============================================================
Statistics[0]   Mashable:                                              215
Statistics[0]   Caschys Blog:                                          183
Statistics[0]   Digital Trends:                                        299
Statistics[0]   Elektroauto-News.net:                                    8
Statistics[0]   TESLARATI:                                              48
Statistics[0]   WIRED:                                                 102
Statistics[0] ------------------------------------------------------------
Statistics[0] Total:                                                   855
Statistics[0] ============================================================
</code></pre></div></div>

<h3 id="live-example">Live example</h3>

<p>At <a href="https://feedmirror.social/public/local">https://feedmirror.social</a>, <code class="language-plaintext highlighter-rouge">Feed to Mastodon</code> is running with a few configured feeds, bringing the latest articles into the Fediverse.</p>

<picture><source srcset="/generated/2024-11-24-feed-to-mastodon/mastodon-post-wired-400-d2af0f1ea.webp 400w, /generated/2024-11-24-feed-to-mastodon/mastodon-post-wired-600-d2af0f1ea.webp 600w, /generated/2024-11-24-feed-to-mastodon/mastodon-post-wired-761-d2af0f1ea.webp 761w" type="image/webp" /><source srcset="/generated/2024-11-24-feed-to-mastodon/mastodon-post-wired-400-61926cbdf.png 400w, /generated/2024-11-24-feed-to-mastodon/mastodon-post-wired-600-61926cbdf.png 600w, /generated/2024-11-24-feed-to-mastodon/mastodon-post-wired-761-61926cbdf.png 761w" type="image/png" /><img src="/generated/2024-11-24-feed-to-mastodon/mastodon-post-wired-761-61926cbdf.png" alt="Wired Mastodon Post" /></picture>]]></content><author><name>David</name></author><category term="mastodon" /><category term="feed" /><category term="rss" /><category term="atom" /><category term="rdf" /><category term="crosspost" /><category term="fediverse" /><category term="en" /><summary type="html"><![CDATA[Many blogs and news websites share information about new articles and posts on various social media platforms like Twitter, Facebook, or Threads. Unfortunately, Mastodon is often not yet a target for these cross-postings. Since I personally enjoy using Mastodon as a news source and aggregator for various blogs, I developed an application that automatically publishes RSS feeds in the Fediverse.]]></summary></entry><entry xml:lang="en"><title type="html">Integration tests with Docker Compose and Azure Service Containers</title><link href="https://www.production-ready.de/2024/05/10/integration-testing-with-docker-en.html" rel="alternate" type="text/html" title="Integration tests with Docker Compose and Azure Service Containers" /><published>2024-05-10T19:00:00+02:00</published><updated>2024-05-10T19:00:00+02:00</updated><id>https://www.production-ready.de/2024/05/10/integration-testing-with-docker-en</id><content type="html" xml:base="https://www.production-ready.de/2024/05/10/integration-testing-with-docker-en.html"><![CDATA[<p>In a <a href="/2024/04/27/integration-testing-with-testcontainers-en.html">previous blog post</a>, I demonstrated how to implement integration tests in a .NET application using <a href="https://testcontainers.com">Testcontainers</a>.</p>

<p>If you prefer not to manage Docker containers within the test code, it is advisable to start and stop the containers independently of the test execution. <a href="https://docs.docker.com/compose/">Docker Compose</a> and the Azure DevOps feature <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/service-containers">Service Containers</a> are good alternatives for this.</p>

<!--more-->

<blockquote>
  <p>The example code for this blog post is available on Github: <a href="https://github.com/davull/demo-docker-compose-test">https://github.com/davull/demo-docker-compose-test</a></p>
</blockquote>

<h2 id="docker-compose">Docker Compose</h2>

<p><a href="https://docs.docker.com/compose/">Docker Compose</a> allows the definition and management of multiple Docker containers in a single file. These can then be started and stopped with just one command. Additionally, it enables the definition of networks between containers, the creation of storage volumes, and the setting of environment variables. For the demo application used here, I utilize a <a href="https://mariadb.org">MariaDB</a> database and a <a href="https://www.phpmyadmin.net">phpMyAdmin</a> container. A simplified version of the Docker Compose file looks like this (the full configuration can be found in the <a href="https://github.com/davull/demo-docker-compose-test/blob/main/docker/docker-compose.yml">Github repository</a>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">orderapp"</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">order-mariadb</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mariadb:11.3</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">order-mariadb</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">order-mariadb:/var/lib/mysql</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">order-net</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">MYSQL_ROOT_PASSWORD</span><span class="pi">:</span> <span class="s2">"</span><span class="s">some-password"</span>
      <span class="na">MYSQL_DATABASE</span><span class="pi">:</span> <span class="s2">"</span><span class="s">orders"</span>

  <span class="na">order-pma</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">phpmyadmin</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">order-pma</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">9002:80"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">order-net</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">order-net</span><span class="pi">:</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">order-mariadb</span><span class="pi">:</span>
</code></pre></div></div>

<p>Using <code class="language-plaintext highlighter-rouge">docker compose up</code> and <code class="language-plaintext highlighter-rouge">docker compose down</code>, you can start and stop all containers respectively.</p>

<p>If you want to use a MariaDB container for the integration tests of your application, there are a few things to consider. You need to initialize the database and populate it with appropriate test data. This can be done using a <a href="https://xunit.net/docs/shared-context">Shared Context</a> within your test suite for <a href="https://xunit.net">xUnit</a>, for instance, performing a <a href="https://en.wikipedia.org/wiki/Database_seeding">Seeding</a> of the database before each test run. After the test run, the database is simply deleted.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">DatabaseFixture</span> <span class="p">:</span> <span class="n">IAsyncLifetime</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="kt">string</span> <span class="n">_databaseName</span><span class="p">;</span>
    
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">InitializeAsync</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">_databaseName</span> <span class="p">=</span> <span class="nf">GetRandomTestDatabaseName</span><span class="p">();</span>
        <span class="k">await</span> <span class="n">Database</span><span class="p">.</span><span class="nf">Seed</span><span class="p">(</span><span class="n">_databaseName</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">DisposeAsync</span><span class="p">()</span>
      <span class="p">=&gt;</span> <span class="k">await</span> <span class="n">Database</span><span class="p">.</span><span class="nf">DeleteDatabase</span><span class="p">(</span><span class="n">_databaseName</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Alternatively, you can utilize a feature of the <a href="https://hub.docker.com/_/mariadb">MariaDB container</a> that allows the execution of arbitrary SQL scripts when the container starts. To do this, you mount a folder into the container at <code class="language-plaintext highlighter-rouge">/docker-entrypoint-initdb.d</code>. All SQL files in this folder will be executed when the container starts.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">order-mariadb</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mariadb:11.3</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./initdb:/docker-entrypoint-initdb.d</span>
</code></pre></div></div>

<p>Besides the initial filling of the database, it is necessary to be able to determine after the start of the container whether the database server is ready to receive requests. This is crucial for running integration tests in a CI/CD pipeline to ensure that the tests are only executed when the container is fully operational. For Docker containers, the option of <a href="https://docs.docker.com/reference/dockerfile/#healthcheck">healthchecks</a> is suitable. MariaDB already includes an appropriate <a href="https://mariadb.com/kb/en/using-healthcheck-sh/">healthcheck.sh script</a> for this purpose.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">order-mariadb</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mariadb:11.3</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">3s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">3</span>
      <span class="na">test</span><span class="pi">:</span>
        <span class="pi">[</span>
          <span class="s2">"</span><span class="s">CMD"</span><span class="pi">,</span>
          <span class="s2">"</span><span class="s">healthcheck.sh"</span><span class="pi">,</span>
          <span class="s2">"</span><span class="s">--su-mysql"</span><span class="pi">,</span>
          <span class="s2">"</span><span class="s">--connect"</span><span class="pi">,</span>
          <span class="s2">"</span><span class="s">--innodb_initialized"</span><span class="pi">,</span>
        <span class="pi">]</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">30s</span>
</code></pre></div></div>

<h2 id="docker-compose-in-a-azure-pipeline">Docker Compose in a Azure Pipeline</h2>

<p>With the previously created Docker Compose file, you can run the integration tests locally and also in an Azure DevOps CI/CD pipeline. First, we start our containers using the <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/docker-compose-v0">DockerCompose@0</a> task:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">DockerCompose@0</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Docker</span><span class="nv"> </span><span class="s">compose</span><span class="nv"> </span><span class="s">up"</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">containerregistrytype</span><span class="pi">:</span> <span class="s">Container Registry</span>
    <span class="na">dockerComposeFile</span><span class="pi">:</span> <span class="s2">"</span><span class="s">./docker/docker-compose.yml"</span>
    <span class="na">action</span><span class="pi">:</span> <span class="s">Run a Docker Compose command</span>
    <span class="na">dockerComposeCommand</span><span class="pi">:</span> <span class="s2">"</span><span class="s">up</span><span class="nv"> </span><span class="s">-d"</span>
    <span class="na">projectName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">orderapp"</span>
</code></pre></div></div>

<p>After starting the containers, you need to wait for the database container to be ready for operation using a <code class="language-plaintext highlighter-rouge">healthcheck</code>.</p>

<p>If you are running your Azure pipeline in a <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/container-phases">Container Job</a>, you must connect the database container to the network of the pipeline container. The environment variable <code class="language-plaintext highlighter-rouge">$(Agent.ContainerNetwork)</code> contains the name of the network in which the pipeline container is running.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Prepair</span><span class="nv"> </span><span class="s">containers"</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">workingDirectory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">./docker"</span>
    <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">echo -e "Connect database to network $(Agent.ContainerNetwork) ...\n"</span>
      <span class="s">docker network connect $(Agent.ContainerNetwork) order-mariadb</span>
      
      <span class="s">echo -e "Waiting for container to be healthy ...\n"</span>
      <span class="s">until [ "$(docker inspect -f '{{.State.Health.Status}}' order-mariadb)" == "healthy" ]; do</span>
        <span class="s">sleep 1</span>
      <span class="s">done</span>
</code></pre></div></div>
<p>Afterwards, the tests can be run. Since the configuration of the database connection in the CI pipeline is different from that on a local PC or in a production environment, you can set it accordingly using a <a href="https://learn.microsoft.com/en-us/visualstudio/test/configure-unit-tests-by-using-a-dot-runsettings-file">.runsettings file</a>. This file allows you to specify different settings for different environments, ensuring that your tests can connect to the correct database instance depending on where they are being run.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;RunSettings&gt;</span>
  <span class="nt">&lt;RunConfiguration&gt;</span>
      <span class="nt">&lt;EnvironmentVariables&gt;</span>
          <span class="nt">&lt;DB_SERVER&gt;</span>order-mariadb<span class="nt">&lt;/DB_SERVER&gt;</span>
          <span class="nt">&lt;DB_PORT&gt;</span>3306<span class="nt">&lt;/DB_PORT&gt;</span>
          <span class="nt">&lt;DB_NAME&gt;</span>orders<span class="nt">&lt;/DB_NAME&gt;</span>
          <span class="nt">&lt;DB_USER&gt;</span>root<span class="nt">&lt;/DB_USER&gt;</span>
          <span class="nt">&lt;DB_PASSWORD&gt;</span>some-password<span class="nt">&lt;/DB_PASSWORD&gt;</span>
      <span class="nt">&lt;/EnvironmentVariables&gt;</span>
  <span class="nt">&lt;/RunConfiguration&gt;</span>
<span class="nt">&lt;/RunSettings&gt;</span>
</code></pre></div></div>

<p>In the <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/dotnet-core-cli-v2">DotNetCoreCLI@2</a> task, you specify the file as an argument. This can be done by including the <code class="language-plaintext highlighter-rouge">--settings</code> option followed by the path to your <code class="language-plaintext highlighter-rouge">.runsettings</code> file, allowing the .NET Core CLI to apply the specified configurations during the test run.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">DotNetCoreCLI@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Dotnet</span><span class="nv"> </span><span class="s">test"</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">test</span>
    <span class="na">arguments</span><span class="pi">:</span> <span class="s2">"</span><span class="s">--settings</span><span class="nv"> </span><span class="s">./src/ci-tests.runsettings"</span>
</code></pre></div></div>

<p>After the test run, the containers are stopped again.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">DockerCompose@0</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Docker</span><span class="nv"> </span><span class="s">compose</span><span class="nv"> </span><span class="s">down"</span>
  <span class="na">condition</span><span class="pi">:</span> <span class="s">always()</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">containerregistrytype</span><span class="pi">:</span> <span class="s">Container Registry</span>
    <span class="na">dockerComposeFile</span><span class="pi">:</span> <span class="s2">"</span><span class="s">./docker/docker-compose.yml"</span>
    <span class="na">currentWorkingDirectory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">./docker"</span>
    <span class="na">action</span><span class="pi">:</span> <span class="s">Run a Docker Compose command</span>
    <span class="na">dockerComposeCommand</span><span class="pi">:</span> <span class="s2">"</span><span class="s">down</span><span class="nv"> </span><span class="s">-v"</span>
    <span class="na">projectName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">orderapp"</span>
</code></pre></div></div>

<h2 id="azure-devops-service-containers">Azure DevOps service containers</h2>

<p>As an alternative to setting up and starting Docker containers using the <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/docker-v2">Docker@2</a> or <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/docker-compose-v0">DockerCompose@0</a> task, Microsoft offers the option to deploy container resources as <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/service-containers">Service Containers</a> within a pipeline. These containers run parallel to the build and test jobs and can be accessed by them. Therefore, Service Containers are also suitable for providing resources such as databases for integration tests.</p>

<p>The configuration of Service Containers is done directly in the <code class="language-plaintext highlighter-rouge">.yaml</code> file of the pipeline definition; the feature is not available for <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/release/define-multistage-release-process">classic Azure Pipelines</a>. The syntax is very similar to that of Docker Compose, so those familiar with it will find it easy to adapt.</p>

<p>Containers are defined under the <code class="language-plaintext highlighter-rouge">resources</code> section and then exposed under <code class="language-plaintext highlighter-rouge">services</code> (container resources can also be used for <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/container-phases">Container Jobs</a>).</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">resources</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">container</span><span class="pi">:</span> <span class="s">order-mariadb</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">mariadb:11.3</span>
      <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">3306:3306</span>
      <span class="na">env</span><span class="pi">:</span>
        <span class="na">MYSQL_ROOT_PASSWORD</span><span class="pi">:</span> <span class="s2">"</span><span class="s">some-password"</span>
        <span class="na">MYSQL_DATABASE</span><span class="pi">:</span> <span class="s2">"</span><span class="s">orders"</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">order-mariadb</span><span class="pi">:</span> <span class="s">order-mariadb</span>
</code></pre></div></div>

<p>With this setup, the MariaDB server can be accessed at <code class="language-plaintext highlighter-rouge">localhost:3306</code>.</p>

<p>If you are not using Docker containers for local development that can reuse configurations for integration tests within the CI pipeline, Service Containers offer a simple way to provide the necessary resources.</p>]]></content><author><name>David</name></author><category term="testing" /><category term="integrationtesting" /><category term="docker" /><category term="container" /><category term="compose" /><category term="azure" /><category term="devops" /><category term="servicecontainer" /><category term="en" /><summary type="html"><![CDATA[In a previous blog post, I demonstrated how to implement integration tests in a .NET application using Testcontainers. If you prefer not to manage Docker containers within the test code, it is advisable to start and stop the containers independently of the test execution. Docker Compose and the Azure DevOps feature Service Containers are good alternatives for this.]]></summary></entry><entry xml:lang="en"><title type="html">Integration tests in .NET with Testcontainers</title><link href="https://www.production-ready.de/2024/04/27/integration-testing-with-testcontainers-en.html" rel="alternate" type="text/html" title="Integration tests in .NET with Testcontainers" /><published>2024-04-27T19:00:00+02:00</published><updated>2024-04-27T19:00:00+02:00</updated><id>https://www.production-ready.de/2024/04/27/integration-testing-with-testcontainers-en</id><content type="html" xml:base="https://www.production-ready.de/2024/04/27/integration-testing-with-testcontainers-en.html"><![CDATA[<p>In software development, it is essential to sufficiently test the written code. In addition to <a href="/2023/06/10/property-based-testing-in-csharp-en.html">Unit Tests</a>, which individually test single components, integration tests examine the interaction between multiple components. However, when there are external dependencies such as a database or a message broker, testing becomes more complex.</p>

<p>To ensure that external systems are reproducibly and predictably available in integration tests, they can be run in Docker containers. A simple setup of such service containers is enabled by the library <a href="https://testcontainers.com">Testcontainers</a>.</p>

<!--more-->

<blockquote>
  <p>The example code for this blog post is available on GitHub: <a href="https://github.com/davull/demo-docker-testcontainers">https://github.com/davull/demo-docker-testcontainers</a></p>
</blockquote>

<p>In a <a href="/2024/05/10/integration-testing-with-docker-en.html">subsequent blog post</a>, I will show how to implement integration tests using <a href="https://docs.docker.com/compose/">Docker Compose</a> and Azure <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/service-containers">Service Containers</a>.</p>

<h2 id="integration-tests">Integration Tests</h2>

<p>Integration tests are designed to examine the interaction of multiple components of a software system. This can involve individual functional units but also a complete pass-through of the application. For example, for a web API, one might send an HTTP request to the application, manipulate data in a database, and then return the result stored in a Redis cache back to the client.</p>

<p>While the behavior of external systems can be emulated using <a href="https://en.wikipedia.org/wiki/Mock_object">mocks</a>, integration tests truly add value when they interact with real systems. This moves us closer to a production environment, allowing for the discovery of problems that would not be visible with mocks.</p>

<p>Most systems can now be operated in a <a href="https://www.docker.com/#build">Docker container</a>. If a pre-configured image is not available, it is usually straightforward to create one’s own.</p>

<p>This containerization of applications can be utilized in testing to start the required systems in a defined state before each test run, such as in a CI/CD pipeline. After the tests are executed, the containers are simply stopped and deleted.</p>

<h2 id="testcontainers">Testcontainers</h2>

<p>The <a href="https://testcontainers.com">Testcontainers</a> project provides libraries for many programming languages to simplify the setup and execution of Docker containers in tests. For .NET, there is a corresponding <a href="https://www.nuget.org/packages/Testcontainers">NuGet package</a> available: <a href="https://dotnet.testcontainers.org">Testcontainers for .NET</a>.</p>

<p>A Testcontainer is simply defined before executing the test methods, started, and then stopped and deleted afterwards. The <code class="language-plaintext highlighter-rouge">ContainerBuilder</code> class offers a Fluent API to configure the containers.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">container</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ContainerBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithImage</span><span class="p">(</span><span class="s">"mariadb:11.3"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithHostname</span><span class="p">(</span><span class="s">"mariadb-test-container"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="nf">StartAsync</span><span class="p">();</span>

<span class="c1">// run integration tests</span>

<span class="k">await</span> <span class="n">container</span><span class="p">.</span><span class="nf">StopAsync</span><span class="p">();</span>
</code></pre></div></div>

<p>To be able to run the tests locally, Docker must be installed on the computer and the Docker service must be running.</p>

<p>Best practice is to map container ports to random host ports to avoid conflicts with other running services and containers.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">containerBuilder</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ContainerBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithPortBinding</span><span class="p">(</span><span class="m">3306</span><span class="p">,</span> <span class="n">assignRandomHostPort</span><span class="p">:</span> <span class="k">true</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">port</span> <span class="p">=</span> <span class="n">container</span><span class="p">.</span><span class="nf">GetMappedPublicPort</span><span class="p">(</span><span class="m">3306</span><span class="p">);</span>
</code></pre></div></div>

<p>To wait for the availability of a service within the container after startup, Testcontainers provides a variety of <code class="language-plaintext highlighter-rouge">Wait</code> strategies. For example, you can wait for the container’s health status or check the availability of any TCP port. Waiting for an HTTP endpoint is also possible.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Wait for MySQL to be ready</span>
<span class="kt">var</span> <span class="n">containerBuilder</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ContainerBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithImage</span><span class="p">(</span><span class="s">"mariadb:11.3"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithWaitStrategy</span><span class="p">(</span><span class="n">Wait</span><span class="p">.</span><span class="nf">ForUnixContainer</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">UntilPortIsAvailable</span><span class="p">(</span><span class="m">3306</span><span class="p">));</span>

<span class="c1">// Wait for HTTP service to be ready</span>
<span class="kt">var</span> <span class="n">containerBuilder</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ContainerBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithImage</span><span class="p">(</span><span class="s">"productservice:latest"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithWaitStrategy</span><span class="p">(</span><span class="n">Wait</span><span class="p">.</span><span class="nf">ForUnixContainer</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">UntilHttpRequestIsSucceeded</span><span class="p">(</span><span class="n">strategy</span> <span class="p">=&gt;</span> <span class="n">strategy</span>
            <span class="p">.</span><span class="nf">ForPort</span><span class="p">(</span><span class="m">443</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">ForPath</span><span class="p">(</span><span class="s">"/api/products"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">WithBasicAuthentication</span><span class="p">(</span><span class="s">"user"</span><span class="p">,</span> <span class="s">"password"</span><span class="p">)));</span>
</code></pre></div></div>

<p>Many container images can be configured via environment variables. These can easily be passed to a Testcontainer using the <code class="language-plaintext highlighter-rouge">WithEnvironment(env)</code> method.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">env</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="p">{</span> <span class="s">"MYSQL_ROOT_PASSWORD"</span><span class="p">,</span> <span class="s">"some-password"</span> <span class="p">},</span>
    <span class="p">{</span> <span class="s">"MYSQL_DATABASE"</span><span class="p">,</span> <span class="s">"products"</span> <span class="p">}</span>
<span class="p">};</span>

<span class="kt">var</span> <span class="n">containerBuilder</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ContainerBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithEnvironment</span><span class="p">(</span><span class="n">env</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="pre-configured-containers">Pre-configured Containers</h3>

<p>Testcontainers offers a range of <a href="https://www.nuget.org/profiles/Testcontainers">pre-configured containers</a> that simplify the use of various services. This eliminates the need for specifying the image or port mapping. A <code class="language-plaintext highlighter-rouge">MySqlContainer</code> provides the option to directly obtain the appropriate connection string for the database.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">container</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MySqlBuilder</span><span class="p">().</span><span class="nf">Build</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">connectionString</span> <span class="p">=</span> <span class="n">container</span><span class="p">.</span><span class="nf">GetConnectionString</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="test-fixtures">Test Fixtures</h3>

<p>To centralize the configuration of Testcontainers and not generate and start a new container for each test method call, one can use the capabilities of various testing frameworks to create so-called test fixtures.</p>

<p>For <a href="https://xunit.net">xUnit</a>, you can create a class called <code class="language-plaintext highlighter-rouge">DatabaseFixture</code> that handles initializing and starting the container. A class that implements the <code class="language-plaintext highlighter-rouge">ICollectionFixture&lt;DatabaseFixture&gt;</code> interface can then control which tests share a container instance.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">DatabaseFixture</span> <span class="p">:</span> <span class="n">IAsyncLifetime</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">InitializeAsync</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">_container</span> <span class="p">=</span> <span class="nf">BuildContainer</span><span class="p">();</span>
        <span class="k">await</span> <span class="n">_container</span><span class="p">.</span><span class="nf">StartAsync</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">DisposeAsync</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">_container</span><span class="p">.</span><span class="nf">StopAsync</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="c1">// ...</span>
<span class="p">}</span>

<span class="p">[</span><span class="nf">CollectionDefinition</span><span class="p">(</span><span class="n">Name</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">DatabaseCollectionFixture</span> <span class="p">:</span> <span class="n">ICollectionFixture</span><span class="p">&lt;</span><span class="n">DatabaseFixture</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=</span> <span class="s">"DatabaseCollection"</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The corresponding tests are annotated with the <code class="language-plaintext highlighter-rouge">Collection</code> attribute and the collection name that provides the fixture container.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Collection</span><span class="p">(</span><span class="n">DatabaseCollectionFixture</span><span class="p">.</span><span class="n">Name</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">DatabaseTests</span>
<span class="p">{</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>The project in the GitHub repository for this post shows a complete example: <a href="https://github.com/davull/demo-docker-testcontainers">https://github.com/davull/demo-docker-testcontainers</a></p>
</blockquote>

<h2 id="cicd-pipeline">CI/CD Pipeline</h2>

<p>Testcontainers can be run in a CI/CD pipeline in the same way as locally. The only requirement is that Docker and the Docker CLI are also installed on the build server. In the case of Azure DevOps, this is already the case for Microsoft-hosted build agents. If you are using your own build containers, the Docker CLI can be installed via the task <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/docker-installer-v0">DockerInstaller@0</a>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">DockerInstaller@0</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Install</span><span class="nv"> </span><span class="s">docker</span><span class="nv"> </span><span class="s">cli"</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">dockerVersion</span><span class="pi">:</span> <span class="s">26.1.0</span>
</code></pre></div></div>

<p>When executing the test step, the required images are downloaded, if not already present, and the containers are started.</p>

<picture><source srcset="/generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-400-6c0b89158.webp 400w, /generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-600-6c0b89158.webp 600w, /generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-800-6c0b89158.webp 800w, /generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-1000-6c0b89158.webp 1000w" type="image/webp" /><source srcset="/generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-400-edfe26354.png 400w, /generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-600-edfe26354.png 600w, /generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-800-edfe26354.png 800w, /generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-1000-edfe26354.png 1000w" type="image/png" /><img src="/generated/2024-04-27-integration-testing-with-testcontainers/azure-devops-output-800-edfe26354.png" alt="Azure DevOps Output" /></picture>]]></content><author><name>David</name></author><category term="testing" /><category term="integrationtesting" /><category term="testcontainers" /><category term="en" /><summary type="html"><![CDATA[In software development, it is essential to sufficiently test the written code. In addition to Unit Tests, which individually test single components, integration tests examine the interaction between multiple components. However, when there are external dependencies such as a database or a message broker, testing becomes more complex. To ensure that external systems are reproducibly and predictably available in integration tests, they can be run in Docker containers. A simple setup of such service containers is enabled by the library Testcontainers.]]></summary></entry><entry xml:lang="en"><title type="html">Lightweight Architecture Documentation with ADRs</title><link href="https://www.production-ready.de/2023/12/28/lightweight-architecture-documentation-adr-en.html" rel="alternate" type="text/html" title="Lightweight Architecture Documentation with ADRs" /><published>2023-12-28T12:00:00+01:00</published><updated>2023-12-28T12:00:00+01:00</updated><id>https://www.production-ready.de/2023/12/28/lightweight-architecture-documentation-adr-en</id><content type="html" xml:base="https://www.production-ready.de/2023/12/28/lightweight-architecture-documentation-adr-en.html"><![CDATA[<p>When a software application reaches a certain size, documentation of the individual components and their interactions makes sense. Recording architectural decisions is also important to be able to understand why a particular solution was chosen even months or years later. Future colleagues and your future self will thank you for it.</p>

<p>A lightweight method for documenting architectural decisions is through <a href="https://adr.github.io">Architectural Decision Records</a>, ADRs.</p>

<!--more-->

<h2 id="architecture-documentation">Architecture Documentation</h2>

<p>Every piece of software also has a software architecture. Whether it is actively designed or “emerges” during development depends on the circumstances and requirements. To be able to comprehend and potentially evaluate decisions made on the way to the current state of the software retrospectively, architecture documentation, or at least documentation of the most important decisions, is advisable. Such decisions are often recorded in a wiki, Confluence, or Word document. The documentation thus occurs outside the actual source code and in an unstructured manner, making maintenance and updates challenging.</p>

<p>With <a href="https://www.arc42.de">arc42</a> or <a href="https://structurizr.com">Structurizr</a>, frameworks exist to systematically document various aspects of software and its architecture. For projects that do not require such extensive documentation, ADRs offer a lightweight solution.</p>

<h2 id="architectural-decision-records">Architectural Decision Records</h2>

<p>The goal of <code class="language-plaintext highlighter-rouge">Architectural Decision Records</code> is not to describe or reflect the entire architecture of software but to document decisions that have led to the current architecture.</p>

<p><code class="language-plaintext highlighter-rouge">ADRs</code> record architectural decisions in plain text files and prescribe only the format. Each decision gets its own file and a consecutive number. <a href="https://www.markdownguide.org">Markdown</a> is used for simple formatting. The files are part of the source code repository, thus being subject to version control. ADRs can be introduced and discussed through pull requests.</p>

<p>Even though the name sets the context to <code class="language-plaintext highlighter-rouge">Architecture</code>, ADRs can be utilized for a variety of technical and non-technical decision documentation.</p>

<h2 id="format">Format</h2>

<p>According to the inventor <a href="https://cognitect.com/authors/MichaelNygard.html">Michael Nygard</a>, ADRs consist of the <a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions">following fields</a>:</p>

<p><strong>Title:</strong> Brief description of the decision.</p>

<p><strong>Context:</strong> Description of the context in which the decision was made, including technological, political, or social conditions. The wording is neutral and describes the factual circumstances.</p>

<p><strong>Decision:</strong> Describes the decision made in the previously outlined context. The wording is active and describes what will be done: “We will…”.</p>

<p><strong>Status:</strong> The status of the decision. Possible values are <code class="language-plaintext highlighter-rouge">proposed</code>, <code class="language-plaintext highlighter-rouge">accepted</code>, <code class="language-plaintext highlighter-rouge">deprecated</code>, or <code class="language-plaintext highlighter-rouge">superseded</code>.</p>

<p><strong>Consequences:</strong> Describes the resulting state after the decision. Both positive and negative consequences are listed.</p>

<p>Such an ADR document has a length of one to two pages. It represents communication with future developers or ourselves. Therefore, it is important that the documentation is clear, comprehensible, and not just a bullet-point list. For the file name, Nygard suggests a format: <code class="language-plaintext highlighter-rouge">doc/arch/adr-NNN.md</code>. Through the unique identifier, individual ADRs can be referenced, such as when later decisions build upon or replace a previous one.</p>

<p>Whether you follow the format shown here or develop your own variation for your project is secondary. What matters is that the format is consistently adhered to throughout the project. <a href="https://www.linkedin.com/in/joelparkerhenderson/">Joel Parker Henderson</a> has compiled <a href="https://github.com/joelparkerhenderson/architecture-decision-record/tree/main/locales/en/templates">a series of ADR templates</a> in a GitHub repository.</p>

<h3 id="example">Example</h3>

<p>The ADR for the decision to use <a href="https://www.rabbitmq.com">RabbitMQ</a> as a message broker might look like the following:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># ADR-039: RabbitMQ Message Broker</span>

<span class="gu">## Context</span>

We need a message broker to exchange asynchronous messages
between the individual components of our application. The
solution should be able to operate in a fault-tolerant manner
and persistently store messages. The solution should be hosted
in our own data center and operated by our service team.
RabbitMQ is already in use in other projects, and the service
team is familiar with its operation.

<span class="gu">## Decision</span>

We will be using RabbitMQ as the message broker. RabbitMQ
meets our requirements and is already established within the
company. The responsibility for operation will be assigned
to the service team. We will communicate the expected message
volume to the service team.

<span class="gu">## Status</span>

accepted

<span class="gu">## Consequences</span>

Due to the decision, knowledge for the use of RabbitMQ needs
to be developed within the software team. The service team
will support the software team in the integration process.
The intended use of asynchronous messages between the components
of the application can be implemented.
</code></pre></div></div>

<p>With such a document, it is also possible to understand retrospectively why <code class="language-plaintext highlighter-rouge">RabbitMQ</code> was chosen instead of, for example, <code class="language-plaintext highlighter-rouge">Azure Service Bus</code>.</p>

<h2 id="tools">Tools</h2>

<p>As ADRs (Architectural Decision Records) represent a structured form of documentation, a <a href="https://adr.github.io/#decision-capturing-tools">variety of tools</a> have been established for their implementation.</p>

<h3 id="adr-tools">adr-tools</h3>

<p><a href="http://www.natpryce.com">Nat Pryce</a> has developed <a href="https://github.com/npryce/adr-tools">adr-tools</a>, a command-line tool for working with ADRs. This tool allows for the creation of new ADRs in the format described by Michael Nygard.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>adr init doc/adrs
doc/adrs/0001-record-architecture-decisions.md
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat </span>doc/adrs/0001-record-architecture-decisions.md

<span class="c"># 1. Record architecture decisions</span>

Date: 2023-12-28

<span class="c">## Status</span>

Accepted

<span class="c">## Context</span>

We need to record the architectural decisions made on this project.

<span class="c">## Decision</span>

We will use Architecture Decision Records, as <span class="o">[</span>described by Michael Nygard]
<span class="o">(</span>http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions<span class="o">)</span><span class="nb">.</span>

<span class="c">## Consequences</span>

See Michael Nygard<span class="s1">'s article, linked above. For a lightweight ADR toolset,
see Nat Pryce'</span>s <span class="o">[</span>adr-tools]<span class="o">(</span>https://github.com/npryce/adr-tools<span class="o">)</span><span class="nb">.</span>
</code></pre></div></div>

<p>New ADRs can be created using the command <code class="language-plaintext highlighter-rouge">adr new</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>adr new Write Blog Post about ADRs
doc/adrs/0002-write-blog-post-about-adrs.md
</code></pre></div></div>

<p>Additional commands allow for linking ADRs, listing them, and generating table of contents for ADRs.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>adr generate toc
<span class="c"># Architecture Decision Records</span>

<span class="k">*</span> <span class="o">[</span>1. Record architecture decisions]<span class="o">(</span>0001-record-architecture-decisions.md<span class="o">)</span>
<span class="k">*</span> <span class="o">[</span>2. Write Blog Post about ADRs]<span class="o">(</span>0002-write-blog-post-about-adrs.md<span class="o">)</span>
</code></pre></div></div>

<h3 id="adr-viewer">adr-viewer</h3>

<p><a href="https://github.com/mrwilson/adr-viewer">adr-viewer</a> generates an HTML page from ADR files. This allows you to integrate ADRs into your CI/CD pipeline and automatically update the documentation.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pip <span class="nb">install </span>adr-viewer

<span class="nv">$ </span>adr-viewer <span class="nt">--adr-path</span> doc/adrs <span class="nt">--output</span> doc/adrs.html
</code></pre></div></div>

<p>The generated page provides an overview of all ADRs and allows navigation between individual documents. Different statuses are highlighted with distinct colors, offering a visual indication.</p>

<picture><source srcset="/generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-400-b64a01c4d.webp 400w, /generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-600-b64a01c4d.webp 600w, /generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-800-b64a01c4d.webp 800w, /generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-1000-b64a01c4d.webp 1000w" type="image/webp" /><source srcset="/generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-400-2c24ef44c.png 400w, /generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-600-2c24ef44c.png 600w, /generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-800-2c24ef44c.png 800w, /generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-1000-2c24ef44c.png 1000w" type="image/png" /><img src="/generated/2023-12-28-lightweight-architecture-documentation-adr/adr-viewer-800-2c24ef44c.png" alt="ADR Viewer" /></picture>]]></content><author><name>David</name></author><category term="adr" /><category term="architecture" /><category term="documentation" /><category term="en" /><summary type="html"><![CDATA[When a software application reaches a certain size, documentation of the individual components and their interactions makes sense. Recording architectural decisions is also important to be able to understand why a particular solution was chosen even months or years later. Future colleagues and your future self will thank you for it. A lightweight method for documenting architectural decisions is through Architectural Decision Records, ADRs.]]></summary></entry><entry xml:lang="en"><title type="html">Architecture Refactoring with ArchUnitNET</title><link href="https://www.production-ready.de/2023/12/10/architecture-refactoring-with-archunitnet-en.html" rel="alternate" type="text/html" title="Architecture Refactoring with ArchUnitNET" /><published>2023-12-10T22:40:00+01:00</published><updated>2023-12-10T22:40:00+01:00</updated><id>https://www.production-ready.de/2023/12/10/architecture-refactoring-with-archunitnet-en</id><content type="html" xml:base="https://www.production-ready.de/2023/12/10/architecture-refactoring-with-archunitnet-en.html"><![CDATA[<p>Besides the functional requirements of a software product, which can be ensured through <a href="/2023/06/10/property-based-testing-in-csharp-en.html">unit and integration tests</a>, there are also a number of non-functional requirements. Aspects of security or the performance of an application can be covered by end-to-end tests. To ensure the maintainability and extensibility of software, in addition to <a href="/2023/04/29/analyse-terraform-files-with-tfsec-en.html">static code analysis</a>, automated architecture tests can be used. These tests can verify dependencies between components or the adherence to naming conventions.</p>

<p>Even when refactoring existing code, such architecture tests can prove to be meaningful.</p>

<p>With <a href="https://archunitnet.readthedocs.io/en/latest/">ArchUnitNET</a>, architecture rules can be defined as code and tested automatically.</p>

<!--more-->

<h2 id="archunitnet">ArchUnitNET</h2>

<p>ArchUnitNET is a .NET port of the well-established Java library <a href="https://www.archunit.org">ArchUnit</a>. With ArchUnitNET, it is possible to define architecture rules as unit tests and continuously and automatically verify them. The rules are written in C# code and can be executed in any testing framework. To use ArchUnitNET, it is sufficient to add the corresponding <a href="https://www.nuget.org/packages/TngTech.ArchUnitNET">NuGet package</a> to your project. For easy integration with different test frameworks, packages are available for <a href="https://www.nuget.org/packages/TngTech.ArchUnitNET.xUnit">xUnit</a>, <a href="https://www.nuget.org/packages/TngTech.ArchUnitNET.NUnit">NUnit</a>, and <a href="https://www.nuget.org/packages/TngTech.ArchUnitNET.MSTestV2">MS-Test</a>.</p>

<h3 id="architecture-rules">Architecture Rules</h3>

<p>ArchUnitNET tests are structured similarly to other unit tests. There is a test class with a series of test methods. For better performance, one can create a static class property with the definition of the architecture.</p>

<p>To achieve this, you make the <code class="language-plaintext highlighter-rouge">ArchLoader</code> aware of a series of assemblies that contain the code related to your architecture. Marker classes, for example, can be used for this purpose. However, the <code class="language-plaintext highlighter-rouge">ArchLoader</code> also has a variety of other methods, such as loading assemblies from a directory.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Assembly</span> <span class="n">DataModuleAssembly</span> <span class="p">=</span> 
    <span class="k">typeof</span><span class="p">(</span><span class="n">DataModuleMarker</span><span class="p">).</span><span class="n">Assembly</span><span class="p">;</span>
<span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Assembly</span> <span class="n">BusinessModuleAssembly</span> <span class="p">=</span> 
    <span class="k">typeof</span><span class="p">(</span><span class="n">BusinessModuleMarker</span><span class="p">).</span><span class="n">Assembly</span><span class="p">;</span>
<span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Assembly</span> <span class="n">DesktopModuleAssembly</span> <span class="p">=</span> 
    <span class="k">typeof</span><span class="p">(</span><span class="n">DesktopModuleMarker</span><span class="p">).</span><span class="n">Assembly</span><span class="p">;</span>

<span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Architecture</span> <span class="n">Architecture</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ArchLoader</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">LoadAssemblies</span><span class="p">(</span><span class="n">DataModuleAssembly</span><span class="p">,</span> <span class="n">BusinessModuleAssembly</span><span class="p">,</span> <span class="n">DesktopModuleAssembly</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<p>Within the architecture, sets of classes, interfaces, or methods can be formed, for example, to group classes into layers. If you use a consistent naming scheme, you can also filter by name patterns and, for instance, aggregate all repository classes together.</p>

<p>The user-friendly Fluent API of ArchUnitNET helps defining these sets.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">readonly</span> <span class="n">IObjectProvider</span><span class="p">&lt;</span><span class="n">IType</span><span class="p">&gt;</span> <span class="n">DataLayer</span> <span class="p">=</span>
    <span class="nf">Types</span><span class="p">().</span><span class="nf">That</span><span class="p">().</span><span class="nf">ResideInAssembly</span><span class="p">(</span><span class="n">DataModuleAssembly</span><span class="p">).</span><span class="nf">As</span><span class="p">(</span><span class="s">"Data layer"</span><span class="p">);</span>

<span class="k">private</span> <span class="k">readonly</span> <span class="n">IObjectProvider</span><span class="p">&lt;</span><span class="n">IType</span><span class="p">&gt;</span> <span class="n">BusinessLayer</span> <span class="p">=</span>
    <span class="nf">Types</span><span class="p">().</span><span class="nf">That</span><span class="p">().</span><span class="nf">ResideInAssembly</span><span class="p">(</span><span class="n">BusinessModuleAssembly</span><span class="p">).</span><span class="nf">As</span><span class="p">(</span><span class="s">"Business layer"</span><span class="p">);</span>

<span class="k">private</span> <span class="k">readonly</span> <span class="n">IObjectProvider</span><span class="p">&lt;</span><span class="n">Class</span><span class="p">&gt;</span> <span class="n">RepositoryClasses</span> <span class="p">=</span>
    <span class="nf">Classes</span><span class="p">().</span><span class="nf">That</span><span class="p">().</span><span class="nf">HaveNameEndingWith</span><span class="p">(</span><span class="s">"Repository"</span><span class="p">).</span><span class="nf">As</span><span class="p">(</span><span class="s">"Repository classes"</span><span class="p">);</span>
</code></pre></div></div>

<p>To test whether all repository classes are indeed located in the data layer, we write a test that defines a corresponding rule and checks our architecture against it. For ease of use, you can include <code class="language-plaintext highlighter-rouge">ArchRuleDefinition</code> as a <code class="language-plaintext highlighter-rouge">static using</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">static</span> <span class="n">ArchUnitNET</span><span class="p">.</span><span class="n">Fluent</span><span class="p">.</span><span class="n">ArchRuleDefinition</span><span class="p">;</span>

<span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">RepositoryClassesShouldBeInDataLayer</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">rule</span> <span class="p">=</span> <span class="nf">Classes</span><span class="p">().</span><span class="nf">That</span><span class="p">().</span><span class="nf">Are</span><span class="p">(</span><span class="n">RepositoryClasses</span><span class="p">).</span><span class="nf">Should</span><span class="p">().</span><span class="nf">Be</span><span class="p">(</span><span class="n">DataLayer</span><span class="p">);</span>
    <span class="n">rule</span><span class="p">.</span><span class="nf">Check</span><span class="p">(</span><span class="n">Architecture</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Checking for intentional or unintentional dependencies between modules also requires only a few lines of code. The following test verifies whether any type from the desktop layer accesses a type from the data layer.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">DesktopLayerShouldNotReferenceDataLayer</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">rule</span> <span class="p">=</span> <span class="nf">Types</span><span class="p">().</span><span class="nf">That</span><span class="p">().</span><span class="nf">Are</span><span class="p">(</span><span class="n">DesktopLayer</span><span class="p">).</span><span class="nf">Should</span><span class="p">().</span><span class="nf">NotDependOnAny</span><span class="p">(</span><span class="n">DataLayer</span><span class="p">);</span>
    <span class="n">rule</span><span class="p">.</span><span class="nf">Check</span><span class="p">(</span><span class="n">Architecture</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If ArchUnitNET detects a violation of our rules, the test fails and provides an appropriate error message.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FailedArchRuleException
"Types that are Desktop layer should not depend on Data layer" failed:
    DesktopModule.ViewModels.ProductListViewModel does depend on
    DataModule.ProductRepository
</code></pre></div></div>

<h3 id="preventing-code-usage">Preventing Code Usage</h3>

<p>Not only can the organization of source code be enforced in this way, but the usage of specific classes or methods can also be regulated. To ensure that only the Data Layer has access to a database, we restrict access to classes from the <code class="language-plaintext highlighter-rouge">Microsoft.Data.SqlClient</code> assembly, which includes the <code class="language-plaintext highlighter-rouge">SqlConnection</code> class, among others. Access to the database classes is only allowed from our own assemblies (<code class="language-plaintext highlighter-rouge">ProjectAssemblies</code>), specifically from within the <code class="language-plaintext highlighter-rouge">DataLayer</code>. To make ArchUnitNET aware of the <code class="language-plaintext highlighter-rouge">Microsoft.Data.SqlClient</code> assembly, we need to explicitly include it in our architecture definition.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Assembly</span><span class="p">[]</span> <span class="n">ProjectAssemblies</span> <span class="p">=</span> <span class="p">{</span>
    <span class="n">DataModuleAssembly</span><span class="p">,</span>
    <span class="n">BusinessModuleAssembly</span><span class="p">,</span>
    <span class="n">DesktopModuleAssembly</span>
<span class="p">};</span>

<span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Assembly</span> <span class="n">SystemDataAssembly</span> <span class="p">=</span>
    <span class="k">typeof</span><span class="p">(</span><span class="n">SqlConnection</span><span class="p">).</span><span class="n">Assembly</span><span class="p">;</span>

<span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Architecture</span> <span class="n">Architecture</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ArchLoader</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">LoadAssemblies</span><span class="p">(</span><span class="cm">/* ... */</span> <span class="n">SystemDataAssembly</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">OnlyDataLayerShouldUseDatabase</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">types</span> <span class="p">=</span> <span class="nf">Types</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">That</span><span class="p">().</span><span class="nf">ResideInAssembly</span><span class="p">(</span><span class="n">ProjectAssemblies</span><span class="p">[</span><span class="m">0</span><span class="p">],</span> <span class="n">ProjectAssemblies</span><span class="p">[</span><span class="m">1</span><span class="p">..])</span>
        <span class="p">.</span><span class="nf">And</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">AreNot</span><span class="p">(</span><span class="n">DataLayer</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">typesNotToUse</span> <span class="p">=</span> <span class="nf">Types</span><span class="p">().</span><span class="nf">That</span><span class="p">().</span><span class="nf">ResideInAssembly</span><span class="p">(</span><span class="n">SystemDataAssembly</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">rule</span> <span class="p">=</span> <span class="n">types</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">NotDependOnAny</span><span class="p">(</span><span class="n">typesNotToUse</span><span class="p">);</span>
    <span class="n">rule</span><span class="p">.</span><span class="nf">Check</span><span class="p">(</span><span class="n">Architecture</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="architecture-refactoring">Architecture Refactoring</h2>

<p>When dealing with an existing codebase and intending to make changes to it, ArchUnitNET allows you to define the desired target state before implementing the corresponding changes. Subsequently, you have the option to gradually transform the existing architecture in the desired direction. At the beginning of the refactoring process, the tests that are still red indicate the areas that need attention. Rules that haven’t been implemented yet can be skipped for the time being. Once all tests are reactivated and green, the refactoring is complete.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Fact</span><span class="p">(</span><span class="n">Skip</span> <span class="p">=</span> <span class="s">"Refactoring of DesktopLayer will be done in the next iteration"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">DesktopLayerShouldNotReferenceDataLayer</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">rule</span> <span class="p">=</span> <span class="nf">Types</span><span class="p">().</span><span class="nf">That</span><span class="p">().</span><span class="nf">Are</span><span class="p">(</span><span class="n">DesktopLayer</span><span class="p">).</span><span class="nf">Should</span><span class="p">().</span><span class="nf">NotDependOnAny</span><span class="p">(</span><span class="n">DataLayer</span><span class="p">);</span>
    <span class="n">rule</span><span class="p">.</span><span class="nf">Check</span><span class="p">(</span><span class="n">Architecture</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="finding-system-resources">Finding System Resources</h3>

<p>The use of non-deterministic system resources such as the current time can make writing tests challenging. If you don’t want to refactor directly towards <a href="/2023/09/28/refactor-to-purity-en.html">Pure Functions</a>, it can be helpful to use an abstraction like the <a href="https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider">TimeProvider</a> introduced since <a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#time-abstraction">.NET 8</a>.</p>

<p>Here, too, a test can help us find all accesses to <code class="language-plaintext highlighter-rouge">DateTime.Now</code> or <code class="language-plaintext highlighter-rouge">DateTime.UtcNow</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">DateTimeNowShouldNotBeUsedInBusinessLayer</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">types</span> <span class="p">=</span> <span class="nf">Types</span><span class="p">().</span><span class="nf">That</span><span class="p">().</span><span class="nf">Are</span><span class="p">(</span><span class="n">BusinessLayer</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">methodsNotToCall</span> <span class="p">=</span> <span class="nf">MethodMembers</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">That</span><span class="p">().</span><span class="nf">AreDeclaredIn</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">DateTime</span><span class="p">))</span>
        <span class="p">.</span><span class="nf">And</span><span class="p">().</span><span class="nf">AreStatic</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">And</span><span class="p">().</span><span class="nf">HaveNameEndingWith</span><span class="p">(</span><span class="s">"get_UtcNow()"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">Or</span><span class="p">().</span><span class="nf">HaveNameEndingWith</span><span class="p">(</span><span class="s">"get_Now()"</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">rule</span> <span class="p">=</span> <span class="n">types</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">NotCallAny</span><span class="p">(</span><span class="n">methodsNotToCall</span><span class="p">);</span>
    <span class="n">rule</span><span class="p">.</span><span class="nf">Check</span><span class="p">(</span><span class="n">Architecture</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A failing test will once again indicate to us where we need to make adjustments in this case.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"Types that are Business layer should not call Method members that are declared
in "System.DateTime" and are static and have name ending with "get_UtcNow()"" failed:
    BusinessModule.ProductService does call System.DateTime
    System.DateTime::get_UtcNow()
</code></pre></div></div>

<p>Kudos to <a href="https://www.linkedin.com/in/andreas-lausen/">Andreas Lausen</a> for his <a href="https://github.com/andreaslausen/seacon23">sample code</a> from <a href="https://www.sea-con.de/seacon2023">SEACON 2023</a>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>ArchUnitNET provides a powerful way to define architecture and code usage rules in the form of unit tests and to automatically and continuously verify them. After a short learning curve, writing new rules becomes straightforward. Since these architecture rules behave like any other unit test, they can also be integrated into existing codebases. The best practice is, of course, to define and use them as early as possible.</p>

<blockquote>
  <p>The example code for this post can be found on GitHub: <a href="https://github.com/davull/demo-archunit">https://github.com/davull/demo-archunit</a></p>
</blockquote>]]></content><author><name>David</name></author><category term="refactoring" /><category term="test" /><category term="archunitnet" /><category term="en" /><summary type="html"><![CDATA[Besides the functional requirements of a software product, which can be ensured through unit and integration tests, there are also a number of non-functional requirements. Aspects of security or the performance of an application can be covered by end-to-end tests. To ensure the maintainability and extensibility of software, in addition to static code analysis, automated architecture tests can be used. These tests can verify dependencies between components or the adherence to naming conventions. Even when refactoring existing code, such architecture tests can prove to be meaningful. With ArchUnitNET, architecture rules can be defined as code and tested automatically.]]></summary></entry><entry xml:lang="en"><title type="html">Uptime Kuma in Azure Web App for Containers</title><link href="https://www.production-ready.de/2023/11/11/kuma-azure-web-app-containers-en.html" rel="alternate" type="text/html" title="Uptime Kuma in Azure Web App for Containers" /><published>2023-11-11T23:30:00+01:00</published><updated>2023-11-11T23:30:00+01:00</updated><id>https://www.production-ready.de/2023/11/11/kuma-azure-web-app-containers-en</id><content type="html" xml:base="https://www.production-ready.de/2023/11/11/kuma-azure-web-app-containers-en.html"><![CDATA[<p><a href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a> is a self-hosted monitoring system that allows you to monitor the availability of various services. It offers a variety of monitoring types, including those for different types of HTTP endpoints such as websites or REST APIs.</p>

<p>As it is also available as a ready-made Docker image, you can easily deploy it as an Azure App Service. For rolling it out using Terraform, a simple configuration is sufficient.</p>

<!--more-->

<blockquote>
  <p>The Terraform code for this post can be found on Github: <a href="https://github.com/davull/uptime-kuma-azure-app-service">https://github.com/davull/uptime-kuma-azure-app-service</a></p>
</blockquote>

<h2 id="azure-app-service">Azure App Service</h2>

<p>To run a Docker container in Azure as a <a href="https://azure.microsoft.com/en-us/products/app-service/containers/">Web App for Container</a>, we need an <a href="https://azure.microsoft.com/en-us/products/app-service/">App Service</a> that resides in an <a href="https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans">App Service Plan</a>. Alternatively, containers can also be run in <code class="language-plaintext highlighter-rouge">Azure Container App</code> or <code class="language-plaintext highlighter-rouge">Azure Kubernetes Service</code>, an overview is provided by Microsoft <a href="https://azure.microsoft.com/en-us/products/category/containers/">here</a>.</p>

<p>To ensure that the data collected by Uptime Kuma (an SQLite database) is not lost with each container restart, we persist it in a <a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview">Storage Account</a>.</p>

<h3 id="resource-group">Resource Group</h3>

<p>To organize resources in Azure, we create a new Resource Group. To do this, we search for <code class="language-plaintext highlighter-rouge">Resource Group</code> in the <a href="https://portal.azure.com/#view/Microsoft_Azure_Marketplace/MarketplaceOffersBlade">Azure Marketplace</a> and click <code class="language-plaintext highlighter-rouge">Create</code>.</p>

<picture><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-400-a40e06a27.webp 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-600-a40e06a27.webp 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-800-a40e06a27.webp 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-1000-a40e06a27.webp 1000w" type="image/webp" /><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-400-7f92fdcb3.png 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-600-7f92fdcb3.png 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-800-7f92fdcb3.png 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-1000-7f92fdcb3.png 1000w" type="image/png" /><img src="/generated/2023-11-11-kuma-azure-web-app-containers/create-resource-group-800-7f92fdcb3.png" alt="Create Resource Group" /></picture>

<p>We assign a name, choose a region, and click <code class="language-plaintext highlighter-rouge">Review + Create</code>. Afterward, we can create our services within the Resource Group.</p>

<h3 id="storage-account">Storage Account</h3>

<p>When creating the <code class="language-plaintext highlighter-rouge">Storage Account</code>, we select the created Resource Group, set the name, region, and redundancy level. For non-critical applications, the most cost-effective option is typically <code class="language-plaintext highlighter-rouge">Local-redundant storage (LRS)</code>. The remaining settings can be kept as default.</p>

<picture><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-400-152ceafa2.webp 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-600-152ceafa2.webp 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-800-152ceafa2.webp 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-1000-152ceafa2.webp 1000w" type="image/webp" /><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-400-622231cbe.png 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-600-622231cbe.png 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-800-622231cbe.png 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-1000-622231cbe.png 1000w" type="image/png" /><img src="/generated/2023-11-11-kuma-azure-web-app-containers/create-storage-account-800-622231cbe.png" alt="Create Storage Account" /></picture>

<p>Once our Storage Account is provisioned, we can create a <code class="language-plaintext highlighter-rouge">File share</code>. For the tier, we choose <code class="language-plaintext highlighter-rouge">Hot</code>, and the backup option can be enabled or disabled based on our needs.</p>

<picture><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-400-224cb8878.webp 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-600-224cb8878.webp 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-800-224cb8878.webp 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-1000-224cb8878.webp 1000w" type="image/webp" /><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-400-dc7bd171d.png 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-600-dc7bd171d.png 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-800-dc7bd171d.png 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-1000-dc7bd171d.png 1000w" type="image/png" /><img src="/generated/2023-11-11-kuma-azure-web-app-containers/create-file-share-800-dc7bd171d.png" alt="Create File Share" /></picture>

<h3 id="app-service-plan-und-web-app-for-container">App Service Plan und Web App for Container</h3>

<p>To create an <code class="language-plaintext highlighter-rouge">App Service Plan</code>, we follow a similar process. As the <code class="language-plaintext highlighter-rouge">Operation System</code>, we choose <code class="language-plaintext highlighter-rouge">Linux</code>, and the smallest non-free pricing plan <code class="language-plaintext highlighter-rouge">Basic B1</code> is sufficient. This plan costs approximately €13 per month and provides 1.75 GB of memory and 1 vCPU. Depending on the number of services to be monitored, it might be advisable to choose a <a href="https://azure.microsoft.com/en-us/pricing/details/app-service/linux/#pricing">larger plan</a>. However, scaling up can also be done at any time later.</p>

<picture><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-400-3ba6718b2.webp 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-600-3ba6718b2.webp 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-800-3ba6718b2.webp 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-945-3ba6718b2.webp 945w" type="image/webp" /><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-400-c1160a689.png 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-600-c1160a689.png 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-800-c1160a689.png 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-945-c1160a689.png 945w" type="image/png" /><img src="/generated/2023-11-11-kuma-azure-web-app-containers/create-app-service-plan-800-c1160a689.png" alt="Create App Service Plan" /></picture>

<p>Now that an App Service Plan and Storage are available, we can create the actual App Service. To start the wizard, we search for <code class="language-plaintext highlighter-rouge">Web App for Containers</code> or <code class="language-plaintext highlighter-rouge">Web App</code> in the Azure Portal. Although they are listed separately in the Marketplace, they represent the same service.</p>

<p>As the Publish method, we choose <code class="language-plaintext highlighter-rouge">Docker</code>, and for the Operating System, we select <code class="language-plaintext highlighter-rouge">Linux</code>. The name chosen here will also be the subdomain under which the service will be accessible later (&lt;name&gt;.azurewebsites.net).</p>

<picture><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-400-76a86039a.webp 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-600-76a86039a.webp 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-800-76a86039a.webp 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-924-76a86039a.webp 924w" type="image/webp" /><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-400-028b71c79.png 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-600-028b71c79.png 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-800-028b71c79.png 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-924-028b71c79.png 924w" type="image/png" /><img src="/generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-1-800-028b71c79.png" alt="Create Web App" /></picture>

<p>In the next tab, <code class="language-plaintext highlighter-rouge">Docker</code>, we switch the Image Source to <code class="language-plaintext highlighter-rouge">Docker Hub</code> and enter the <a href="https://hub.docker.com/r/louislam/uptime-kuma">desired image</a> with the tag, for example, <code class="language-plaintext highlighter-rouge">louislam/uptime-kuma:latest</code>.</p>

<picture><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-400-ba4bcd61a.webp 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-600-ba4bcd61a.webp 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-800-ba4bcd61a.webp 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-920-ba4bcd61a.webp 920w" type="image/webp" /><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-400-690ef7094.png 400w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-600-690ef7094.png 600w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-800-690ef7094.png 800w, /generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-920-690ef7094.png 920w" type="image/png" /><img src="/generated/2023-11-11-kuma-azure-web-app-containers/create-web-app-step-2-800-690ef7094.png" alt="Create Web App" /></picture>

<p>After completing the wizard, Azure pulls the Docker image from the Hub and starts our container. During the first launch of Uptime Kuma, it takes some time for the service to be up and running and the website to be accessible. You can track the process under the <code class="language-plaintext highlighter-rouge">Deployment Center</code> entry in the <code class="language-plaintext highlighter-rouge">Logs</code> tab.</p>

<h4 id="mount-azure-file-share">Mount Azure File Share</h4>

<p>To ensure that the Uptime Kuma database is not lost with each container restart, we mount the previously created File Share into the container. To do this, select the <code class="language-plaintext highlighter-rouge">Configuration</code> section and go to the <code class="language-plaintext highlighter-rouge">Path mappings</code> tab. Add a new entry, choosing the previously created Storage Account and the Storage Container created for the File Share. Select <code class="language-plaintext highlighter-rouge">Azure File</code> as the Storage Type. The path under which the File Share is mounted in the container should be <code class="language-plaintext highlighter-rouge">/app/data</code>. This is where Uptime Kuma stores its application data. After saving, the container will be restarted, and the data will now be stored in our Storage Container.</p>

<picture><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/add-path-mapping-400-5a965269d.webp 400w, /generated/2023-11-11-kuma-azure-web-app-containers/add-path-mapping-600-5a965269d.webp 600w, /generated/2023-11-11-kuma-azure-web-app-containers/add-path-mapping-797-5a965269d.webp 797w" type="image/webp" /><source srcset="/generated/2023-11-11-kuma-azure-web-app-containers/add-path-mapping-400-354da946e.png 400w, /generated/2023-11-11-kuma-azure-web-app-containers/add-path-mapping-600-354da946e.png 600w, /generated/2023-11-11-kuma-azure-web-app-containers/add-path-mapping-797-354da946e.png 797w" type="image/png" /><img src="/generated/2023-11-11-kuma-azure-web-app-containers/add-path-mapping-797-354da946e.png" alt="Add Path Mapping" /></picture>

<p>When we now access the website of the Web App, Uptime Kuma greets us with the setup dialog, where we can create the admin user.</p>

<h2 id="terraform">Terraform</h2>

<p>As an alternative to the Azure Portal, managing the configuration of our application in a true Infrastructure as Code manner using Terraform is a great option. For this, we can utilize the <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest">azurerm Provider</a>. The various authentication methods are described in the <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs">documentation</a>. We create the same resources as we did in the portal using Terraform.</p>

<p>The configuration file looks like this:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">azurerm</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="o">=</span> <span class="s2">"hashicorp/azurerm"</span>
      <span class="nx">version</span> <span class="o">=</span> <span class="s2">"~&gt; 3.79"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">provider</span> <span class="s2">"azurerm"</span> <span class="p">{</span>
  <span class="nx">features</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_resource_group"</span> <span class="s2">"rg_kuma"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">resource_group_name</span>
  <span class="nx">location</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">location</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_storage_account"</span> <span class="s2">"sa_kuma"</span> <span class="p">{</span>
  <span class="nx">name</span>                     <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">storage_account_name</span>
  <span class="nx">resource_group_name</span>      <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">rg_kuma</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>                 <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">rg_kuma</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">account_tier</span>             <span class="o">=</span> <span class="s2">"Standard"</span>
  <span class="nx">account_replication_type</span> <span class="o">=</span> <span class="s2">"LRS"</span>
  <span class="nx">min_tls_version</span>          <span class="o">=</span> <span class="s2">"TLS1_2"</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_storage_share"</span> <span class="s2">"share_kuma"</span> <span class="p">{</span>
  <span class="nx">name</span>                 <span class="o">=</span> <span class="s2">"kumashare"</span>
  <span class="nx">storage_account_name</span> <span class="o">=</span> <span class="nx">azurerm_storage_account</span><span class="p">.</span><span class="nx">sa_kuma</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">quota</span>                <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">storage_quota</span>
  <span class="nx">access_tier</span>          <span class="o">=</span> <span class="s2">"Hot"</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_service_plan"</span> <span class="s2">"service_plan_kuma"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"service-plan-kuma"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">rg_kuma</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">rg_kuma</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">os_type</span>             <span class="o">=</span> <span class="s2">"Linux"</span>
  <span class="nx">sku_name</span>            <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">service_plan_sku</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_linux_web_app"</span> <span class="s2">"web_app_kuma"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">web_app_name</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">rg_kuma</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">rg_kuma</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">service_plan_id</span>     <span class="o">=</span> <span class="nx">azurerm_service_plan</span><span class="p">.</span><span class="nx">service_plan_kuma</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">https_only</span>          <span class="o">=</span> <span class="kc">true</span>

  <span class="nx">site_config</span> <span class="p">{</span>
    <span class="nx">http2_enabled</span>       <span class="o">=</span> <span class="kc">true</span>
    <span class="nx">minimum_tls_version</span> <span class="o">=</span> <span class="s2">"1.2"</span>

    <span class="nx">application_stack</span> <span class="p">{</span>
      <span class="nx">docker_image_name</span>   <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">docker_image</span>
      <span class="nx">docker_registry_url</span> <span class="o">=</span> <span class="s2">"https://index.docker.io"</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">app_settings</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"DOCKER_ENABLE_CI"</span> <span class="p">=</span> <span class="s2">"true"</span>
  <span class="p">}</span>

  <span class="nx">storage_account</span> <span class="p">{</span>
    <span class="nx">access_key</span>   <span class="o">=</span> <span class="nx">azurerm_storage_account</span><span class="p">.</span><span class="nx">sa_kuma</span><span class="p">.</span><span class="nx">primary_access_key</span>
    <span class="nx">account_name</span> <span class="o">=</span> <span class="nx">azurerm_storage_account</span><span class="p">.</span><span class="nx">sa_kuma</span><span class="p">.</span><span class="nx">name</span>
    <span class="nx">name</span>         <span class="o">=</span> <span class="s2">"webappstorage"</span>
    <span class="nx">type</span>         <span class="o">=</span> <span class="s2">"AzureFiles"</span>
    <span class="nx">share_name</span>   <span class="o">=</span> <span class="nx">azurerm_storage_share</span><span class="p">.</span><span class="nx">share_kuma</span><span class="p">.</span><span class="nx">name</span>
    <span class="nx">mount_path</span>   <span class="o">=</span> <span class="s2">"/app/data"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The variables are defined in a separate file.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"resource_group_name"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"storage_account_name"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"web_app_name"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"location"</span> <span class="p">{</span>
  <span class="nx">type</span>    <span class="o">=</span> <span class="nx">string</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"westeurope"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"storage_quota"</span> <span class="p">{</span>
  <span class="nx">type</span>    <span class="o">=</span> <span class="nx">number</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="mi">50</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"service_plan_sku"</span> <span class="p">{</span>
  <span class="nx">type</span>    <span class="o">=</span> <span class="nx">string</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"B1"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"docker_image"</span> <span class="p">{</span>
  <span class="nx">type</span>    <span class="o">=</span> <span class="nx">string</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"louislam/uptime-kuma:latest"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In a <code class="language-plaintext highlighter-rouge">.tfvar</code> file, the values for the variables are set.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource_group_name</span>  <span class="o">=</span> <span class="s2">"rg-kuma"</span>
<span class="nx">storage_account_name</span> <span class="o">=</span> <span class="s2">"&lt;your-storage-account-name&gt;"</span>
<span class="nx">web_app_name</span>         <span class="o">=</span> <span class="s2">"&lt;your-web-app-name&gt;"</span>
</code></pre></div></div>

<p>Now, with a single command, we can create the entire environment and also delete it when needed.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terraform apply <span class="nt">-var-file</span><span class="o">=</span><span class="s2">"terraform.tfvar"</span>
terraform destroy <span class="nt">-var-file</span><span class="o">=</span><span class="s2">"terraform.tfvar"</span>
</code></pre></div></div>]]></content><author><name>David</name></author><category term="kuma" /><category term="azure" /><category term="container" /><category term="en" /><summary type="html"><![CDATA[Uptime Kuma is a self-hosted monitoring system that allows you to monitor the availability of various services. It offers a variety of monitoring types, including those for different types of HTTP endpoints such as websites or REST APIs. As it is also available as a ready-made Docker image, you can easily deploy it as an Azure App Service. For rolling it out using Terraform, a simple configuration is sufficient.]]></summary></entry><entry xml:lang="en"><title type="html">Refactor to Purity</title><link href="https://www.production-ready.de/2023/09/28/refactor-to-purity-en.html" rel="alternate" type="text/html" title="Refactor to Purity" /><published>2023-09-28T00:00:00+02:00</published><updated>2023-09-28T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/09/28/refactor-to-purity-en</id><content type="html" xml:base="https://www.production-ready.de/2023/09/28/refactor-to-purity-en.html"><![CDATA[<p><code class="language-plaintext highlighter-rouge">Pure Functions</code> are program methods that can be executed without causing side effects. In functional programming, they are more of a rule than an exception. However, in most object-oriented languages, you encounter them less often, or at least they are not frequently considered the preferred approach. In the <a href="https://dotnet.microsoft.com/en-us/">dotnet</a> environment, much emphasis is placed on <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection">Dependency Injection</a> and more or less extensive abstractions using interfaces.</p>

<p>The following article will demonstrate how to transition from a codebase with many such indirections to a simpler version that removes a lot of unnecessary complexity.</p>

<!--more-->

<h2 id="initial-situation">Initial Situation</h2>

<p>As a starting point for our refactoring, let’s consider a fictional example of an online shop. The <a href="https://github.com/davull/demo-refactor-to-purity">source code</a> is available on GitHub, and there’s a separate branch in the repository for each refactoring step.</p>

<blockquote>
  <p>Source code on GitHub, Branch <a href="https://github.com/davull/demo-refactor-to-purity/tree/steps/01-initial-state">steps/01-initial-state</a></p>
</blockquote>

<p>The application consists of an application project and a project for associated tests. The structure doesn’t strictly adhere to an architectural style but is meant to illustrate the components you might expect in such a system. Additionally, we will focus on the backend side of our online shop here. The folder structure looks like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>├───Refactor.Application
│   ├───Controllers
│   ├───CQRS
│   │   ├───Handlers
│   │   └───Requests
│   ├───Data
│   ├───Models
│   ├───Repositories
│   │   ├───Implementations
│   │   └───Interfaces
│   └───Services
└───Refactor.Application.Test
    ├───Controllers
    ├───CQRS
    │   └───Handlers
    ├───Repositories
    └───Services
</code></pre></div></div>

<p>The application is written in C# and uses ASP.NET controllers. Business logic is implemented in service classes, and domain models are located in the <code class="language-plaintext highlighter-rouge">Models</code> folder. Database access is done through the <a href="https://medium.com/@pererikbergman/repository-design-pattern-e28c0f3e4a30">Repository pattern</a>, and the <a href="https://en.wikipedia.org/wiki/Plain_old_CLR_object">POCO</a> classes for the database are placed in the <code class="language-plaintext highlighter-rouge">Data</code> folder. For communication between controllers and services, the <a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs">CQRS (Command Query Responsibility Segregation)</a> pattern is employed.</p>

<p>All these individual components are managed and wired together using <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection">Dependency Injection</a>.</p>

<h3 id="abstraction">Abstraction</h3>

<p>In software development, abstraction is a commonly used concept. However, it often falls short of its primary goal, which is to reduce complexity and maintenance efforts. Additionally, abstraction layers are frequently introduced without providing concrete benefits but rather because “that’s how things are done.” This not only impacts code readability but also makes it challenging to understand the runtime behavior without a detailed analysis of dependencies. While the abstractions in our example might appear artificially enforced for a small demo, they are indeed encountered in real projects from time to time.</p>

<h4 id="base-classes-and-marker-interfaces">Base Classes and Marker Interfaces</h4>

<p>All our model classes inherit from an abstract base class or record called <code class="language-plaintext highlighter-rouge">ModelBase</code>, which doesn’t provide any implementation. The database POCOs implement an interface called <code class="language-plaintext highlighter-rouge">IData</code>, which at least defines a property <code class="language-plaintext highlighter-rouge">Id</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ./Models</span>
<span class="k">public</span> <span class="k">abstract</span> <span class="k">record</span> <span class="nc">ModelBase</span><span class="p">;</span>

<span class="k">public</span> <span class="k">record</span> <span class="nc">Customer</span><span class="p">(</span>
    <span class="n">Guid</span> <span class="n">Id</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">FirstName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">LastName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">Email</span><span class="p">)</span> <span class="p">:</span> <span class="n">ModelBase</span><span class="p">;</span>

<span class="c1">// ./Data</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IData</span>
<span class="p">{</span>
    <span class="n">Guid</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">record</span> <span class="nc">Customer</span><span class="p">(</span>
    <span class="n">Guid</span> <span class="n">Id</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">FirstName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">LastName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">Email</span><span class="p">,</span>
    <span class="kt">bool</span> <span class="n">Active</span><span class="p">)</span> <span class="p">:</span> <span class="n">IData</span><span class="p">;</span>
</code></pre></div></div>

<h4 id="repository-interfaces">Repository Interfaces</h4>

<p>In the <code class="language-plaintext highlighter-rouge">Repositories</code> folder, you can find both a generic interface <code class="language-plaintext highlighter-rouge">IRepository&lt;T&gt;</code> and specific interfaces for each database table or POCO class, such as <code class="language-plaintext highlighter-rouge">ICustomerRepository</code>. Additionally, there’s an abstract base class called <code class="language-plaintext highlighter-rouge">AbstractRepository&lt;T&gt;</code>, which simply implements the methods of the generic interface one-to-one.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IRepository</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="k">where</span> <span class="n">T</span> <span class="p">:</span> <span class="n">IData</span>
<span class="p">{</span>
    <span class="n">T</span> <span class="nf">Get</span><span class="p">(</span><span class="n">Guid</span> <span class="n">id</span><span class="p">);</span>
    <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="nf">GetAll</span><span class="p">();</span>
    <span class="k">void</span> <span class="nf">Add</span><span class="p">(</span><span class="n">T</span> <span class="n">entity</span><span class="p">);</span>
    <span class="p">...</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">abstract</span> <span class="k">class</span> <span class="nc">AbstractRepository</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="p">:</span> <span class="n">IRepository</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="k">where</span> <span class="n">T</span> <span class="p">:</span> <span class="n">IData</span>
<span class="p">{</span>
    <span class="k">protected</span> <span class="k">readonly</span> <span class="n">IDatabase</span> <span class="n">_database</span><span class="p">;</span>

    <span class="k">protected</span> <span class="nf">AbstractRepository</span><span class="p">(</span><span class="n">IDatabase</span> <span class="n">database</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">_database</span> <span class="p">=</span> <span class="n">database</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">abstract</span> <span class="n">T</span> <span class="nf">Get</span><span class="p">(</span><span class="n">Guid</span> <span class="n">id</span><span class="p">);</span>
    <span class="k">public</span> <span class="k">abstract</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="nf">GetAll</span><span class="p">();</span>
    <span class="k">public</span> <span class="k">abstract</span> <span class="k">void</span> <span class="nf">Add</span><span class="p">(</span><span class="n">T</span> <span class="n">entity</span><span class="p">);</span>
    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Abstracting the concrete database access through an interface like <code class="language-plaintext highlighter-rouge">IDatabase</code> can be meaningful, as it allows you to replace external systems like a database with a <a href="https://en.wikipedia.org/wiki/Mock_object">mock object</a> during testing. However, we will find a different solution to this problem as we proceed.</p>

<p>In most cases, the concrete implementations of the repositories typically consist of forwarding calls to the base class or an <code class="language-plaintext highlighter-rouge">IDatabase</code> object.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">CustomerRepository</span> <span class="p">:</span> <span class="n">AbstractRepository</span><span class="p">&lt;</span><span class="n">Customer</span><span class="p">&gt;,</span> <span class="n">ICustomerRepository</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="nf">CustomerRepository</span><span class="p">(</span><span class="n">IDatabase</span> <span class="n">database</span><span class="p">)</span> <span class="p">:</span> <span class="k">base</span><span class="p">(</span><span class="n">database</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">Add</span><span class="p">(</span><span class="n">Customer</span> <span class="n">entity</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">_database</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">entity</span><span class="p">);</span>
    <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">Update</span><span class="p">(</span><span class="n">Customer</span> <span class="n">entity</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">_database</span><span class="p">.</span><span class="nf">Update</span><span class="p">(</span><span class="n">entity</span><span class="p">);</span>
    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="services-and-cqrs">Services and CQRS</h4>

<p>All our services are defined as interfaces with precisely one implementation; there is no need for multiple implementations or runtime swapping.</p>

<p>The example of <code class="language-plaintext highlighter-rouge">ITaxService</code> illustrates the frequent use of interfaces. The interface defines only a single method, which has no dependencies apart from its direct method parameters.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">ITaxService</span>
<span class="p">{</span>
    <span class="p">(</span><span class="kt">decimal</span> <span class="n">taxAmount</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">grossPrice</span><span class="p">)</span> <span class="nf">CalculateTax</span><span class="p">(</span>
        <span class="kt">decimal</span> <span class="n">netPrice</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">taxRate</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">TaxService</span> <span class="p">:</span> <span class="n">ITaxService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="p">(</span><span class="kt">decimal</span> <span class="n">taxAmount</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">grossPrice</span><span class="p">)</span> <span class="nf">CalculateTax</span><span class="p">(</span>
        <span class="kt">decimal</span> <span class="n">netPrice</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">taxRate</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">taxAmount</span> <span class="p">=</span> <span class="n">netPrice</span> <span class="p">*</span> <span class="n">taxRate</span> <span class="p">/</span> <span class="m">100m</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">grossPrice</span> <span class="p">=</span> <span class="n">netPrice</span> <span class="p">+</span> <span class="n">taxAmount</span><span class="p">;</span>

        <span class="k">return</span> <span class="p">(</span><span class="n">taxAmount</span><span class="p">,</span> <span class="n">grossPrice</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="tests">Tests</h3>

<p>Now, what does a (unit) test for such code look like? Testing the <code class="language-plaintext highlighter-rouge">GetOrderItems()</code> method of the <code class="language-plaintext highlighter-rouge">OrderItemService</code> illustrates how much setup code is already required to mock dependencies and feed them with data. In the case of the <code class="language-plaintext highlighter-rouge">ITaxService</code> interface, even the business logic is implemented in the mock object.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Should_Return_OrderItems</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Arrange</span>
    <span class="kt">var</span> <span class="n">orderId</span> <span class="p">=</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">();</span>

    <span class="kt">var</span> <span class="n">orderItem1</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderItem</span><span class="p">(</span><span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span>
        <span class="n">orderId</span><span class="p">,</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span> <span class="m">2</span><span class="p">,</span> <span class="m">19.75m</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">orderItem2</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderItem</span><span class="p">(</span><span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span>
        <span class="n">orderId</span><span class="p">,</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span> <span class="m">3</span><span class="p">,</span> <span class="m">9.66m</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">orderItemData</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">OrderItem</span><span class="p">&gt;</span> <span class="p">{</span> <span class="n">orderItem1</span><span class="p">,</span> <span class="n">orderItem2</span> <span class="p">};</span>

    <span class="kt">var</span> <span class="n">orderItemRepository</span> <span class="p">=</span> <span class="n">Substitute</span><span class="p">.</span><span class="n">For</span><span class="p">&lt;</span><span class="n">IOrderItemRepository</span><span class="p">&gt;();</span>
    <span class="n">orderItemRepository</span><span class="p">.</span><span class="nf">GetByOrderId</span><span class="p">(</span><span class="n">orderId</span><span class="p">).</span><span class="nf">Returns</span><span class="p">(</span><span class="n">orderItemData</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">taxService</span> <span class="p">=</span> <span class="n">Substitute</span><span class="p">.</span><span class="n">For</span><span class="p">&lt;</span><span class="n">ITaxService</span><span class="p">&gt;();</span>

    <span class="n">taxService</span><span class="p">.</span><span class="nf">CalculateTax</span><span class="p">(</span><span class="k">default</span><span class="p">,</span> <span class="k">default</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">ReturnsForAnyArgs</span><span class="p">(</span><span class="n">info</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">netPrice</span> <span class="p">=</span> <span class="n">info</span><span class="p">.</span><span class="n">ArgAt</span><span class="p">&lt;</span><span class="kt">decimal</span><span class="p">&gt;(</span><span class="m">0</span><span class="p">);</span>
            <span class="kt">var</span> <span class="n">taxRate</span> <span class="p">=</span> <span class="n">info</span><span class="p">.</span><span class="n">ArgAt</span><span class="p">&lt;</span><span class="kt">decimal</span><span class="p">&gt;(</span><span class="m">1</span><span class="p">);</span>

            <span class="kt">var</span> <span class="n">taxAmount</span> <span class="p">=</span> <span class="n">netPrice</span> <span class="p">*</span> <span class="n">taxRate</span> <span class="p">/</span> <span class="m">100m</span><span class="p">;</span>
            <span class="kt">var</span> <span class="n">grossPrice</span> <span class="p">=</span> <span class="n">netPrice</span> <span class="p">+</span> <span class="n">taxAmount</span><span class="p">;</span>

            <span class="k">return</span> <span class="p">(</span><span class="n">taxAmount</span><span class="p">,</span> <span class="n">grossPrice</span><span class="p">);</span>
        <span class="p">});</span>

    <span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderItemService</span><span class="p">(</span><span class="n">orderItemRepository</span><span class="p">,</span> <span class="n">taxService</span><span class="p">);</span>

    <span class="c1">// Act</span>
    <span class="kt">var</span> <span class="n">orderItems</span> <span class="p">=</span> <span class="n">sut</span><span class="p">.</span><span class="nf">GetOrderItems</span><span class="p">(</span><span class="n">orderId</span><span class="p">);</span>

    <span class="c1">// Assert</span>
    <span class="n">orderItems</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">NotBeNullOrEmpty</span><span class="p">();</span>
    <span class="n">orderItems</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">HaveCount</span><span class="p">(</span><span class="m">2</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">firstOrderItem</span> <span class="p">=</span> <span class="n">orderItems</span><span class="p">.</span><span class="nf">First</span><span class="p">();</span>
    <span class="n">firstOrderItem</span><span class="p">.</span><span class="n">Id</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">Be</span><span class="p">(</span><span class="n">orderItem1</span><span class="p">.</span><span class="n">Id</span><span class="p">);</span>
    <span class="n">firstOrderItem</span><span class="p">.</span><span class="n">TaxRate</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">Be</span><span class="p">(</span><span class="m">19</span><span class="p">);</span>
    <span class="n">firstOrderItem</span><span class="p">.</span><span class="n">GrossPrice</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">Be</span><span class="p">(</span><span class="m">19.75m</span> <span class="p">*</span> <span class="m">1.19m</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As shown in <a href="#step-1-test-dummies">Step 1</a> of our refactoring process, you can significantly reduce the setup code for tests with minimal effort.</p>

<h3 id="code-analysis">Code Analysis</h3>

<p>Exploring our application using <a href="https://www.hello2morrow.com/products/sonargraph/architect9">Sonargraph</a> reveals the number of dependencies between the individual classes we are dealing with at this stage.</p>

<picture><source srcset="/generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-400-4beb01fdd.webp 400w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-600-4beb01fdd.webp 600w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-800-4beb01fdd.webp 800w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-1000-4beb01fdd.webp 1000w" type="image/webp" /><source srcset="/generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-400-dcb768727.png 400w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-600-dcb768727.png 600w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-800-dcb768727.png 800w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-1000-dcb768727.png 1000w" type="image/png" /><img src="/generated/2023-09-28-refactor-to-purity/sonargraph-exploration-initial-state-800-dcb768727.png" alt="Sonargraph Dependency graph initial state" /></picture>

<p>At this point, the codebase consists of 917 lines of code in 53 files and has an <a href="https://5c-design.info/metrics.html#connect">Average Component Dependency (ACD)</a> of 5.3.</p>

<h2 id="step-1-test-dummies">Step 1: Test Dummies</h2>

<p>In the first step, we focus on the test classes. A robust test suite is the foundation of safe refactoring, which is why we start here.</p>

<p>Following the motto <code class="language-plaintext highlighter-rouge">new is glue</code>, we move the instantiation of test data out of the test methods and into <code class="language-plaintext highlighter-rouge">Dummies</code>. There’s a dedicated blog post on the topic of Dummy Factories <a href="/2023/09/14/test-setup-with-dummy-factories-en.html">Simple test setup with dummy factories</a>, so we’ll only briefly touch on the changes to our example code here.</p>

<blockquote>
  <p>Source code on GitHub, Branch <a href="https://github.com/davull/demo-refactor-to-purity/tree/steps/02-introduce-dummies">steps/02-introduce-dummies</a></p>
</blockquote>

<p>We add a class called <code class="language-plaintext highlighter-rouge">DataDummies</code> that takes care of instantiating data objects for us. Additionally, we define some static instances of <code class="language-plaintext highlighter-rouge">Customer</code> objects that we can use in our tests.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">DataDummies</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">Customer</span> <span class="n">JohnDoe</span> <span class="p">=&gt;</span> <span class="nf">Customer</span><span class="p">(</span>
        <span class="k">new</span> <span class="nf">Guid</span><span class="p">(</span><span class="s">"bfbffb19-cdd4-42ac-b536-606a16d03eae"</span><span class="p">),</span> <span class="s">"John"</span><span class="p">,</span>
        <span class="s">"Doe"</span><span class="p">,</span> <span class="s">"john.doe@example.com"</span><span class="p">);</span>

    <span class="k">public</span> <span class="k">static</span> <span class="n">Customer</span> <span class="n">JaneDoe</span> <span class="p">=&gt;</span> <span class="nf">Customer</span><span class="p">(</span>
        <span class="k">new</span> <span class="nf">Guid</span><span class="p">(</span><span class="s">"95a6db4a-4635-4fb3-b7f6-c206ff7272f1"</span><span class="p">),</span> <span class="s">"Jane"</span><span class="p">,</span>
        <span class="s">"Doe"</span><span class="p">,</span> <span class="s">"Jane.doe@example.com"</span><span class="p">,</span> <span class="k">false</span><span class="p">);</span>

    <span class="k">public</span> <span class="k">static</span> <span class="n">Customer</span> <span class="nf">Customer</span><span class="p">(</span>
        <span class="n">Guid</span><span class="p">?</span> <span class="n">id</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span> <span class="kt">string</span> <span class="n">firstName</span> <span class="p">=</span> <span class="s">"Peter"</span><span class="p">,</span> <span class="kt">string</span> <span class="n">lastName</span> <span class="p">=</span> <span class="s">"Parker"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">email</span> <span class="p">=</span> <span class="s">"peter.parker@example.com"</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">active</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">Customer</span><span class="p">(</span><span class="n">id</span> <span class="p">??</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span>
            <span class="n">firstName</span><span class="p">,</span> <span class="n">lastName</span><span class="p">,</span> <span class="n">email</span><span class="p">,</span> <span class="n">active</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We follow the same approach for our domain objects. Here, we can take advantage of the fact that POCO classes and domain models are usually structured similarly, allowing us to use the data objects from <code class="language-plaintext highlighter-rouge">DataDummies</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">ModelDummies</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">Customer</span> <span class="n">JohnDoe</span> <span class="p">=&gt;</span> <span class="nf">FromData</span><span class="p">(</span><span class="n">DataDummies</span><span class="p">.</span><span class="n">JohnDoe</span><span class="p">);</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">Customer</span> <span class="n">JaneDoe</span> <span class="p">=&gt;</span> <span class="nf">FromData</span><span class="p">(</span><span class="n">DataDummies</span><span class="p">.</span><span class="n">JaneDoe</span><span class="p">);</span>

    <span class="k">public</span> <span class="k">static</span> <span class="n">Customer</span> <span class="nf">FromData</span><span class="p">(</span><span class="n">Data</span><span class="p">.</span><span class="n">Customer</span> <span class="n">data</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="nf">Customer</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="n">data</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span> <span class="n">firstName</span><span class="p">:</span> <span class="n">data</span><span class="p">.</span><span class="n">FirstName</span><span class="p">,</span>
            <span class="n">lastName</span><span class="p">:</span> <span class="n">data</span><span class="p">.</span><span class="n">LastName</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="n">data</span><span class="p">.</span><span class="n">Email</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With this approach, our test setup becomes simpler and, most importantly, more resilient to changes in the data objects since we only need to adjust them in one place.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Should_Return_OrderItems</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Arrange</span>
    <span class="kt">var</span> <span class="n">orderId</span> <span class="p">=</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">();</span>

    <span class="kt">var</span> <span class="n">orderItem1</span> <span class="p">=</span> <span class="nf">OrderItem</span><span class="p">(</span><span class="n">price</span><span class="p">:</span> <span class="m">19.75m</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">orderItem2</span> <span class="p">=</span> <span class="nf">OrderItem</span><span class="p">(</span><span class="n">price</span><span class="p">:</span> <span class="m">9.66m</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">orderItemData</span> <span class="p">=</span> <span class="nf">Collection</span><span class="p">(</span><span class="n">orderItem1</span><span class="p">,</span> <span class="n">orderItem2</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">orderItemRepository</span> <span class="p">=</span> <span class="n">Substitute</span><span class="p">.</span><span class="n">For</span><span class="p">&lt;</span><span class="n">IOrderItemRepository</span><span class="p">&gt;();</span>
    <span class="n">orderItemRepository</span><span class="p">.</span><span class="nf">GetByOrderId</span><span class="p">(</span><span class="n">orderId</span><span class="p">).</span><span class="nf">Returns</span><span class="p">(</span><span class="n">orderItemData</span><span class="p">);</span>

    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>After these preparations, we can proceed with refactoring the production code.</p>

<h2 id="step-2-removing-interfaces">Step 2: Removing Interfaces</h2>

<p>In the second step, we aim to remove unnecessary abstractions through interfaces and base classes. It’s often argued that test code can leverage these abstractions, replacing dependencies implemented as interfaces with <a href="https://www.educative.io/answers/what-is-faking-vs-mocking-vs-stubbing">mocks, stubs, or fakes</a>. This is certainly true for external dependencies, such as databases or email servers. However, for self-created abstractions, it usually leads to unnecessary complexity and a high overhead in test setup. Test mocks are challenging to maintain and require knowledge of the internals of the real implementation, necessitating their recreation.</p>

<p>Our first point of focus is the <code class="language-plaintext highlighter-rouge">TaxService</code>. The <code class="language-plaintext highlighter-rouge">CalculateTax()</code> method is already a pure function. Therefore, we can delete the <code class="language-plaintext highlighter-rouge">ITaxService</code> interface, make the class and the method <code class="language-plaintext highlighter-rouge">static</code>, and simply call it directly. There’s no need for dependency injection, and the test mock can also be eliminated.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">TaxService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="p">(</span><span class="kt">decimal</span> <span class="n">taxAmount</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">grossPrice</span><span class="p">)</span> <span class="nf">CalculateTax</span><span class="p">(</span>
        <span class="kt">decimal</span> <span class="n">netPrice</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">taxRate</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="p">...</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The corresponding <a href="https://github.com/davull/demo-refactor-to-purity/commit/01283dc">Git commit</a> shows 43 deleted lines.</p>

<p>Now let’s turn our attention to the service classes <code class="language-plaintext highlighter-rouge">OrderService</code> and <code class="language-plaintext highlighter-rouge">OrderItemService</code>. From the dependencies provided via <a href="https://freecontent.manning.com/understanding-constructor-injection/">constructor injection</a> (e.g., <code class="language-plaintext highlighter-rouge">ICustomerRepository</code>), we only need individual methods or even just the return value of a method. Instead of injecting repository classes, we pass method pointers (<a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/using-delegates">delegates</a>) to the service classes. This eliminates the need for private properties, makes the classes stateless and <code class="language-plaintext highlighter-rouge">static</code>, and allows us to remove the interfaces.</p>

<p>The <code class="language-plaintext highlighter-rouge">OrderService</code> class previously had three dependencies.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">OrderService</span> <span class="p">:</span> <span class="n">IOrderService</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">ICustomerRepository</span> <span class="n">_customerRepository</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IOrderItemRepository</span> <span class="n">_orderItemRepository</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IOrderRepository</span> <span class="n">_orderRepository</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">OrderService</span><span class="p">(</span><span class="n">IOrderRepository</span> <span class="n">orderRepository</span><span class="p">,</span>
        <span class="n">ICustomerRepository</span> <span class="n">customerRepository</span><span class="p">,</span>
        <span class="n">IOrderItemRepository</span> <span class="n">orderItemRepository</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_orderRepository</span> <span class="p">=</span> <span class="n">orderRepository</span><span class="p">;</span>
        <span class="n">_customerRepository</span> <span class="p">=</span> <span class="n">customerRepository</span><span class="p">;</span>
        <span class="n">_orderItemRepository</span> <span class="p">=</span> <span class="n">orderItemRepository</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">Order</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">Guid</span> <span class="n">id</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">orderData</span> <span class="p">=</span> <span class="n">_orderRepository</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
        <span class="k">return</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">orderData</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="n">Order</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">Data</span><span class="p">.</span><span class="n">Order</span> <span class="n">orderData</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">customerData</span> <span class="p">=</span> <span class="n">_customerRepository</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">orderData</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">orderItemData</span> <span class="p">=</span> <span class="n">_orderItemRepository</span><span class="p">.</span><span class="nf">GetByOrderId</span><span class="p">(</span><span class="n">orderData</span><span class="p">.</span><span class="n">Id</span><span class="p">);</span>

        <span class="p">...</span>

        <span class="k">return</span> <span class="n">orderModel</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>After the refactoring, the class looks like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">OrderService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">Order</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">Guid</span> <span class="n">id</span><span class="p">,</span>
        <span class="n">Func</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">,</span> <span class="n">Data</span><span class="p">.</span><span class="n">Order</span><span class="p">&gt;</span> <span class="n">getOrder</span><span class="p">,</span>
        <span class="n">Func</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">,</span> <span class="n">Customer</span><span class="p">&gt;</span> <span class="n">getCustomer</span><span class="p">,</span>
        <span class="n">Func</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">,</span> <span class="n">IReadOnlyCollection</span><span class="p">&lt;</span><span class="n">OrderItem</span><span class="p">&gt;&gt;</span> <span class="n">getOrderItems</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">orderData</span> <span class="p">=</span> <span class="nf">getOrder</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">customerData</span> <span class="p">=</span> <span class="nf">getCustomer</span><span class="p">(</span><span class="n">orderData</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">orderItemData</span> <span class="p">=</span> <span class="nf">getOrderItems</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
        <span class="k">return</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">orderData</span><span class="p">,</span> <span class="n">customerData</span><span class="p">,</span> <span class="n">orderItemData</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">GetOrder()</code> method is now simply called by passing the relevant methods of the repositories as parameters.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">OrderService</span><span class="p">.</span><span class="nf">GetOrder</span><span class="p">(</span>
    <span class="n">id</span><span class="p">:</span> <span class="n">id</span><span class="p">,</span>
    <span class="n">getOrder</span><span class="p">:</span> <span class="n">_orderRepository</span><span class="p">.</span><span class="n">Get</span><span class="p">,</span>
    <span class="n">getCustomer</span><span class="p">:</span> <span class="n">_customerRepository</span><span class="p">.</span><span class="n">Get</span><span class="p">,</span>
    <span class="n">getOrderItems</span><span class="p">:</span> <span class="n">_orderItemRepository</span><span class="p">.</span><span class="n">GetByOrderId</span><span class="p">);</span>
</code></pre></div></div>

<p>If the method signatures differ, we can easily adapt them using <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions">lambda expressions</a>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">OrderService</span><span class="p">.</span><span class="nf">GetOrder</span><span class="p">(</span>
    <span class="n">id</span><span class="p">:</span> <span class="n">id</span><span class="p">,</span>
    <span class="n">getCustomer</span><span class="p">:</span> <span class="n">id</span> <span class="p">=&gt;</span> <span class="n">_customerRepository</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">id</span><span class="p">:</span> <span class="n">id</span><span class="p">,</span> <span class="n">activeOnly</span><span class="p">:</span> <span class="k">true</span><span class="p">),</span>
    <span class="p">...</span>
</code></pre></div></div>

<p>The corresponding unit tests also become simpler. We no longer need to assemble mock objects but only define methods. These local lambda expressions are one-liners.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">getOrder</span> <span class="p">=</span> <span class="p">(</span><span class="n">Guid</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">DataDummies</span><span class="p">.</span><span class="nf">Order</span><span class="p">(</span><span class="n">orderId</span><span class="p">,</span> <span class="n">peterPan</span><span class="p">.</span><span class="n">Id</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">getCustomer</span> <span class="p">=</span> <span class="p">(</span><span class="n">Guid</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">peterPan</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">getByOrderId</span> <span class="p">=</span> <span class="p">(</span><span class="n">Guid</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">DataDummies</span><span class="p">.</span><span class="nf">Collection</span><span class="p">(</span><span class="n">orderItem1</span><span class="p">,</span> <span class="n">orderItem2</span><span class="p">);</span>

<span class="c1">// Act</span>
<span class="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="n">OrderService</span><span class="p">.</span><span class="nf">GetOrder</span><span class="p">(</span><span class="n">orderId</span><span class="p">,</span> <span class="n">getOrder</span><span class="p">,</span> <span class="n">getCustomer</span><span class="p">,</span> <span class="n">getByOrderId</span><span class="p">);</span>
</code></pre></div></div>

<p>Alternatively, with pure functions, you can pass the return value of the same function as a parameter. This is referred to as <a href="https://en.wikipedia.org/wiki/Referential_transparency">referential transparency</a>. However, for methods that have side effects (such as database updates) or filter large data sets, this is not always advisable</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="n">_orderRepository</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">customer</span> <span class="p">=</span> <span class="n">_customerRepository</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">orderItems</span> <span class="p">=</span> <span class="n">_orderItemRepository</span><span class="p">.</span><span class="nf">GetByOrderId</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">OrderService</span><span class="p">.</span><span class="nf">GetOrder</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">customer</span><span class="p">,</span> <span class="n">orderItems</span><span class="p">);</span>
</code></pre></div></div>

<p>By passing dependencies as method parameters rather than using dependency injection to bring them into a class, we shift the responsibility for creating and managing dependencies to the calling code.</p>

<h2 id="step-3-removing-cqrs">Step 3: Removing CQRS</h2>

<p>Next, we remove the CQRS pattern implemented with <a href="https://github.com/jbogard/MediatR">MediatR</a> from our codebase. The <a href="https://www.nuget.org/packages/MediatR">library</a> is great, and CQRS is a powerful tool when there is a genuine need to separate commands and queries. However, in our example, we want to demonstrate that this is often unnecessary and might be <a href="https://stackify.com/premature-optimization-evil/">premature optimization</a> that never materializes.</p>

<p>Instead of distributing the source code that connects the controller and domain logic across multiple <code class="language-plaintext highlighter-rouge">IRequest</code> and <code class="language-plaintext highlighter-rouge">IRequestHandler&lt;&gt;</code> implementations, we consolidate it into a few integration classes.</p>

<p>Instead of having an <code class="language-plaintext highlighter-rouge">AddOrderHandler</code> along with its corresponding <code class="language-plaintext highlighter-rouge">AddOrderRequest</code>, we now have a single method that receives the required dependencies as parameters and orchestrates the invocation of the service classes.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">OrdersIntegration</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="nf">AddOrder</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">,</span>
        <span class="n">ICustomerRepository</span> <span class="n">customerRepository</span><span class="p">,</span>
        <span class="n">IOrderItemRepository</span> <span class="n">orderItemRepository</span><span class="p">,</span>
        <span class="n">IOrderRepository</span> <span class="n">orderRepository</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">order</span><span class="p">.</span><span class="n">Items</span><span class="p">.</span><span class="nf">Any</span><span class="p">())</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span><span class="s">"Order must have at least one item."</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">customerData</span> <span class="p">=</span> <span class="n">customerRepository</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="n">Customer</span><span class="p">.</span><span class="n">Id</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">customerData</span><span class="p">.</span><span class="n">Active</span> <span class="k">is</span> <span class="k">false</span><span class="p">)</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span><span class="s">"Customer is not active."</span><span class="p">);</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">orderItem</span> <span class="k">in</span> <span class="n">order</span><span class="p">.</span><span class="n">Items</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">orderItemData</span> <span class="p">=</span> <span class="n">OrderItemService</span><span class="p">.</span><span class="nf">AddOrderItem</span><span class="p">(</span><span class="n">orderItem</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
            <span class="n">orderItemRepository</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">orderItemData</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="n">OrderService</span><span class="p">.</span><span class="nf">AddOrder</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">orderRepository</span><span class="p">.</span><span class="n">Add</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In another step, similar to what we did for the services, we can switch from injecting repositories to using method delegates. This allows us to remove all <code class="language-plaintext highlighter-rouge">IRepository</code> interfaces since we no longer need to substitute them in our tests. An <a href="https://github.com/davull/demo-refactor-to-purity/commit/220178b">example Git commit</a> demonstrates this for the <code class="language-plaintext highlighter-rouge">IOrderRepository</code>.</p>

<h2 id="schritt-4-statische-repositories">Schritt 4: Statische Repositories</h2>

<p>After removing a few more <a href="https://github.com/davull/demo-refactor-to-purity/commit/9ebbd15">abstract base classes</a> and interfaces, let’s take another look at the repository classes. They all have only one dependency on <code class="language-plaintext highlighter-rouge">IDatabase</code>. Switching from <a href="https://freecontent.manning.com/understanding-constructor-injection/">constructor injection</a> to <a href="https://freecontent.manning.com/understanding-method-injection/">method injection</a> can be done quickly.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- public class OrderRepository
</span><span class="gi">+ public static class OrderRepository
</span>{
<span class="gd">-    private readonly IDatabase _database;
-    public OrderRepository(IDatabase database) =&gt; _database = database;
</span><span class="err">
</span><span class="gd">-    public IEnumerable&lt;OrderData&gt; GetOrdersByDate(
-       DateTime startDate, DateTime endDate)
-        =&gt; _database.GetAll&lt;OrderData&gt;()
-               .Where(x =&gt; x.OrderDate &gt;= startDate &amp;&amp; x.OrderDate &lt;= endDate);
</span><span class="err">

</span><span class="gi">+    public static IEnumerable&lt;OrderData&gt; GetOrdersByDate(
+        DateTime startDate, DateTime endDate, IDatabase db)
+            =&gt; db.GetAll&lt;OrderData&gt;()
+                .Where(x =&gt; x.OrderDate &gt;= startDate &amp;&amp; x.OrderDate &lt;= endDate);
</span>     ...
}
</code></pre></div></div>

<p>If we take it a step further here and expect method delegates as method parameters instead of an instance of <code class="language-plaintext highlighter-rouge">IDatabase</code>, we can completely remove the dependency on <code class="language-plaintext highlighter-rouge">IDatabase</code> from our repositories.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">public static class OrderRepository
</span>{
<span class="gd">-    public static IEnumerable&lt;OrderData&gt; GetOrdersByDate(
-       DateTime startDate, DateTime endDate, IDatabase db)
-        =&gt; db.GetAll&lt;OrderData&gt;()
-            .Where(x =&gt; x.OrderDate &gt;= startDate &amp;&amp; x.OrderDate &lt;= endDate);
</span><span class="err">
</span><span class="gi">+    public static IEnumerable&lt;OrderData&gt; GetOrdersByDate(
+        DateTime startDate, DateTime endDate, 
+        Func&lt;IEnumerable&lt;OrderData&gt;&gt; getAll)
+        =&gt; getAll().Where(x =&gt; x.OrderDate &gt;= startDate &amp;&amp;
+                               x.OrderDate &lt;= endDate);
</span><span class="err">
</span>    ...
}
</code></pre></div></div>

<p>Alternatively, it may be worth considering passing the return values of these methods to our service method instead of the methods themselves. This eliminates all side effects, resulting in a <code class="language-plaintext highlighter-rouge">pure function</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="n">IReadOnlyCollection</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;</span> <span class="nf">GetOrdersByDate</span><span class="p">(</span>
    <span class="n">DateTime</span> <span class="n">startDate</span><span class="p">,</span> <span class="n">DateTime</span> <span class="n">endDate</span><span class="p">,</span>
    <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">OrderData</span><span class="p">&gt;</span> <span class="n">allOrderData</span><span class="p">,</span>
    <span class="n">IDictionary</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">,</span> <span class="n">CustomerData</span><span class="p">&gt;</span> <span class="n">customerData</span><span class="p">,</span>
    <span class="n">ILookup</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">,</span> <span class="n">OrderItemData</span><span class="p">&gt;</span> <span class="n">orderItemData</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">allOrderData</span>
        <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">OrderDate</span> <span class="p">&gt;=</span> <span class="n">startDate</span> <span class="p">&amp;&amp;</span> <span class="n">x</span><span class="p">.</span><span class="n">OrderDate</span> <span class="p">&lt;=</span> <span class="n">endDate</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">order</span> <span class="p">=&gt;</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">order</span><span class="p">,</span>
            <span class="n">customerData</span><span class="p">[</span><span class="n">order</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">],</span> <span class="n">orderItemData</span><span class="p">[</span><span class="n">order</span><span class="p">.</span><span class="n">Id</span><span class="p">]))</span>
        <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The calling method is now responsible for collecting the data.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">allOrderData</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">GetAll</span><span class="p">&lt;</span><span class="n">OrderData</span><span class="p">&gt;();</span>

<span class="kt">var</span> <span class="n">customerData</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">GetAll</span><span class="p">&lt;</span><span class="n">CustomerData</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">ToDictionary</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span> <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">orderData</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">GetAll</span><span class="p">&lt;</span><span class="n">OrderItemData</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">ToLookup</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">OrderId</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">OrderService</span><span class="p">.</span><span class="nf">GetOrdersByDate</span><span class="p">(</span><span class="n">startDate</span><span class="p">,</span> <span class="n">endDate</span><span class="p">,</span>
    <span class="n">allOrderData</span><span class="p">,</span> <span class="n">customerData</span><span class="p">,</span> <span class="n">orderData</span><span class="p">);</span>
</code></pre></div></div>

<p>For external data sources like databases or files, this approach is usually not suitable due to the late filtering, as it may load too much data. We don’t want to load the entire database into memory just to use a few records. However, for small data sets or data already in memory, this is a good way to (<a href="https://github.com/davull/demo-refactor-to-purity/commit/49cde56">reduce complexity</a>).</p>

<h2 id="results">Results</h2>

<p>What have we achieved with these refactoring steps? Our codebase has become significantly smaller, with almost all interfaces removed.</p>

<p>The dependency graph shows significantly fewer lines. The number of lines of code has been reduced to 715 (25% less), the number of files to 34 (35% less), and the Average Component Dependency has dropped from 5.3 to 3.6.</p>

<picture><source srcset="/generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-400-2971a3bb9.webp 400w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-600-2971a3bb9.webp 600w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-800-2971a3bb9.webp 800w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-938-2971a3bb9.webp 938w" type="image/webp" /><source srcset="/generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-400-d9176d44a.png 400w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-600-d9176d44a.png 600w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-800-d9176d44a.png 800w, /generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-938-d9176d44a.png 938w" type="image/png" /><img src="/generated/2023-09-28-refactor-to-purity/sonargraph-exploration-pure-functions-800-d9176d44a.png" alt="Sonargraph Dependency graph pure functions" /></picture>

<p>Besides the raw numbers, what’s more important is that the code is now easier to understand and follow. You no longer have to hunt for interfaces and potential implementations to understand the runtime behavior.</p>

<p>The entire source code is available on <a href="https://github.com/davull/demo-refactor-to-purity">GitHub</a>.</p>]]></content><author><name>David</name></author><category term="c#" /><category term="refactoring" /><category term="purity" /><category term="en" /><summary type="html"><![CDATA[Pure Functions are program methods that can be executed without causing side effects. In functional programming, they are more of a rule than an exception. However, in most object-oriented languages, you encounter them less often, or at least they are not frequently considered the preferred approach. In the dotnet environment, much emphasis is placed on Dependency Injection and more or less extensive abstractions using interfaces. The following article will demonstrate how to transition from a codebase with many such indirections to a simpler version that removes a lot of unnecessary complexity.]]></summary></entry><entry xml:lang="en"><title type="html">Simple test setup with dummy factories</title><link href="https://www.production-ready.de/2023/09/14/test-setup-with-dummy-factories-en.html" rel="alternate" type="text/html" title="Simple test setup with dummy factories" /><published>2023-09-14T00:00:00+02:00</published><updated>2023-09-14T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/09/14/test-setup-with-dummy-factories-en</id><content type="html" xml:base="https://www.production-ready.de/2023/09/14/test-setup-with-dummy-factories-en.html"><![CDATA[<p>Meaningful software verification involves comprehensive testing. A recurring task is writing source code that generates or describes <em>test data</em>. Depending on the volume of data, this can be tedious and slow down the development process, and in the worst case, lead to the complete omission of testing. An elegant way to generate test data easily and quickly is by using dummy factories.</p>

<!--more-->

<h2 id="test-data">Test data</h2>

<p>For calling methods from tests, they often need to be fed with defined input parameters. The further down we are in the <a href="https://martinfowler.com/articles/practical-test-pyramid.html">test pyramid</a>, the more synthetic these data will be. The structure of the data can range from simple primitive data types like strings, booleans, or numerical values to complex object structures.</p>

<p>Let’s take a look at an imaginary e-commerce software with the following data classes. The examples are written in C#, but can be adapted to other programming languages.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">record</span> <span class="nc">Address</span><span class="p">(</span><span class="n">Guid</span> <span class="n">Id</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Street</span><span class="p">,</span> <span class="kt">string</span> <span class="n">City</span><span class="p">,</span>
  <span class="kt">string</span> <span class="n">State</span><span class="p">,</span> <span class="kt">string</span> <span class="n">ZipCode</span><span class="p">);</span>

<span class="k">public</span> <span class="k">record</span> <span class="nc">Customer</span><span class="p">(</span><span class="n">Guid</span> <span class="n">Id</span><span class="p">,</span> <span class="kt">string</span> <span class="n">FirstName</span><span class="p">,</span> <span class="kt">string</span> <span class="n">LastName</span><span class="p">,</span>
  <span class="n">Address</span> <span class="n">Address</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Email</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">Active</span><span class="p">);</span>

<span class="k">public</span> <span class="k">record</span> <span class="nc">Product</span><span class="p">(</span><span class="kt">string</span> <span class="n">Sku</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Name</span><span class="p">,</span>
  <span class="kt">string</span> <span class="n">Description</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">Price</span><span class="p">);</span>

<span class="k">public</span> <span class="k">record</span> <span class="nc">Order</span><span class="p">(</span><span class="kt">string</span> <span class="n">OrderNumber</span><span class="p">,</span> <span class="n">DateTime</span> <span class="n">OrderDate</span><span class="p">,</span>
  <span class="n">Customer</span> <span class="n">Customer</span><span class="p">,</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">OrderItem</span><span class="p">&gt;</span> <span class="n">Items</span><span class="p">);</span>

<span class="k">public</span> <span class="k">record</span> <span class="nc">OrderItem</span><span class="p">(</span><span class="n">Product</span> <span class="n">Product</span><span class="p">,</span> <span class="kt">int</span> <span class="n">Quantity</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">Price</span><span class="p">);</span>
</code></pre></div></div>

<p>For a method <code class="language-plaintext highlighter-rouge">OrderService.PlaceOrder(..)</code>, a unit test could begin something like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Should_Place_Order</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Arrange</span>
    <span class="kt">var</span> <span class="n">address</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Address</span><span class="p">(</span><span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span> <span class="s">"123 Main St"</span><span class="p">,</span>
      <span class="s">"Anytown"</span><span class="p">,</span> <span class="s">"TX"</span><span class="p">,</span> <span class="s">"12345"</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">customer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Customer</span><span class="p">(</span><span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span> <span class="s">"John"</span><span class="p">,</span> <span class="s">"Doe"</span><span class="p">,</span> <span class="n">address</span><span class="p">,</span>
      <span class="s">"john.doe@example.com"</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">product1</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Product</span><span class="p">(</span><span class="s">"P-001"</span><span class="p">,</span> <span class="s">"Product 1"</span><span class="p">,</span> <span class="s">"Product 1 Description"</span><span class="p">,</span> <span class="m">9.99m</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">product2</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Product</span><span class="p">(</span><span class="s">"P-002"</span><span class="p">,</span> <span class="s">"Product 2"</span><span class="p">,</span> <span class="s">"Product 2 Description"</span><span class="p">,</span> <span class="m">19.99m</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">orderItems</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span>
    <span class="p">{</span>
        <span class="k">new</span> <span class="nf">OrderItem</span><span class="p">(</span><span class="n">product1</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="n">product1</span><span class="p">.</span><span class="n">Price</span><span class="p">),</span>
        <span class="k">new</span> <span class="nf">OrderItem</span><span class="p">(</span><span class="n">product2</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="n">product2</span><span class="p">.</span><span class="n">Price</span><span class="p">)</span>
    <span class="p">};</span>
    <span class="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Order</span><span class="p">(</span><span class="s">"O-001"</span><span class="p">,</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="p">,</span> <span class="n">customer</span><span class="p">,</span> <span class="n">orderItems</span><span class="p">);</span>

    <span class="c1">// Act</span>
    <span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderService</span><span class="p">();</span>
    <span class="n">sut</span><span class="p">.</span><span class="nf">PlaceOrder</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>

    <span class="c1">// Assert</span>
    <span class="c1">//...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>However, before we can reach the point of calling our method to be tested with the correct parameters, we need to write several lines of setup code just to create a valid <code class="language-plaintext highlighter-rouge">Order</code>. An <code class="language-plaintext highlighter-rouge">Order</code> contains a <code class="language-plaintext highlighter-rouge">Customer</code> with an <code class="language-plaintext highlighter-rouge">Address</code> and a quantity of <code class="language-plaintext highlighter-rouge">OrderItems</code> with associated <code class="language-plaintext highlighter-rouge">Products</code>.</p>

<p>This code not only becomes quickly redundant but also error-prone. Now, if we imagine that we have to create a new <code class="language-plaintext highlighter-rouge">Order</code> for each test method, it becomes evident that we can lose a lot of time here.</p>

<p>Another disadvantage: if there are changes to our data classes, for example, if a <code class="language-plaintext highlighter-rouge">Customer</code> now also needs to have a phone number, and we add non-optional parameters, we would have to update the constructor calls in all test methods that create these data instances.</p>

<h2 id="dummy-factories">Dummy factories</h2>

<p>One solution to these issues is to create dummy factories. These are classes and methods that are available to us in all tests after being written once, making it easier to generate test data. For each data class, a dummy method is created, ideally with default parameters. These parameters can be overridden as needed in the test methods for the specific use case. For nested data structures, <a href="https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references">nullable reference types</a> can be used, which, in the absence of data, can be replaced with dummy values.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">Dummies</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">Address</span> <span class="nf">Address</span><span class="p">(</span>
        <span class="n">Guid</span><span class="p">?</span> <span class="n">id</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">street</span> <span class="p">=</span> <span class="s">"123 Main St"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">city</span> <span class="p">=</span> <span class="s">"Anytown"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">state</span> <span class="p">=</span> <span class="s">"TX"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">zipCode</span> <span class="p">=</span> <span class="s">"12345"</span><span class="p">)</span> <span class="p">=&gt;</span>
        <span class="k">new</span><span class="p">(</span><span class="n">id</span> <span class="p">??</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span> <span class="n">street</span><span class="p">,</span> <span class="n">city</span><span class="p">,</span> <span class="n">state</span><span class="p">,</span> <span class="n">zipCode</span><span class="p">);</span>

    <span class="k">public</span> <span class="k">static</span> <span class="n">Customer</span> <span class="nf">Customer</span><span class="p">(</span>
        <span class="n">Guid</span><span class="p">?</span> <span class="n">id</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">firstName</span> <span class="p">=</span> <span class="s">"John"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">lastName</span> <span class="p">=</span> <span class="s">"Doe"</span><span class="p">,</span>
        <span class="n">Address</span><span class="p">?</span> <span class="n">address</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">email</span> <span class="p">=</span> <span class="s">"john.doe@example.com"</span><span class="p">,</span>
        <span class="kt">bool</span> <span class="n">active</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span> <span class="p">=&gt;</span>
        <span class="k">new</span><span class="p">(</span><span class="n">id</span> <span class="p">??</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span> <span class="n">firstName</span><span class="p">,</span> <span class="n">lastName</span><span class="p">,</span> 
          <span class="n">address</span> <span class="p">??</span> <span class="nf">Address</span><span class="p">(),</span> <span class="n">email</span><span class="p">,</span> <span class="n">active</span><span class="p">);</span>

    <span class="k">public</span> <span class="k">static</span> <span class="n">Product</span> <span class="nf">Product</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">sku</span> <span class="p">=</span> <span class="s">"P-001"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">name</span> <span class="p">=</span> <span class="s">"Product 1"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">description</span> <span class="p">=</span> <span class="s">"Product 1 Description"</span><span class="p">,</span>
        <span class="kt">decimal</span> <span class="n">price</span> <span class="p">=</span> <span class="m">9.99m</span><span class="p">)</span> <span class="p">=&gt;</span>
        <span class="k">new</span><span class="p">(</span><span class="n">sku</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">description</span><span class="p">,</span> <span class="n">price</span><span class="p">);</span>
    
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With small helper methods, you can further simplify your life.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="n">T</span><span class="p">[]</span> <span class="n">Many</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="k">params</span> <span class="n">T</span><span class="p">[]</span> <span class="n">items</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">items</span><span class="p">.</span><span class="nf">ToArray</span><span class="p">();</span>
</code></pre></div></div>

<p>The test method shown above can now look like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">static</span> <span class="n">Company</span><span class="p">.</span><span class="n">Webstore</span><span class="p">.</span><span class="n">Tests</span><span class="p">.</span><span class="n">Dummies</span><span class="p">;</span>

<span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Should_Place_Order</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Arrange</span>
    <span class="kt">var</span> <span class="n">orderItems</span> <span class="p">=</span> <span class="nf">Many</span><span class="p">(</span>
        <span class="nf">OrderItem</span><span class="p">(),</span>
        <span class="nf">OrderItem</span><span class="p">(</span><span class="nf">Product</span><span class="p">(</span><span class="n">sku</span><span class="p">:</span> <span class="s">"P-002"</span><span class="p">,</span> <span class="n">price</span><span class="p">:</span> <span class="m">19.99m</span><span class="p">),</span> <span class="m">2</span><span class="p">));</span>
    <span class="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="nf">Order</span><span class="p">(</span><span class="n">items</span><span class="p">:</span> <span class="n">orderItems</span><span class="p">);</span>

    <span class="c1">// Act</span>
    <span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderService</span><span class="p">();</span>
    <span class="n">sut</span><span class="p">.</span><span class="nf">PlaceOrder</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>

    <span class="c1">// Assert</span>
    <span class="c1">//...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>By using a <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive#static-modifier">using static</a> statement, you can call the dummy method directly without having to prepend the class name.</p>

<p>With this, our test setup is reduced to just a few relevant lines.</p>

<p>If there are any changes to our data classes, we only need to adjust the dummy method, and the test methods remain untouched.</p>

<h2 id="alternatives">Alternatives</h2>

<p>To generate more or less random test or fake data, libraries like <a href="https://github.com/bchavez/Bogus">Bogus</a> or <a href="https://fscheck.github.io/FsCheck/">FsCheck</a> are available. Of course, using these libraries involves giving up some control, whereas using dummies is much more explicit. Depending on the use case, this can be an advantage and justify the additional complexity.</p>]]></content><author><name>David</name></author><category term="c#" /><category term="testing" /><category term="dummy" /><category term="dummies" /><category term="en" /><summary type="html"><![CDATA[Meaningful software verification involves comprehensive testing. A recurring task is writing source code that generates or describes test data. Depending on the volume of data, this can be tedious and slow down the development process, and in the worst case, lead to the complete omission of testing. An elegant way to generate test data easily and quickly is by using dummy factories.]]></summary></entry><entry xml:lang="en"><title type="html">Docker Scale-to-Zero with Traefik and Sablier</title><link href="https://www.production-ready.de/2023/08/20/docker-scale-to-zero-with-traefik-sablier-en.html" rel="alternate" type="text/html" title="Docker Scale-to-Zero with Traefik and Sablier" /><published>2023-08-20T00:00:00+02:00</published><updated>2023-08-20T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/08/20/docker-scale-to-zero-with-traefik-sablier-en</id><content type="html" xml:base="https://www.production-ready.de/2023/08/20/docker-scale-to-zero-with-traefik-sablier-en.html"><![CDATA[<p>When operating web applications or services that are only used sporadically, it can be useful to start them only when there is a specific need. In the context of Docker containers and Kubernetes, the concept of “Scale-to-Zero” exists for such use cases, which means scaling down workloads to zero.</p>

<p>For HTTP services in conjunction with a reverse proxy, <a href="https://acouvreur.github.io/sablier/">Sablier</a> offers a simple way to implement Scale-to-Zero in your own infrastructure.</p>

<!--more-->

<h2 id="scale-to-zero">Scale-to-Zero</h2>

<p>Scale-to-Zero refers to the complete scaling down of workloads to zero, so that no resources are being consumed. In the context of Kubernetes, solutions like <a href="https://keda.sh">Keda</a> exist, but they are more geared towards event-driven use cases and are not primarily suitable for HTTP services. The <a href="https://github.com/kedacore/http-add-on">HTTP Addon</a> for Keda is currently in the beta stage.</p>

<p>Fully shutting down applications can be advantageous when operating a large number of services with only a few requests each. This reduces the number of concurrently running containers and resource consumption. If you’re using a cloud provider and paying per resource or time unit, this can lead to cost savings.</p>

<blockquote>
  <p>The example code for this post can be found on Github: <a href="https://github.com/davull/demo-docker-traefik-sablier">https://github.com/davull/demo-docker-traefik-sablier</a></p>
</blockquote>

<h2 id="sablier-and-traefik">Sablier and Traefik</h2>

<p><a href="https://acouvreur.github.io/sablier/">Sablier</a> is a lightweight solution for starting and stopping HTTP services on demand. It supports various <a href="https://acouvreur.github.io/sablier/#/providers/overview">container providers</a>, currently Docker, Docker Swarm, and Kubernetes. To scale up and down the corresponding containers based on incoming requests, there are <a href="https://acouvreur.github.io/sablier/#/plugins/overview">plugins</a> available for the reverse proxies Traefik, Nginx, and Caddy. Since Sablier is defined as an API, it can theoretically be integrated into other systems as well.</p>

<picture><source srcset="/generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-400-250e16d7d.webp 400w, /generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-600-250e16d7d.webp 600w, /generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-800-250e16d7d.webp 800w, /generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-1000-250e16d7d.webp 1000w" type="image/webp" /><source srcset="/generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-400-98222baa3.png 400w, /generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-600-98222baa3.png 600w, /generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-800-98222baa3.png 800w, /generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-1000-98222baa3.png 1000w" type="image/png" /><img src="/generated/2023-08-20-docker-scale-to-zero/reverse-proxy-integration-800-98222baa3.png" alt="Reverse Proxy Integration" /></picture>

<blockquote>
  <p>Source <a href="https://acouvreur.github.io/sablier/#/plugins/overview">https://acouvreur.github.io/sablier/</a></p>
</blockquote>

<p>Below, the use of Sablier with <a href="https://traefik.io/traefik/">Traefik</a> and Docker is described. However, the configuration for other reverse proxies and container providers is analogous.</p>

<p>The starting point is a Traefik instance and a workload that should be controlled via Sablier. The definition of the services is done using Docker Compose.</p>

<p>The configuration file <code class="language-plaintext highlighter-rouge">docker-compose-traefik.yaml</code> for Traefik contains a single service description and a network:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">traefik</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik:v2.10</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">80:80</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>
      <span class="pi">-</span> <span class="s">./traefik/traefik.yml:/etc/traefik/traefik.yml</span>
      <span class="pi">-</span> <span class="s">./traefik/dynamic_config/:/etc/traefik/dynamic_config/</span>
      <span class="pi">-</span> <span class="s">./traefik/log/:/etc/traefik/log/</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik</span>
    <span class="s">...</span>
    
<span class="na">networks</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">traefik</span>
</code></pre></div></div>

<p>The workload consists of three services (all represented by traefik/whoami images), which are meant to simulate a distributed application. The file <code class="language-plaintext highlighter-rouge">docker-compose-whoami.yaml</code> has the following content:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">whoami</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">whoami</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik/whoami</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.enable=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.whoami-router.rule=Host(`whoami.example.com`)"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.whoami-router.entrypoints=web"</span>

  <span class="na">whoami-nginx</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">nginx</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik/whoami</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik</span>

  <span class="na">whoami-mariadb</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">mariadb</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik/whoami</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">traefik</span>
    <span class="na">external</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<h2 id="setting-up-sablier">Setting up Sablier</h2>

<p>Sablier itself can be run as a Docker container or as a binary. Here, we’re using the container and extending the file <code class="language-plaintext highlighter-rouge">docker-compose-traefik.yaml</code> with an additional service description:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">sablier</span><span class="pi">:</span>
  <span class="na">container_name</span><span class="pi">:</span> <span class="s">traefik-sablier</span>
  <span class="na">image</span><span class="pi">:</span> <span class="s">acouvreur/sablier:1.3.0</span>
  <span class="na">command</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">start</span>
    <span class="pi">-</span> <span class="s">--provider.name=docker</span>
  <span class="na">volumes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>
  <span class="na">networks</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">traefik</span>
</code></pre></div></div>

<p>Sablier requires access to the Docker host service, which is why we’re mounting the path <code class="language-plaintext highlighter-rouge">/var/run/docker.sock</code> to the same location in the container. Using the parameter <code class="language-plaintext highlighter-rouge">--provider.name</code>, we specify the used container provider as <code class="language-plaintext highlighter-rouge">docker</code>.</p>

<h3 id="adding-the-traefik-plugin">Adding the Traefik Plugin</h3>

<p>To enable Traefik to interact with the Sablier API and be aware of it, we configure the <a href="https://plugins.traefik.io/plugins/633b4658a4caa9ddeffda119/sablier">Traefik plugin</a> for Sablier. For this purpose, the configuration file <code class="language-plaintext highlighter-rouge">traefik.yml</code> is extended with the following lines:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">experimental</span><span class="pi">:</span>
  <span class="na">plugins</span><span class="pi">:</span>
    <span class="na">sablier</span><span class="pi">:</span>
      <span class="na">moduleName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">github.com/acouvreur/sablier"</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">v1.3.0"</span>
</code></pre></div></div>

<h3 id="preparing-configuration">Preparing Configuration</h3>

<p>In order for Sablier to start and stop the containers in our workload, two adjustments need to be made.</p>

<p>Since Traefik no longer has access to container labels when a container is not running (scaled down to zero), we first need to change the configuration of the workload service from container labels to a configuration file. This is only necessary if container labels were used for Traefik configuration.</p>

<p>The labels from the workload service are transferred to a file <code class="language-plaintext highlighter-rouge">dynamic_config/whoami.yaml</code>.</p>

<p>From</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># docker-compose-whoami.yaml</span>
<span class="nn">...</span>
<span class="na">labels</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.enable=true"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.whoami-router.rule=Host(`whoami.example.com`)"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.whoami-router.entrypoints=web"</span>
</code></pre></div></div>

<p>it becomes</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># dynamic_config/whoami.yaml</span>
<span class="na">http</span><span class="pi">:</span>
  <span class="na">services</span><span class="pi">:</span>
    <span class="na">whoami-service</span><span class="pi">:</span>
      <span class="na">loadBalancer</span><span class="pi">:</span>
        <span class="na">servers</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">http://whoami:80</span>

  <span class="na">routers</span><span class="pi">:</span>
    <span class="na">whoami-router</span><span class="pi">:</span>
      <span class="na">rule</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Host(`whoami.example.com`)"</span>
      <span class="na">service</span><span class="pi">:</span> <span class="s">whoami-service</span>
      <span class="na">entryPoints</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">web</span>
</code></pre></div></div>

<p>Afterwards, we can proceed with integrating Sablier.</p>

<h3 id="configuring-sablier">Configuring Sablier</h3>

<p>Sablier has two types of <a href="https://acouvreur.github.io/sablier/#/strategies">strategies</a> for responding to an incoming HTTP request when the corresponding container is not running:</p>

<ul>
  <li>Dynamic Strategy: Sablier serves a status webpage that informs the user their requested service is being started and automatically redirects them or refreshes the page after the start.</li>
  <li>Blocking Strategy: Sablier blocks the request until the underlying container is started and then delivers the response.</li>
</ul>

<p>The Blocking Strategy is especially suitable for APIs, so that the calling client only experiences a longer response time on the first access to a shutdown service, without needing to deal with retries and redirects.</p>

<h4 id="dynamic-strategy">Dynamic Strategy</h4>

<p>To configure our workload with the Dynamic Strategy, we start by creating a Traefik middleware for Sablier. This is defined in the <code class="language-plaintext highlighter-rouge">dynamic_config/sablier.yaml</code> file:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">http</span><span class="pi">:</span>
  <span class="na">middlewares</span><span class="pi">:</span>
    <span class="na">sablier-dynamic</span><span class="pi">:</span>
      <span class="na">plugin</span><span class="pi">:</span>
        <span class="na">sablier</span><span class="pi">:</span>
          <span class="na">sablierUrl</span><span class="pi">:</span> <span class="s">http://sablier:10000</span>
          <span class="na">sessionDuration</span><span class="pi">:</span> <span class="s">1m</span>
          <span class="na">names</span><span class="pi">:</span> <span class="s">whoami,nginx,mariadb</span>
          <span class="na">dynamic</span><span class="pi">:</span>
            <span class="na">displayName</span><span class="pi">:</span> <span class="s">whoami</span>
            <span class="na">refreshFrequency</span><span class="pi">:</span> <span class="s">1s</span>
            <span class="na">showDetails</span><span class="pi">:</span> <span class="kc">true</span>
            <span class="na">theme</span><span class="pi">:</span> <span class="s">shuffle</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sablierUrl</code> points to our Sablier container, which we defined in the <code class="language-plaintext highlighter-rouge">docker-compose-traefik.yaml</code> file, and port 10,000 is the default port.</p>

<p>The <code class="language-plaintext highlighter-rouge">sessionDuration</code> parameter indicates how long a container should remain running after the last access to it. After that, the container will be shut down.</p>

<p>With <code class="language-plaintext highlighter-rouge">names</code>, we specify the names of the containers that Sablier should control. These names must match the names of the containers in our <code class="language-plaintext highlighter-rouge">docker-compose-whoami.yaml</code> file (parameter <code class="language-plaintext highlighter-rouge">container_name</code>).</p>

<p>Under the <code class="language-plaintext highlighter-rouge">dynamic</code> key, the behavior of the Dynamic Strategy is configured. We can provide a display name (<code class="language-plaintext highlighter-rouge">displayName</code>) for the status page, set the refresh frequency (<code class="language-plaintext highlighter-rouge">refreshFrequency</code>) of the status page, decide whether to show details (<code class="language-plaintext highlighter-rouge">showDetails</code>), and specify which theme (<code class="language-plaintext highlighter-rouge">theme</code>) to use. There are several <a href="https://acouvreur.github.io/sablier/#/themes">themes to choose from</a>, but custom themes can also be defined.</p>

<picture><source srcset="/generated/2023-08-20-docker-scale-to-zero/screenshot-dynamic-strategy-shuffle-400-5977d1ec0.webp 400w, /generated/2023-08-20-docker-scale-to-zero/screenshot-dynamic-strategy-shuffle-600-5977d1ec0.webp 600w, /generated/2023-08-20-docker-scale-to-zero/screenshot-dynamic-strategy-shuffle-727-5977d1ec0.webp 727w" type="image/webp" /><source srcset="/generated/2023-08-20-docker-scale-to-zero/screenshot-dynamic-strategy-shuffle-400-e38f64cf2.png 400w, /generated/2023-08-20-docker-scale-to-zero/screenshot-dynamic-strategy-shuffle-600-e38f64cf2.png 600w, /generated/2023-08-20-docker-scale-to-zero/screenshot-dynamic-strategy-shuffle-727-e38f64cf2.png 727w" type="image/png" /><img src="/generated/2023-08-20-docker-scale-to-zero/screenshot-dynamic-strategy-shuffle-727-e38f64cf2.png" alt="Dynamic Strategy Shuffle Theme" /></picture>

<p>Now we configure our workload to use the Sablier middleware. In the <code class="language-plaintext highlighter-rouge">dynamic_config/whoami.yaml</code> file, we modify the definition of the whoami router and add the middleware:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">http</span><span class="pi">:</span>
  <span class="s">...</span>
  <span class="s">routers</span><span class="err">:</span>
    <span class="na">whoami-router</span><span class="pi">:</span>
      <span class="na">rule</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Host(`whoami.example.com`)"</span>
      <span class="na">service</span><span class="pi">:</span> <span class="s">whoami-service</span>
      <span class="na">entryPoints</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">web</span>
      <span class="na">middlewares</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">sablier-dynamic@file</span>
</code></pre></div></div>

<p>With this, Sablier is ready to receive our request, start the underlying container for us, and shut it down after a minute of inactivity.</p>

<p><img src="/assets/images/2023-08-20-docker-scale-to-zero/animation-dynamic-strategy.gif" alt="Dynamic Strategy" title="Dynamic Strategy" /></p>

<blockquote>
  <p>A note about the Dynamic Strategy: Unfortunately, the Safari browser on iOS devices doesn’t display the status page on the first access and instead gives a <code class="language-plaintext highlighter-rouge">This site can't be reached</code> error.</p>
</blockquote>

<h4 id="blocking-strategy">Blocking Strategy</h4>

<p>The Blocking Strategy is configured in a similar way to the Dynamic Strategy. We start by creating another Traefik middleware for Sablier. This is defined in the <code class="language-plaintext highlighter-rouge">dynamic_config/sablier.yaml</code> file as well:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">http</span><span class="pi">:</span>
  <span class="na">middlewares</span><span class="pi">:</span>
    <span class="na">sablier-dynamic</span><span class="pi">:</span>
      <span class="s">...</span>

    <span class="na">sablier-blocking</span><span class="pi">:</span>
      <span class="na">plugin</span><span class="pi">:</span>
        <span class="na">sablier</span><span class="pi">:</span>
          <span class="na">sablierUrl</span><span class="pi">:</span> <span class="s">http://sablier:10000</span>
          <span class="na">sessionDuration</span><span class="pi">:</span> <span class="s">1m</span>
          <span class="na">names</span><span class="pi">:</span> <span class="s">whoami</span>
          <span class="na">blocking</span><span class="pi">:</span>
            <span class="na">defaultTimeout</span><span class="pi">:</span> <span class="s">10s</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">defaultTimeout</code> specifies how long to wait for the target container to start before aborting the request.</p>

<p>In the <code class="language-plaintext highlighter-rouge">dynamic_config/whoami.yaml</code> file, we use the new middleware:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">http</span><span class="pi">:</span>
  <span class="s">...</span>
  <span class="s">routers</span><span class="err">:</span>
    <span class="na">whoami-router</span><span class="pi">:</span>
      <span class="s">...</span>
      <span class="s">middlewares</span><span class="err">:</span>
        <span class="pi">-</span> <span class="s">sablier-blocking@file</span>
</code></pre></div></div>

<p>When we call our workload with the Blocking Strategy, on the first access, we experience a longer response time as Sablier needs to start the underlying container.</p>

<p><img src="/assets/images/2023-08-20-docker-scale-to-zero/animation-blocking-strategy.gif" alt="Blocking Strategy" title="Blocking Strategy" /></p>

<h2 id="conclusion">Conclusion</h2>

<p>Sablier offers a straightforward way to implement Scale-to-Zero for HTTP services. Once you’ve gathered the necessary information from the various documentation sources for Sablier, Traefik, and the Traefik plugin, the configuration isn’t overly complex. However, the documentation can sometimes be thin, and you might encounter empty pages here and there.</p>

<p>The example code with commits for each configuration step can be found on GitHub: <a href="https://github.com/davull/demo-docker-traefik-sablier">https://github.com/davull/demo-docker-traefik-sablier</a>.</p>]]></content><author><name>David</name></author><category term="docker" /><category term="traefik" /><category term="sablier" /><category term="infrastructure" /><category term="en" /><summary type="html"><![CDATA[When operating web applications or services that are only used sporadically, it can be useful to start them only when there is a specific need. In the context of Docker containers and Kubernetes, the concept of “Scale-to-Zero” exists for such use cases, which means scaling down workloads to zero. For HTTP services in conjunction with a reverse proxy, Sablier offers a simple way to implement Scale-to-Zero in your own infrastructure.]]></summary></entry><entry xml:lang="en"><title type="html">Automating DNS with DNSControl</title><link href="https://www.production-ready.de/2023/07/02/dnscontrol-en.html" rel="alternate" type="text/html" title="Automating DNS with DNSControl" /><published>2023-07-02T00:00:00+02:00</published><updated>2023-07-02T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/07/02/dnscontrol-en</id><content type="html" xml:base="https://www.production-ready.de/2023/07/02/dnscontrol-en.html"><![CDATA[<p>Configuration of DNS zones is a necessary part of most (web) software projects. Whether during the initial setup or over time, domains need to be registered, and DNS records need to be set up or adjusted. While this can often be done manually through the web interface of the corresponding provider, it’s error-prone, hard to track, and not easy to automate.</p>

<p>If you use Infrastructure-as-Code (IaC) tools like Terraform for provisioning your cloud infrastructure, you can also manage DNS configuration through it. However, this approach often restricts you to the major hyperscale cloud providers for DNS services.</p>

<p><a href="https://docs.dnscontrol.org">DNSControl</a> provides a simpler and less configuration-intensive way to define DNS configuration for dozens of large and small providers using JavaScript code. This allows you to set up and manage your DNS environment in an automated and reproducible manner. The configuration is versioned in a Git repository and can undergo code reviews, seamlessly integrating the process into the DevOps toolchain.</p>

<!--more-->

<h2 id="installation">Installation</h2>

<p>You can <a href="https://docs.dnscontrol.org/getting-started/getting-started">install</a> DNSControl through a package manager like <code class="language-plaintext highlighter-rouge">brew</code> or by downloading the binary from the <a href="https://github.com/StackExchange/dnscontrol/releases">project’s GitHub page</a>. The program, written in Go, is available for Windows, Linux, and macOS. Additionally, there’s a pre-built Docker image.</p>

<h2 id="configuration">Configuration</h2>

<p>DNSControl understands DNS registrars and DNS service providers (DSP). A registrar allows you to register new domains and specify the responsible nameservers. A DSP manages DNS zones and serves DNS requests. Often, a registrar also functions as a DSP. However, the configuration still needs to be done for both entities.</p>

<h3 id="providing-credentials">Providing credentials</h3>

<p>In the <code class="language-plaintext highlighter-rouge">creds.json</code> file, you store the credentials for the providers you’re using. Most providers require access credentials in the form of an API key or something similar. The exact syntax can be found in the <a href="https://docs.dnscontrol.org/service-providers/providers">documentation</a> of each provider. Some providers can only be used as DSPs, while others can only be used as registrars. To create a local zone file for BIND configuration, you can use the <code class="language-plaintext highlighter-rouge">BIND</code> type. If you’re not using a registrar, you can use the <code class="language-plaintext highlighter-rouge">NONE</code> type.</p>

<p>To avoid storing credentials in plain text in the configuration file, you can use environment variables. These variables are referenced in the configuration file using the <code class="language-plaintext highlighter-rouge">$NAME</code> syntax.</p>

<p>The following example configures BIND, a dummy entry, and Hetzner as a provider.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"local"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"TYPE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BIND"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"none"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"TYPE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"NONE"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"hetzner"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"TYPE"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HETZNER"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"api_key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"$DNSCONTROL_HETZNER_API_KEY"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>You can check if your configuration is correct using <code class="language-plaintext highlighter-rouge">check-creds</code>. The <code class="language-plaintext highlighter-rouge">get-zones</code> command lets you retrieve the DNS zones that are already present in your DNS provider. Formats like <code class="language-plaintext highlighter-rouge">tsv</code> (tab-separated values) and the BIND format <code class="language-plaintext highlighter-rouge">zone</code> are available. <code class="language-plaintext highlighter-rouge">nameonly</code> displays only the zone names.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check credentials</span>
dnscontrol check-creds hetzner

<span class="c"># Get all zones from provider</span>
dnscontrol get-zones <span class="nt">--format</span><span class="o">=</span>nameonly hetzner - all
</code></pre></div></div>

<h2 id="configuring-dns-zones">Configuring DNS Zones</h2>

<p>The configuration of DNS zones is done using JavaScript code, typically in the <code class="language-plaintext highlighter-rouge">dnsconfig.js</code> file. This approach offers great flexibility for recurring settings such as DNS or mail servers.</p>

<p>The <code class="language-plaintext highlighter-rouge">NewRegistrar()</code> and <code class="language-plaintext highlighter-rouge">NewDnsProvider()</code> functions are used to read the configured providers from the <code class="language-plaintext highlighter-rouge">creds.json</code> file. Entries are identified by their names.</p>

<p>The <code class="language-plaintext highlighter-rouge">D()</code> function is used to define a DNS zone. It takes parameters such as the name of the zone, the registrar, the DSP, and a list of DNS entries. For different types of entries, there are corresponding functions known as <a href="https://docs.dnscontrol.org/language-reference/domain-modifiers">Domain Modifiers</a> available, such as <code class="language-plaintext highlighter-rouge">A()</code> and <code class="language-plaintext highlighter-rouge">AAAA()</code> for A and AAAA records, respectively.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">REG</span> <span class="o">=</span> <span class="nc">NewRegistrar</span><span class="p">(</span><span class="dl">"</span><span class="s2">none</span><span class="dl">"</span><span class="p">);;</span>
<span class="kd">var</span> <span class="nx">DNS</span> <span class="o">=</span> <span class="nc">NewDnsProvider</span><span class="p">(</span><span class="dl">"</span><span class="s2">hetzner</span><span class="dl">"</span><span class="p">);;</span>

<span class="kd">var</span> <span class="nx">HETZNER_NAMESERVER_RECORDS</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nc">NAMESERVER</span><span class="p">(</span><span class="dl">"</span><span class="s2">helium.ns.hetzner.de.</span><span class="dl">"</span><span class="p">),</span>
    <span class="nc">NAMESERVER</span><span class="p">(</span><span class="dl">"</span><span class="s2">hydrogen.ns.hetzner.com.</span><span class="dl">"</span><span class="p">),</span>
    <span class="nc">NAMESERVER</span><span class="p">(</span><span class="dl">"</span><span class="s2">oxygen.ns.hetzner.com.</span><span class="dl">"</span><span class="p">)</span>
<span class="p">];</span>

<span class="nc">D</span><span class="p">(</span><span class="dl">"</span><span class="s2">production-ready.de</span><span class="dl">"</span><span class="p">,</span> <span class="nx">REG</span><span class="p">,</span> <span class="nc">DnsProvider</span><span class="p">(</span><span class="nx">DNS</span><span class="p">),</span>
    <span class="nc">A</span><span class="p">(</span><span class="dl">"</span><span class="s2">@</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">116.203.23.216</span><span class="dl">"</span><span class="p">),</span>
    <span class="nc">A</span><span class="p">(</span><span class="dl">"</span><span class="s2">www</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">116.203.23.216</span><span class="dl">"</span><span class="p">),</span>
    <span class="nc">AAAA</span><span class="p">(</span><span class="dl">"</span><span class="s2">@</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">2a01:4f8:c0c:bf6a::1</span><span class="dl">"</span><span class="p">),</span>
    <span class="nc">MX</span><span class="p">(</span><span class="dl">"</span><span class="s2">@</span><span class="dl">"</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="dl">"</span><span class="s2">mail.example.com.</span><span class="dl">"</span><span class="p">),</span>
    <span class="nx">HETZNER_NAMESERVER_RECORDS</span>
<span class="p">);</span>
</code></pre></div></div>

<p>For constructing more complex records, there are builder functions available. The resulting object can be easily included in the list of DNS entries.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">CAA</span> <span class="o">=</span> <span class="nc">CAA_BUILDER</span><span class="p">({</span>
    <span class="na">label</span><span class="p">:</span> <span class="dl">"</span><span class="s2">@</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">issue</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">letsencrypt.org</span><span class="dl">"</span><span class="p">],</span>
    <span class="na">iodef</span><span class="p">:</span> <span class="dl">"</span><span class="s2">mailto:administrator@example.com</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">iodef_critical</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="na">issuewild</span><span class="p">:</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span>
<span class="p">});</span>

<span class="kd">var</span> <span class="nx">DMARC</span> <span class="o">=</span> <span class="nc">DMARC_BUILDER</span><span class="p">({</span>
    <span class="na">policy</span><span class="p">:</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">subdomainPolicy</span><span class="p">:</span> <span class="dl">"</span><span class="s2">none</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">rua</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">mailto:postmaster@example.de</span><span class="dl">"</span><span class="p">],</span>
    <span class="na">ruf</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">mailto:postmaster@example.de</span><span class="dl">"</span><span class="p">],</span>
    <span class="na">reportInterval</span><span class="p">:</span> <span class="dl">"</span><span class="s2">7d</span><span class="dl">"</span>
<span class="p">});</span>

<span class="nc">D</span><span class="p">(</span><span class="dl">"</span><span class="s2">example.com</span><span class="dl">"</span><span class="p">,</span> <span class="nx">REG</span><span class="p">,</span> <span class="nc">DnsProvider</span><span class="p">(</span><span class="nx">DNS</span><span class="p">),</span>
    <span class="nc">A</span><span class="p">(</span><span class="dl">"</span><span class="s2">@</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">116.203.23.216</span><span class="dl">"</span><span class="p">),</span>
    <span class="p">...</span>
    <span class="nx">CAA</span><span class="p">,</span>
    <span class="nx">DMARC</span>
<span class="p">);</span>
</code></pre></div></div>

<p>If you have multiple domains to manage, it’s a good idea to split the configuration into multiple files. These files can then be imported into the main <code class="language-plaintext highlighter-rouge">dnsconfig.js</code> file.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">domains/production-ready.de.js</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="importing-existing-dns-zones">Importing Existing DNS Zones</h3>

<p>If you want to migrate existing DNS zones into DNSControl, you can save some typing by using <code class="language-plaintext highlighter-rouge">get-zones</code> with the output format set to <code class="language-plaintext highlighter-rouge">js</code>. Then, you can write the content to a file using <code class="language-plaintext highlighter-rouge">--out [file].js</code>. The generated code can serve as a starting point for your own configuration.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> dnscontrol get-zones <span class="nt">--format</span> js <span class="nt">--out</span> import.js hetzner - production-ready.de

var DSP_HETZNER <span class="o">=</span> NewDnsProvider<span class="o">(</span><span class="s2">"hetzner"</span><span class="o">)</span><span class="p">;</span>
var REG_CHANGEME <span class="o">=</span> NewRegistrar<span class="o">(</span><span class="s2">"none"</span><span class="o">)</span><span class="p">;</span>
D<span class="o">(</span><span class="s2">"production-ready.de"</span>, REG_CHANGEME,
        DnsProvider<span class="o">(</span>DSP_HETZNER<span class="o">)</span>,
        DefaultTTL<span class="o">(</span>3600<span class="o">)</span>,
        //NAMESERVER<span class="o">(</span><span class="s1">'oxygen.ns.hetzner.com.'</span><span class="o">)</span>,
        //NAMESERVER<span class="o">(</span><span class="s1">'hydrogen.ns.hetzner.com.'</span><span class="o">)</span>,
        //NAMESERVER<span class="o">(</span><span class="s1">'helium.ns.hetzner.de.'</span><span class="o">)</span>,
        A<span class="o">(</span><span class="s1">'@'</span>, <span class="s1">'116.203.23.216'</span><span class="o">)</span>,
        A<span class="o">(</span><span class="s1">'www'</span>, <span class="s1">'116.203.23.216'</span><span class="o">)</span>,
        ...
<span class="o">)</span>
</code></pre></div></div>

<h3 id="type-checking">Type-Checking</h3>

<p>To enable type-checking and code completion similar like TypeScript in your JavaScript files, DNSControl offers a useful feature: generating a TypeScript declaration file.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dnscontrol write-types
</code></pre></div></div>

<p>At the beginning of the <code class="language-plaintext highlighter-rouge">dnsconfig.js</code> file, you reference the generated TypeScript Declaration File.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @ts-check</span>
<span class="c1">/// &lt;reference path="types-dnscontrol.d.ts" /&gt;</span>

<span class="p">...</span>
</code></pre></div></div>

<h2 id="checking-and-applying-changes">Checking and Applying Changes</h2>

<p>For validating the configuration, DNSControl provides the <code class="language-plaintext highlighter-rouge">check</code> command. This command checks the JavaScript files for errors but doesn’t interact with any DNS provider. The <code class="language-plaintext highlighter-rouge">preview</code> command compares the current configuration with the state of DNS zones at the provider and displays potential changes. In the following example, a CNAME record is added, and an A record is modified.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check and validate dnsconfig.js</span>
<span class="o">&gt;</span> dnscontrol check
No errors.

<span class="c"># Preview changes</span>
<span class="o">&gt;</span> dnscontrol preview
<span class="k">********************</span> Domain: production-ready.de
2 corrections <span class="o">(</span>hetzner<span class="o">)</span>
<span class="c">#1: Batch creation of records:</span>
   + CREATE CNAME www2.production-ready.de www.production-ready.de. <span class="nv">ttl</span><span class="o">=</span>3600
<span class="c">#2: Batch modification of records:</span>
   ± MODIFY A production-ready.de: <span class="o">(</span>116.203.23.216 <span class="nv">ttl</span><span class="o">=</span>3600<span class="o">)</span> -&gt; <span class="o">(</span>192.168.0.1 <span class="nv">ttl</span><span class="o">=</span>3600<span class="o">)</span>
Done. 2 corrections.
</code></pre></div></div>

<p>If the changes align with the expected results, you can apply them using the <code class="language-plaintext highlighter-rouge">push</code> command.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> dnscontrol push

2 corrections <span class="o">(</span>hetzner<span class="o">)</span>
<span class="c">#1: Batch creation of records:</span>
   + CREATE CNAME www2.production-ready.de www.production-ready.de. <span class="nv">ttl</span><span class="o">=</span>3600
SUCCESS!
<span class="c">#2: Batch modification of records:</span>
   ± MODIFY A production-ready.de: <span class="o">(</span>116.203.23.216 <span class="nv">ttl</span><span class="o">=</span>3600<span class="o">)</span> -&gt; <span class="o">(</span>192.168.0.1 <span class="nv">ttl</span><span class="o">=</span>3600<span class="o">)</span>
SUCCESS!
Done. 2 corrections.
</code></pre></div></div>

<p>With these tools and approaches, you are capable of effectively and transparently managing an entire DNS landscape using an Infrastructure-as-Code approach.</p>

<h2 id="azure-devops-pipeline">Azure DevOps Pipeline</h2>

<p>To avoid the need to execute DNS configuration changes locally, creating a build pipeline is a convenient option. This pipeline can also be used to validate pull requests containing DNS changes. The provided code is tailored for Azure DevOps, but similar approaches can be adapted for other CI/CD systems.</p>

<p>If DNSControl is not installed on your build agent, such as when using Microsoft’s Hosted Agents or your own Docker-based agents, you can perform this installation through a simple CmdLine task. Create a folder, download the file, extract it, and make the binary executable.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Install</span><span class="nv"> </span><span class="s">DNSControl"</span>
  <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">mkdir -p $(Agent.TempDirectory)/bin</span>
        <span class="s">cd $(Agent.TempDirectory)/bin</span>
        <span class="s">wget -q https://github.com/StackExchange/dnscontrol/releases/download/v4.1.1/dnscontrol_4.1.1_linux_amd64.tar.gz -O dnscontrol.tar.gz</span>
        <span class="s">tar -xf dnscontrol.tar.gz dnscontrol</span>
        <span class="s">chmod +x dnscontrol</span>
</code></pre></div></div>

<p>Subsequently, you can validate the configuration and display the changes. The API key for the provider can be stored as a secret variable in the pipeline configuration or retrieved from an Azure KeyVault. When using a variable, explicit mapping to an environment variable is crucial; otherwise, the API key won’t be accessible in the script.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Run</span><span class="nv"> </span><span class="s">DNSControl</span><span class="nv"> </span><span class="s">check"</span>
  <span class="na">inputs</span><span class="pi">:</span> 
    <span class="na">script</span><span class="pi">:</span> <span class="s">$(Agent.TempDirectory)/bin/dnscontrol check</span>

<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Run</span><span class="nv"> </span><span class="s">DNSControl</span><span class="nv"> </span><span class="s">preview"</span>
  <span class="na">inputs</span><span class="pi">:</span> 
    <span class="na">script</span><span class="pi">:</span> <span class="s">$(Agent.TempDirectory)/bin/dnscontrol preview</span>
  <span class="na">env</span><span class="pi">:</span>
    <span class="na">DNSCONTROL_HETZNER_API_KEY</span><span class="pi">:</span> <span class="s">$(DNSCONTROL_HETZNER_API_KEY)</span>
</code></pre></div></div>

<p>If the changes align with the desired outcome, you can apply them in a deployment step.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">deployment</span><span class="pi">:</span> 
    <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Apply</span><span class="nv"> </span><span class="s">DNS</span><span class="nv"> </span><span class="s">Update"</span>
    <span class="na">environment</span><span class="pi">:</span> <span class="s">HETZNER_DNS_PROD</span>
    <span class="na">strategy</span><span class="pi">:</span>
      <span class="na">runOnce</span><span class="pi">:</span>
        <span class="na">deploy</span><span class="pi">:</span>
          <span class="na">steps</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
              <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Run</span><span class="nv"> </span><span class="s">DNSControl</span><span class="nv"> </span><span class="s">push"</span>
              <span class="na">inputs</span><span class="pi">:</span> 
                <span class="na">script</span><span class="pi">:</span> <span class="s">$(Agent.TempDirectory)/bin/dnscontrol push</span>
              <span class="na">env</span><span class="pi">:</span>
                <span class="na">DNSCONTROL_HETZNER_API_KEY</span><span class="pi">:</span> <span class="s">$(DNSCONTROL_HETZNER_API_KEY)</span>
</code></pre></div></div>

<picture><source srcset="/generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-400-b2f07a7ca.webp 400w, /generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-600-b2f07a7ca.webp 600w, /generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-800-b2f07a7ca.webp 800w, /generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-859-b2f07a7ca.webp 859w" type="image/webp" /><source srcset="/generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-400-784384f3f.png 400w, /generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-600-784384f3f.png 600w, /generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-800-784384f3f.png 800w, /generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-859-784384f3f.png 859w" type="image/png" /><img src="/generated/2023-07-02-dnscontrol/screenshot-dnscontrol-azure-pipeline-800-784384f3f.png" alt="DNSControl in Azure DevOps Pipeline" /></picture>]]></content><author><name>David</name></author><category term="dns" /><category term="dnscontrol" /><category term="devops" /><category term="en" /><summary type="html"><![CDATA[Configuration of DNS zones is a necessary part of most (web) software projects. Whether during the initial setup or over time, domains need to be registered, and DNS records need to be set up or adjusted. While this can often be done manually through the web interface of the corresponding provider, it’s error-prone, hard to track, and not easy to automate. If you use Infrastructure-as-Code (IaC) tools like Terraform for provisioning your cloud infrastructure, you can also manage DNS configuration through it. However, this approach often restricts you to the major hyperscale cloud providers for DNS services. DNSControl provides a simpler and less configuration-intensive way to define DNS configuration for dozens of large and small providers using JavaScript code. This allows you to set up and manage your DNS environment in an automated and reproducible manner. The configuration is versioned in a Git repository and can undergo code reviews, seamlessly integrating the process into the DevOps toolchain.]]></summary></entry><entry xml:lang="en"><title type="html">Property-based testing in C# with FsCheck</title><link href="https://www.production-ready.de/2023/06/10/property-based-testing-in-csharp-en.html" rel="alternate" type="text/html" title="Property-based testing in C# with FsCheck" /><published>2023-06-10T00:00:00+02:00</published><updated>2023-06-10T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/06/10/property-based-testing-in-csharp-en</id><content type="html" xml:base="https://www.production-ready.de/2023/06/10/property-based-testing-in-csharp-en.html"><![CDATA[<p>As part of the toolkit for software development, writing (meaningful) automated tests is essential. Through unit, integration, and end-to-end tests, the proper functionality of our code is ensured, and we gain confidence that when implementing new requirements and refactoring existing code, we won’t break any existing functionality. Michael Feathers even defines legacy software as code that lacks tests.</p>

<blockquote>
  <p>legacy code is code without tests</p>

  <p>– <cite>Michael Feathers</cite></p>
</blockquote>

<p>In addition to common approaches like example-based testing or snapshot testing, there are other methods that help us verify the correctness of our code. One of these approaches is property-based testing.</p>

<p>Property-based testing doesn’t check individual values for equality; rather, it aims to verify properties that our implementation must exhibit. For example, in mathematical addition, commutativity is a property that we can check without looking at specific numbers. For the addition of two numbers <code class="language-plaintext highlighter-rouge">a</code> and <code class="language-plaintext highlighter-rouge">b</code>, the commutative property always holds:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>a + b = b + a
</code></pre></div></div>

<!--more-->

<h2 id="property-based-testing">Property-based testing</h2>

<p><code class="language-plaintext highlighter-rouge">Property testing</code> or <code class="language-plaintext highlighter-rouge">Property-based testing</code> (PBT) is a testing technique that has become popular in functional programming, often associated with tools like <a href="https://hackage.haskell.org/package/QuickCheck">QuickCheck</a> in Haskell. It is well-suited for problem domains where verifying properties is much simpler than solving the problems themselves. However, it can also be valuable for testing more common scenarios.</p>

<p>In property-based testing, you define properties that the code under test must satisfy for a range of input data. Note that a property, in this context, isn’t the same as a C# property.</p>

<p>For instance, the property of <code class="language-plaintext highlighter-rouge">commutativity</code> in mathematical addition can be described as a function in C#:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">bool</span><span class="p">&gt;</span> <span class="n">commutativity</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">a</span> <span class="p">+</span> <span class="n">b</span> <span class="p">==</span> <span class="n">b</span> <span class="p">+</span> <span class="n">a</span><span class="p">;</span>
</code></pre></div></div>

<p>The definition of <code class="language-plaintext highlighter-rouge">Addition</code> states that the <code class="language-plaintext highlighter-rouge">commutative law</code> holds true for all combinations of <code class="language-plaintext highlighter-rouge">a</code> and <code class="language-plaintext highlighter-rouge">b</code>. This means that our <code class="language-plaintext highlighter-rouge">commutativity</code> function must return <code class="language-plaintext highlighter-rouge">true</code> for all integer values.</p>

<p>When we implement our own <code class="language-plaintext highlighter-rouge">Addition</code> function, we can use this property to verify the correctness of our implementation.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">bool</span><span class="p">&gt;</span> <span class="n">commutativity</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span> 
    <span class="p">=&gt;</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="p">==</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">a</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="example-based-testing">Example-based testing</h2>

<p>A naive approach to test the correctness of our <code class="language-plaintext highlighter-rouge">Addition</code> function is to call the function with a few example values and verify the return values, here using <a href="https://nunit.org">NUnit</a>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">TestCase</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">0</span><span class="p">)]</span>
<span class="p">[</span><span class="nf">TestCase</span><span class="p">(</span><span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">)]</span>
<span class="p">[</span><span class="nf">TestCase</span><span class="p">(</span><span class="m">999</span><span class="p">,</span> <span class="m">9999</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">TestCommutativity_WithExamples</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">left</span> <span class="p">=</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">right</span> <span class="p">=</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">a</span><span class="p">);</span>

    <span class="n">Assert</span><span class="p">.</span><span class="nf">AreEqual</span><span class="p">(</span><span class="n">left</span><span class="p">,</span> <span class="n">right</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Since we are not interested in the specific values of <code class="language-plaintext highlighter-rouge">a</code> and <code class="language-plaintext highlighter-rouge">b</code>, we can generate them randomly. This can be done for any number of combinations of <code class="language-plaintext highlighter-rouge">a</code> and <code class="language-plaintext highlighter-rouge">b</code>, in this case, it’s 1,000 iterations. By doing this, we have tested our <code class="language-plaintext highlighter-rouge">Addition</code> function with 1,000 randomly generated values instead of just three concrete values. This makes us more confident that our <code class="language-plaintext highlighter-rouge">Addition</code> function is working correctly.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">TestCommutativity_WithRandomValues</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">var</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="m">1_000</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">a</span> <span class="p">=</span> <span class="n">Random</span><span class="p">.</span><span class="n">Shared</span><span class="p">.</span><span class="nf">Next</span><span class="p">(-</span><span class="m">999</span><span class="p">,</span> <span class="m">999</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">b</span> <span class="p">=</span> <span class="n">Random</span><span class="p">.</span><span class="n">Shared</span><span class="p">.</span><span class="nf">Next</span><span class="p">(-</span><span class="m">123</span><span class="p">,</span> <span class="m">321</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">left</span> <span class="p">=</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">right</span> <span class="p">=</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">b</span><span class="p">,</span> <span class="n">a</span><span class="p">);</span>

        <span class="n">Assert</span><span class="p">.</span><span class="nf">AreEqual</span><span class="p">(</span><span class="n">left</span><span class="p">,</span> <span class="n">right</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>However, this approach also comes with a few drawbacks: It’s clear that it requires more code, and our test case becomes more complex. Additionally, the generated values are chosen randomly and may not cover the entire range of possible input values.</p>

<p>A more significant issue is that if an error occurs, we won’t know which input values triggered the error since they are randomly generated in each iteration.</p>

<h2 id="fscheck">FsCheck</h2>

<p>The library <a href="https://fscheck.github.io/FsCheck/">FsCheck</a> written in F# provides a solution to this issue and can also be used in C#. The comprehensive <a href="https://fscheck.github.io/FsCheck//Properties.html">documentation</a> primarily uses F# syntax but also provides examples in C#.</p>

<p>It offers functions to simplify property testing. A test in <code class="language-plaintext highlighter-rouge">FsCheck</code> essentially consists of the following three parts:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&gt; for all (x, y, ..)
&gt; such as precondition(x, y, ...) holds 
&gt; property(x, y, ...) is satisfied
</code></pre></div></div>

<p>This way, we can test the <code class="language-plaintext highlighter-rouge">Commutativity</code> property using FsCheck as follows:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Test</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">TestCommutativity</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="kt">int</span> <span class="n">y</span><span class="p">)</span>
        <span class="p">=&gt;</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">==</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">x</span><span class="p">);</span>

    <span class="n">Prop</span><span class="p">.</span><span class="nf">ForAll</span><span class="p">(</span>
            <span class="n">Arb</span><span class="p">.</span><span class="n">From</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;(),</span> <span class="c1">// Parameter x</span>
            <span class="n">Arb</span><span class="p">.</span><span class="n">From</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;(),</span> <span class="c1">// Parameter y</span>
            <span class="n">property</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">QuickCheckThrowOnFailure</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It becomes even easier when we use the extension <a href="https://www.nuget.org/packages/FsCheck.NUnit">FsCheck.NUnit</a> (equivalent to <a href="https://www.nuget.org/packages/FsCheck.Xunit/">FsCheck.Xunit</a> for XUnit). Instead of annotating our test method with <code class="language-plaintext highlighter-rouge">[Test]</code> or <code class="language-plaintext highlighter-rouge">[Fact]</code>, we annotate it with <code class="language-plaintext highlighter-rouge">[Property]</code> and can directly pass the properties as parameters. FsCheck automatically generates input values of the corresponding type.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="kt">bool</span> <span class="nf">TestCommutativity</span><span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="kt">int</span> <span class="n">y</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">==</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">x</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If we want to learn something about the distribution of the generated input values, we can classify them based on certain conditions. By calling <code class="language-plaintext highlighter-rouge">Classify()</code>, we get a value of type <code class="language-plaintext highlighter-rouge">Property</code>, so we need to adjust the return value of the test method accordingly.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">TestCommutativity_WithClassification</span><span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="kt">int</span> <span class="n">y</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">==</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">x</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">property</span>
        <span class="p">.</span><span class="nf">Classify</span><span class="p">(</span><span class="n">x</span> <span class="p">==</span> <span class="m">0</span><span class="p">,</span> <span class="s">"x == 0"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">Classify</span><span class="p">(</span><span class="n">x</span> <span class="p">&gt;</span> <span class="m">0</span><span class="p">,</span> <span class="s">"x &gt; 0"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">Classify</span><span class="p">(</span><span class="n">x</span> <span class="p">&lt;</span> <span class="m">0</span><span class="p">,</span> <span class="s">"x &lt; 0"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The corresponding output provides results similar to the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ok, passed 100 tests.
50% x &gt; 0.
46% x &lt; 0.
4% x == 0.
</code></pre></div></div>

<p>If you want to have an output for each individual test case, the <code class="language-plaintext highlighter-rouge">Collect()</code> method can be helpful.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">TestCommutativity_WithCollect</span><span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="kt">int</span> <span class="n">y</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">==</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">x</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">property</span>
        <span class="p">.</span><span class="nf">Collect</span><span class="p">(</span><span class="s">$"x: </span><span class="p">{</span><span class="n">x</span><span class="p">}</span><span class="s">, y: </span><span class="p">{</span><span class="n">y</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The output provides a line for each combination along with its distribution.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ok, passed 100 tests.
2% "x: 5, y: 9".
2% "x: 0, y: 0".
2% "x: -16, y: -6".
1% "x: 9, y: 32".
1% "x: 9, y: -26".
...
</code></pre></div></div>

<h3 id="conditions">Conditions</h3>

<p>If not all values of the data type of an input parameter are valid for a specific test case, we can use <code class="language-plaintext highlighter-rouge">When()</code> to define a precondition. To prevent dividing by 0 when testing <code class="language-plaintext highlighter-rouge">Division</code>, we exclude that case using this approach.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">TestDivide</span><span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="kt">int</span> <span class="n">y</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Divide</span><span class="p">(</span><span class="n">x</span> <span class="p">*</span> <span class="n">y</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">==</span> <span class="n">x</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">property</span><span class="p">.</span><span class="nf">When</span><span class="p">(</span><span class="n">y</span> <span class="p">!=</span> <span class="m">0</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Important to note: The definition of the <code class="language-plaintext highlighter-rouge">property</code> is executed as a method to allow for <code class="language-plaintext highlighter-rouge">lazy evaluation</code> by FsCheck.</p>

<h3 id="generator-shrinker-arbitrary">Generator, Shrinker, Arbitrary</h3>

<p>To generate suitable <a href="https://fscheck.github.io/FsCheck//TestData.html">test data</a>, FsCheck uses <code class="language-plaintext highlighter-rouge">Generators</code>, <code class="language-plaintext highlighter-rouge">Shrinker</code>, and <code class="language-plaintext highlighter-rouge">Arbitraries</code>. The <code class="language-plaintext highlighter-rouge">Gen</code> class provides the foundation for generating values with its three functions: <code class="language-plaintext highlighter-rouge">Choose()</code>, <code class="language-plaintext highlighter-rouge">Constant()</code>, and <code class="language-plaintext highlighter-rouge">OneOf()</code>. Values can be transformed and filtered using the <code class="language-plaintext highlighter-rouge">Select()</code> and <code class="language-plaintext highlighter-rouge">Where()</code> methods.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Generate a list of numbers between 0 and 100 that are divisible by 2</span>
<span class="kt">var</span> <span class="n">generator</span> <span class="p">=</span> <span class="n">Gen</span><span class="p">.</span><span class="nf">Choose</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">100</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">i</span> <span class="p">=&gt;</span> <span class="n">i</span> <span class="p">%</span> <span class="m">2</span> <span class="p">==</span> <span class="m">0</span><span class="p">);</span>

<span class="c1">// Randomly choose one of the two values, "yes" or "no"</span>
<span class="kt">var</span> <span class="n">generator</span> <span class="p">=</span> <span class="n">Gen</span><span class="p">.</span><span class="nf">OneOf</span><span class="p">(</span>
        <span class="n">Gen</span><span class="p">.</span><span class="nf">Constant</span><span class="p">(</span><span class="k">true</span><span class="p">),</span>
        <span class="n">Gen</span><span class="p">.</span><span class="nf">Constant</span><span class="p">(</span><span class="k">false</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">b</span> <span class="p">=&gt;</span> <span class="n">b</span> <span class="p">?</span> <span class="s">"yes"</span> <span class="p">:</span> <span class="s">"no"</span><span class="p">);</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Sample()</code> method of a generator provides a list of concrete values. The first parameter, <code class="language-plaintext highlighter-rouge">size</code>, has different effects depending on the generator, such as determining the range of values. In the case of <code class="language-plaintext highlighter-rouge">Gen.Choose()</code>, it has no effect. The second parameter, <code class="language-plaintext highlighter-rouge">numberOfSamples</code>, determines the number of elements in the generated list.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">sample</span> <span class="p">=</span> <span class="n">Gen</span><span class="p">.</span><span class="nf">Choose</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">100</span><span class="p">).</span><span class="nf">Sample</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="m">10</span><span class="p">);</span>
<span class="c1">// 22, 6, 16, 8, 27, 22, 14, 49, 42, 99</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Shrinkers</code> are used to simplify the process of finding errors. They attempt to find the smallest value for a failed test run, at which point the property being tested no longer holds true. In the following test, if the value is greater than or equal to 20, the test fails. By using shrinking, FsCheck can identify that the first value that causes the test to fail is 20.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="kt">bool</span> <span class="nf">Test_Shrink</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="k">value</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="k">value</span><span class="p">.</span><span class="n">Item</span> <span class="p">&lt;</span> <span class="m">20</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Output:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Falsifiable, after 25 tests (2 shrinks) (StdGen (195734675,297194399)):
Original:
PositiveInt 24
Shrunk:
PositiveInt 20
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Arbitraries</code> combine <code class="language-plaintext highlighter-rouge">Generators</code> and <code class="language-plaintext highlighter-rouge">Shrinkers</code> and serve as inputs for property tests. The <code class="language-plaintext highlighter-rouge">Arb</code> class provides a set of default implementations for various data types. It can also be used to generate <code class="language-plaintext highlighter-rouge">Generators</code> for complex types.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">arbitrary1</span> <span class="p">=</span> <span class="n">Arb</span><span class="p">.</span><span class="n">Default</span><span class="p">.</span><span class="nf">PositiveInt</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">arbitrary2</span> <span class="p">=</span> <span class="n">Arb</span><span class="p">.</span><span class="n">Default</span><span class="p">.</span><span class="nf">DateTime</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">arbitrary3</span> <span class="p">=</span> <span class="n">Arb</span><span class="p">.</span><span class="n">Default</span><span class="p">.</span><span class="nf">IPv4Address</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">arbitrary4</span> <span class="p">=</span> <span class="n">Arb</span><span class="p">.</span><span class="n">From</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">Filter</span><span class="p">(</span><span class="n">i</span> <span class="p">=&gt;</span> <span class="n">i</span> <span class="p">%</span> <span class="m">2</span> <span class="p">==</span> <span class="m">0</span><span class="p">);</span>

<span class="c1">// --- </span>

<span class="n">Gen</span><span class="p">&lt;</span><span class="n">Point</span><span class="p">&gt;</span> <span class="n">generator</span> <span class="p">=</span> <span class="n">Arb</span><span class="p">.</span><span class="n">Generate</span><span class="p">&lt;</span><span class="n">Point</span><span class="p">&gt;();</span>

<span class="c1">// --- </span>

<span class="n">Gen</span><span class="p">&lt;</span><span class="n">Point</span><span class="p">&gt;</span> <span class="n">generator</span> <span class="p">=</span> <span class="k">from</span> <span class="n">x</span> <span class="k">in</span> <span class="n">Arb</span><span class="p">.</span><span class="n">Default</span><span class="p">.</span><span class="nf">PositiveInt</span><span class="p">().</span><span class="n">Generator</span>
                       <span class="k">from</span> <span class="n">y</span> <span class="k">in</span> <span class="n">Arb</span><span class="p">.</span><span class="n">Default</span><span class="p">.</span><span class="nf">NegativeInt</span><span class="p">().</span><span class="n">Generator</span>
                       <span class="n">select</span> <span class="k">new</span> <span class="nf">Point</span><span class="p">(</span><span class="n">x</span><span class="p">.</span><span class="n">Item</span><span class="p">,</span> <span class="n">y</span><span class="p">.</span><span class="n">Item</span><span class="p">);</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Property</code> attribute’s properties can be used to control the distribution of values.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Property</span><span class="p">(</span><span class="n">StartSize</span> <span class="p">=</span> <span class="m">0</span><span class="p">,</span> <span class="n">EndSize</span> <span class="p">=</span> <span class="m">10</span><span class="p">)]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">Test_Distribution</span><span class="p">(</span><span class="kt">int</span> <span class="k">value</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="k">true</span><span class="p">;</span>

    <span class="k">return</span> <span class="n">property</span><span class="p">.</span><span class="nf">Collect</span><span class="p">(</span><span class="s">$"value: </span><span class="p">{</span><span class="k">value</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The values in the middle of the value range (here 0, since both positive and negative numbers are generated) are generated more frequently than the values at the edges. The specific values for <code class="language-plaintext highlighter-rouge">StartSize</code> and <code class="language-plaintext highlighter-rouge">EndSize</code> depend on the generator.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ok, passed 100 tests.
18% "value: 0".
13% "value: 1".
9% "value: 2".
8% "value: -2".
7% "value: -4".
7% "value: -1".
5% "value: 5".
5% "value: 3".
5% "value: -8".
5% "value: -6".
4% "value: 4".
3% "value: 7".
2% "value: 9".
2% "value: 8".
2% "value: -7".
1% "value: 6".
1% "value: -9".
1% "value: -5".
1% "value: -3".
1% "value: -10".
</code></pre></div></div>

<h3 id="arguments-exhausted-after-x-tests">Arguments exhausted after X tests</h3>

<p>FsCheck performs 100 test runs by default. The <code class="language-plaintext highlighter-rouge">MaxTest</code> property allows you to control the number of runs. If you restrict the valid options too much, it’s possible that FsCheck won’t be able to generate any values and will throw an exception. This helps prevent excessive runtime for individual tests.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Property</span><span class="p">(</span><span class="n">MaxTest</span> <span class="p">=</span> <span class="m">100</span><span class="p">)]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">Test_Exhausted</span><span class="p">(</span><span class="kt">int</span> <span class="k">value</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="k">true</span><span class="p">;</span>

    <span class="k">return</span> <span class="n">property</span>
        <span class="p">.</span><span class="nf">When</span><span class="p">(</span><span class="k">value</span> <span class="p">%</span> <span class="m">15</span> <span class="p">==</span> <span class="m">0</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The test fails with a message:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Exhausted: Arguments exhausted after 61 tests.
</code></pre></div></div>

<p>Now you have the option to reduce the number of test runs. However, you should also consider whether you need to narrow down the valid range of values too much. In the example, using an <code class="language-plaintext highlighter-rouge">int</code> value is not a well-chosen data type.</p>

<h2 id="addition-example">Addition Example</h2>

<p>To complete the example of <a href="https://en.wikipedia.org/wiki/Addition">addition</a>, let’s now test the remaining properties of <code class="language-plaintext highlighter-rouge">associativity</code>, <code class="language-plaintext highlighter-rouge">identity</code>, and <code class="language-plaintext highlighter-rouge">inverse</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="kt">bool</span> <span class="nf">TestCommutativity</span><span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="kt">int</span> <span class="n">y</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// x + y == y + x</span>
    <span class="k">return</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span> <span class="p">==</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">x</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="kt">bool</span> <span class="nf">TestAssociativity</span><span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">,</span> <span class="kt">int</span> <span class="n">y</span><span class="p">,</span> <span class="kt">int</span> <span class="n">z</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// x + (y + z) == (x + y) + z</span>
    <span class="k">return</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">))</span> <span class="p">==</span>
           <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">),</span> <span class="n">z</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="kt">bool</span> <span class="nf">TestAdditiveIdentity</span><span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// x + 0 == x</span>
    <span class="k">return</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="m">0</span><span class="p">)</span> <span class="p">==</span> <span class="n">x</span><span class="p">;</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="kt">bool</span> <span class="nf">TestAdditiveInverse</span><span class="p">(</span><span class="kt">int</span> <span class="n">x</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// x + (-x) == 0</span>
    <span class="k">return</span> <span class="n">MathOperations</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="p">-</span><span class="n">x</span><span class="p">)</span> <span class="p">==</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="fizzbuzz-kata">FizzBuzz Kata</h2>

<p>For the well-known <a href="https://codingdojo.org/kata/FizzBuzz/">FizzBuzz kata</a>, the objective is to generate one of the following strings for a positive number:</p>

<ul>
  <li>“Fizz” if the number is divisible by 3</li>
  <li>“Buzz” if the number is divisible by 5</li>
  <li>“FizzBuzz” if the number is divisible by both 3 and 5</li>
  <li>the number itself, otherwise</li>
</ul>

<p>A property-based test to verify a potential solution could look like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">Should_Get_Fizz</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="n">n</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="n">FizzBuzzer</span><span class="p">.</span><span class="nf">GetFizzBuzz</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span><span class="p">).</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"Fizz"</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">property</span><span class="p">.</span><span class="nf">When</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span> <span class="p">%</span> <span class="m">3</span> <span class="p">==</span> <span class="m">0</span> <span class="p">&amp;&amp;</span> <span class="n">n</span><span class="p">.</span><span class="n">Item</span> <span class="p">%</span> <span class="m">5</span> <span class="p">!=</span> <span class="m">0</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">Should_Get_Buzz</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="n">n</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="n">FizzBuzzer</span><span class="p">.</span><span class="nf">GetFizzBuzz</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span><span class="p">).</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"Buzz"</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">property</span><span class="p">.</span><span class="nf">When</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span> <span class="p">%</span> <span class="m">3</span> <span class="p">!=</span> <span class="m">0</span> <span class="p">&amp;&amp;</span> <span class="n">n</span><span class="p">.</span><span class="n">Item</span> <span class="p">%</span> <span class="m">5</span> <span class="p">==</span> <span class="m">0</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">[</span><span class="nf">Property</span><span class="p">(</span><span class="n">MaxTest</span> <span class="p">=</span> <span class="m">10</span><span class="p">)]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">Should_Get_FizzBuzz</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="n">n</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="n">FizzBuzzer</span><span class="p">.</span><span class="nf">GetFizzBuzz</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span><span class="p">).</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"FizzBuzz"</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">property</span><span class="p">.</span><span class="nf">When</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span> <span class="p">%</span> <span class="m">3</span> <span class="p">==</span> <span class="m">0</span> <span class="p">&amp;&amp;</span> <span class="n">n</span><span class="p">.</span><span class="n">Item</span> <span class="p">%</span> <span class="m">5</span> <span class="p">==</span> <span class="m">0</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">Property</span><span class="p">]</span>
<span class="k">public</span> <span class="n">Property</span> <span class="nf">Should_Get_Number</span><span class="p">(</span><span class="n">PositiveInt</span> <span class="n">n</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">property</span> <span class="p">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="n">FizzBuzzer</span><span class="p">.</span><span class="nf">GetFizzBuzz</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span><span class="p">).</span><span class="nf">Equals</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>

    <span class="k">return</span> <span class="n">property</span><span class="p">.</span><span class="nf">When</span><span class="p">(</span><span class="n">n</span><span class="p">.</span><span class="n">Item</span> <span class="p">%</span> <span class="m">3</span> <span class="p">!=</span> <span class="m">0</span> <span class="p">&amp;&amp;</span> <span class="n">n</span><span class="p">.</span><span class="n">Item</span> <span class="p">%</span> <span class="m">5</span> <span class="p">!=</span> <span class="m">0</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A small example project with the demonstrated source code can be found on <a href="https://github.com/davull/demo-property-based-testing">GitHub</a>.</p>]]></content><author><name>David</name></author><category term="c#" /><category term="testing" /><category term="fscheck" /><category term="en" /><summary type="html"><![CDATA[As part of the toolkit for software development, writing (meaningful) automated tests is essential. Through unit, integration, and end-to-end tests, the proper functionality of our code is ensured, and we gain confidence that when implementing new requirements and refactoring existing code, we won’t break any existing functionality. Michael Feathers even defines legacy software as code that lacks tests. legacy code is code without tests – Michael Feathers In addition to common approaches like example-based testing or snapshot testing, there are other methods that help us verify the correctness of our code. One of these approaches is property-based testing. Property-based testing doesn’t check individual values for equality; rather, it aims to verify properties that our implementation must exhibit. For example, in mathematical addition, commutativity is a property that we can check without looking at specific numbers. For the addition of two numbers a and b, the commutative property always holds: a + b = b + a]]></summary></entry><entry xml:lang="en"><title type="html">Devcontainer with Visual Studio Code</title><link href="https://www.production-ready.de/2023/05/06/devcontainer-with-vscode-en.html" rel="alternate" type="text/html" title="Devcontainer with Visual Studio Code" /><published>2023-05-06T00:00:00+02:00</published><updated>2023-05-06T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/05/06/devcontainer-with-vscode-en</id><content type="html" xml:base="https://www.production-ready.de/2023/05/06/devcontainer-with-vscode-en.html"><![CDATA[<p>For running applications, especially in the cloud environment, the use of containers has become established. With the advent of Docker, it became easy to execute software in a defined environment that can be easily reproduced. All runtime dependencies are packaged into a container image and can be run on a different system effortlessly. The disparity between different stages in the development process (Development, Testing, Production) is reduced, and “works on my machine” is no longer a valid excuse.</p>

<p>To make not only the runtime but also the development environment reproducible and portable, <a href="https://containers.dev">Development Containers</a> are a suitable solution.</p>

<!--more-->

<h2 id="development-container">Development Container</h2>

<p>Creating a development environment nowadays often requires more than just a text editor or an IDE. Runtimes, compilers, CLIs, and various tools are needed to develop software. If you work with multiple programming languages or projects, the setup process can quickly consume several hours. Another issue arises when different versions of software are needed. Node.js, for instance, is an example for which the <a href="https://github.com/jasongin/nvs">Node Version Switcher</a> provides more of a workaround than a good solution.</p>

<p>To address these problems, pre-configured virtual machines have been used for a long time, either executed locally or accessed remotely through protocols like SSH or RDP. However, the developer experience is often suboptimal in both cases.</p>

<p>When using <a href="https://code.visualstudio.com">Visual Studio Code</a> for development, utilizing <a href="https://containers.dev">Devcontainers</a> offers a lightweight alternative. Currently, besides VS Code, only Visual Studio supports the use of Devcontainers, and <a href="https://devblogs.microsoft.com/cppblog/dev-containers-for-c-in-visual-studio/">only for C++ projects</a>. If your workflow can accommodate these limitations, Devcontainers are a great option. An additional advantage is that Devcontainers behave consistently across different computers and operating systems, making them suitable for team collaboration. Configuration can be stored alongside the source code in a Git repository and versioned.</p>

<h2 id="devcontainers-in-vs-code">Devcontainers in VS Code</h2>

<p>The prerequisites for using Devcontainers in VS Code include having an installed version of <a href="https://www.docker.com/products/docker-desktop/">Docker</a> and the <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers">Dev Containers extension</a>, which is provided directly by Microsoft. Through the <a href="https://code.visualstudio.com/api/ux-guidelines/command-palette">Command Palette</a>, this extension offers a set of commands to create and manage Devcontainers.</p>

<picture><source srcset="/generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-400-5a1003c99.webp 400w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-600-5a1003c99.webp 600w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-800-5a1003c99.webp 800w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-992-5a1003c99.webp 992w" type="image/webp" /><source srcset="/generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-400-d2f90f24d.png 400w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-600-d2f90f24d.png 600w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-800-d2f90f24d.png 800w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-992-d2f90f24d.png 992w" type="image/png" /><img src="/generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-command-palette-800-d2f90f24d.png" alt="The VS Code Command Palette with commands for using Devcontainers" /></picture>

<h3 id="creating-a-devcontainer">Creating a Devcontainer</h3>

<p>To create a Devcontainer, a JSON file named <code class="language-plaintext highlighter-rouge">devcontainer.json</code> in the <code class="language-plaintext highlighter-rouge">.devcontainer</code> subfolder is all you need. The <a href="https://containers.dev/implementors/json_reference/">definition</a> of a container essentially consists of three sections: Image, Features, and Configuration. When you choose the <code class="language-plaintext highlighter-rouge">Dev Containers: Add Dev Container Configuration Files...</code> command in VS Code, you will be guided through these three sections.</p>

<ul>
  <li>
    <p><a href="https://containers.dev/implementors/json_reference/#image-specific">Image</a>: Describes the Docker image that serves as the base for the Devcontainer. You can use a <a href="https://containers.dev/templates">pre-configured Devcontainer image</a> or any other Docker image. Alternatively, you can specify a Dockerfile or a Docker Compose file to create your own image.</p>
  </li>
  <li>
    <p><a href="https://containers.dev/implementors/features/">Features</a>: Packages and options to add and configure software to the base image. This can include tools like Git, various CLIs, Docker, or even entire development stacks for programming languages such as Go, .NET, or PHP as features. There are <a href="https://containers.dev/features">some predefined features</a>, or you can create <a href="https://containers.dev/implementors/features/">custom features</a> using shell scripts.</p>
  </li>
  <li>
    <p><a href="https://containers.dev/implementors/spec/#other-options">Configuration</a>: Configuration includes aspects like port forwarding, lifecycle scripts, or adjustments for VS Code.</p>
  </li>
</ul>

<p>The following configuration uses the Node.js 20 base image with TypeScript, additionally installs the AWS and GitHub CLI, and runs the <code class="language-plaintext highlighter-rouge">yarn install</code> command after creating the container:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"$schema"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Node.js &amp; TypeScript"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"image"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mcr.microsoft.com/devcontainers/typescript-node:0-20"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"features"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ghcr.io/devcontainers/features/aws-cli:1"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"latest"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"ghcr.io/devcontainers/features/github-cli:1"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"installDirectlyFromGitHubRelease"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"latest"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="err">//</span><span class="w"> </span><span class="err">Use</span><span class="w"> </span><span class="err">'postCreateCommand'</span><span class="w"> </span><span class="err">to</span><span class="w"> </span><span class="err">run</span><span class="w"> </span><span class="err">commands</span><span class="w"> </span><span class="err">after</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">container</span><span class="w"> </span><span class="err">is</span><span class="w"> </span><span class="err">created.</span><span class="w">
  </span><span class="nl">"postCreateCommand"</span><span class="p">:</span><span class="w"> </span><span class="s2">"yarn install"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>When you use the VS Code Command Palette to execute the <code class="language-plaintext highlighter-rouge">Dev Containers: Reopen in Container</code> command, the Devcontainer is created, and VS Code establishes a connection to the container. The creation process may take a few minutes as it involves downloading the base image and installing the specified features. The root folder of your project is mounted inside the container.</p>

<p>Once the container is up and running, VS Code displays the mounted folder in the Explorer, allowing you to start your development work. You can access a shell running inside the container by clicking the <code class="language-plaintext highlighter-rouge">+</code> icon in the Terminal window. This provides a seamless environment for development within the Devcontainer.</p>

<picture><source srcset="/generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-400-09ec62062.webp 400w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-600-09ec62062.webp 600w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-800-09ec62062.webp 800w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-1000-09ec62062.webp 1000w" type="image/webp" /><source srcset="/generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-400-31f8a79db.png 400w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-600-31f8a79db.png 600w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-800-31f8a79db.png 800w, /generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-1000-31f8a79db.png 1000w" type="image/png" /><img src="/generated/2023-05-06-devcontainer-with-vscode/screenshot-vscode-new-devcontainer-800-31f8a79db.png" alt="VS Code with Devcontainer" /></picture>

<h3 id="creating-a-custom-devcontainer-image">Creating a Custom Devcontainer Image</h3>

<p>If a preconfigured image combined with the available features doesn’t meet your requirements, you can create a custom Devcontainer image using a Dockerfile.</p>

<p>For a basic Ruby setup, your <code class="language-plaintext highlighter-rouge">Dockerfile</code> might look like this:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> ruby:3.0-bullseye</span>

<span class="k">RUN </span>apt-get update <span class="o">&amp;&amp;</span> <span class="nb">export </span><span class="nv">DEBIAN_FRONTEND</span><span class="o">=</span>noninteractive <span class="se">\
</span>  <span class="o">&amp;&amp;</span> apt-get <span class="nt">-y</span> <span class="nb">install</span> <span class="nt">--no-install-recommends</span> nano libvips libvips-dev libvips-tools

<span class="k">RUN </span>gem <span class="nb">install </span>jekyll bundler
</code></pre></div></div>

<p>Inside the <code class="language-plaintext highlighter-rouge">devcontainer.json</code> file, instead of using the <code class="language-plaintext highlighter-rouge">image</code> keyword, you provide the Dockerfile path and the build context under the <code class="language-plaintext highlighter-rouge">build</code> section. The <code class="language-plaintext highlighter-rouge">Features</code> block is used to install <a href="https://github.com/devcontainers/features/tree/main/src/common-utils">ZSH as the default shell</a> and <a href="https://github.com/devcontainers/features/tree/main/src/git">git</a>. Additionally, VS Code is instructed to install the Ruby and Github Copilot extensions. Using the <code class="language-plaintext highlighter-rouge">postCreateCommand</code> entry ensures that the required gems are installed.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"$schema"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Ruby DevContainer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"dockerfile"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Dockerfile"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"context"</span><span class="p">:</span><span class="w"> </span><span class="s2">"."</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"features"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ghcr.io/devcontainers/features/common-utils:2"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"installZsh"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"configureZshAsDefaultShell"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"installOhMyZsh"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vscode"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"userUid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1000"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"userGid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1000"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"upgradePackages"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"nonFreePackages"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"ghcr.io/devcontainers/features/git:1"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"latest"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"ppa"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"customizations"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"vscode"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"extensions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"rebornix.Ruby"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"GitHub.copilot"</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"forwardPorts"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
  </span><span class="nl">"postCreateCommand"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bundler install --gemfile=./Gemfile &amp;&amp; ruby --version"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"remoteUser"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vscode"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>If you want to install additional software or have made changes to the container image, you can simply create a new image using the command <code class="language-plaintext highlighter-rouge">Dev Containers: Rebuild and Reopen in Container</code>.</p>]]></content><author><name>David</name></author><category term="devcontainer" /><category term="vscode" /><category term="en" /><summary type="html"><![CDATA[For running applications, especially in the cloud environment, the use of containers has become established. With the advent of Docker, it became easy to execute software in a defined environment that can be easily reproduced. All runtime dependencies are packaged into a container image and can be run on a different system effortlessly. The disparity between different stages in the development process (Development, Testing, Production) is reduced, and “works on my machine” is no longer a valid excuse. To make not only the runtime but also the development environment reproducible and portable, Development Containers are a suitable solution.]]></summary></entry><entry xml:lang="en"><title type="html">Static code analysis for terraform</title><link href="https://www.production-ready.de/2023/04/29/analyse-terraform-files-with-tfsec-en.html" rel="alternate" type="text/html" title="Static code analysis for terraform" /><published>2023-04-29T00:00:00+02:00</published><updated>2023-04-29T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/04/29/analyse-terraform-files-with-tfsec-en</id><content type="html" xml:base="https://www.production-ready.de/2023/04/29/analyse-terraform-files-with-tfsec-en.html"><![CDATA[<p>Static code analysis is a crucial component of quality assurance for software projects. It allows errors to be detected early in a short feedback loop and addressed promptly. Source code is examined against defined rules to identify errors and potential vulnerabilities, and depending on the tool used, possible solutions are often suggested.</p>

<p>With the emergence of “Infrastructure as Code” approaches, infrastructure descriptions are increasingly being stored in the form of code or at least structured text files. This enables the advantages of static code analysis to be applied to infrastructure definitions as well.</p>

<!--more-->

<h2 id="analysis-of-terraform-files">Analysis of Terraform Files</h2>

<p>For analyzing Terraform definition files, there is the CLI tool <a href="https://tfsec.dev/">tfsec</a>. It is available under the MIT license as an executable file for Windows, Linux, and macOS, or as a Docker image for <a href="https://github.com/aquasecurity/tfsec/releases">download</a>.
<code class="language-plaintext highlighter-rouge">tfsec</code> comes with an extensive set of <a href="https://aquasecurity.github.io/tfsec/v1.28.1/checks/aws/api-gateway/enable-access-logging/">rules</a> for major hyperscalers like AWS, Azure, and Google Cloud. However, there are fewer rules available for a self-hosted Kubernetes environment or an OpenStack cluster. While you can define custom rules using <a href="https://aquasecurity.github.io/tfsec/v1.28.1/guides/configuration/custom-checks/">Custom Checks</a>, using <code class="language-plaintext highlighter-rouge">tfsec</code> in a managed environment is probably the most practical approach.</p>

<h2 id="tfsec">tfsec</h2>

<p>In the simplest case, you can run <code class="language-plaintext highlighter-rouge">tfsec</code> in the folder containing your Terraform files or pass the path as the first command-line argument. <code class="language-plaintext highlighter-rouge">tfsec</code> recursively scans all files with the extensions <code class="language-plaintext highlighter-rouge">.tf</code> and <code class="language-plaintext highlighter-rouge">.tfvars</code> and displays the results in the console.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./tfsec ./terraform
</code></pre></div></div>

<p>If no rule violations are detected, <code class="language-plaintext highlighter-rouge">tfsec</code> will output some statistics and provide an “no problems” message.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  timings
  ──────────────────────────────────────────
  disk i/o             5.3983ms
  parsing              0s
  adaptation           610.2µs
  checks               3.2634ms
  total                9.2719ms

  counts
  ──────────────────────────────────────────
  modules downloaded   0
  modules processed    1
  blocks processed     11
  files <span class="nb">read           </span>1

  results
  ──────────────────────────────────────────
  passed               6
  ignored              0
  critical             0
  high                 0
  medium               0
  low                  0

No problems detected!
</code></pre></div></div>

<p>When issues are detected, <code class="language-plaintext highlighter-rouge">tfsec</code> provides details about the problematic file, the exact location of the issue, as well as hints and links to further information. Using the <code class="language-plaintext highlighter-rouge">--concise-output</code> parameter reduces the output to the most essential information.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> ./tfsec ./terraform <span class="nt">--concise-output</span>

Result <span class="c">#1 CRITICAL Storage account uses an insecure TLS version. </span>
────────────────────────────────────────────────────────────────────────────────
  ./Terraform/main.tf:55
────────────────────────────────────────────────────────────────────────────────
          ID azure-storage-use-secure-tls-policy
      Impact The TLS version being outdated and has known vulnerabilities
  Resolution Use a more recent TLS/SSL policy <span class="k">for </span>the load balancer

  More Information
  - https://aquasecurity.github.io/tfsec/v1.28.1/checks/azure/storage/use-secure-tls-policy/
  - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account#min_tls_version
────────────────────────────────────────────────────────────────────────────────

  5 passed, 1 potential problem<span class="o">(</span>s<span class="o">)</span> detected.
</code></pre></div></div>

<p>In the example, <code class="language-plaintext highlighter-rouge">tfsec</code> is flagging an outdated TLS version for an Azure Storage Account.</p>

<p>If you want to exclude specific rules from being applied, you can use the <code class="language-plaintext highlighter-rouge">--exclude [RULE-ID]</code> parameter to exclude them from the analysis. You can also ignore issues related to individual resources or options within your Terraform definitions by using <a href="https://aquasecurity.github.io/tfsec/v1.28.1/guides/configuration/ignores/">comments</a> in your code.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"azurerm_storage_account"</span> <span class="s2">"storage_account"</span> <span class="p">{</span>
  <span class="nx">account_kind</span>    <span class="o">=</span> <span class="s2">"StorageV2"</span>
  <span class="nx">account_tier</span>    <span class="o">=</span> <span class="s2">"Standard"</span>
  <span class="nx">min_tls_version</span> <span class="o">=</span> <span class="s2">"TLS1_1"</span> <span class="c1">#tfsec:ignore:azure-storage-use-secure-tls-policy</span>
  <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="ci-pipeline">CI Pipeline</h2>

<p>A useful feature of <code class="language-plaintext highlighter-rouge">tfsec</code> is its ability to output the results of an analysis in various formats. Using the <code class="language-plaintext highlighter-rouge">--format</code> parameter, you can select the desired formats, including options like Markdown, HTML, and JUnit. By using the <code class="language-plaintext highlighter-rouge">--out</code> parameter, you specify the directory (which must exist) and the filename prefix for the output files. The first format specified will also be displayed on the console.</p>

<p>The following command generates the files <code class="language-plaintext highlighter-rouge">results.lovely</code>, <code class="language-plaintext highlighter-rouge">results.junit</code>, and <code class="language-plaintext highlighter-rouge">results.markdown</code> in the directory <code class="language-plaintext highlighter-rouge">./terraform/tfsec-results</code>.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./tfsec ./terraform <span class="nt">--concise-output</span> <span class="se">\</span>
    <span class="nt">--format</span> lovely,junit,markdown <span class="se">\</span>
    <span class="nt">--out</span> ./terraform/tfsec-results/results
</code></pre></div></div>

<p>The results can now be further processed in a CI pipeline, and the build can fail in case of rule violations.</p>

<h3 id="azure-devops-pipeline">Azure DevOps Pipeline</h3>

<p>If you are using Azure DevOps Pipelines, you can publish the JUnit file as test results. This way, you can easily integrate <code class="language-plaintext highlighter-rouge">tfsec</code> into existing test and build workflows and visualize errors directly within the pipeline.</p>

<p>For simpler adaptation to other CI systems, I will demonstrate how to use <code class="language-plaintext highlighter-rouge">tfsec</code> as a shell command. Additionally, there is a pipeline extension available in the <a href="https://marketplace.visualstudio.com/items?itemName=AquaSecurityOfficial.tfsec-official">Visual Studio Marketplace</a> and a <a href="https://github.com/marketplace/actions/tfsec-action">GitHub Action</a>.</p>

<p>First, download the appropriate binary for your system, set the execute bit if necessary, and create the output folder. Then, run <code class="language-plaintext highlighter-rouge">tfsec</code>. If you want to see the result in Markdown format as part of the pipeline run in the web interface, you can upload it using a <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands">logging command</a> like <code class="language-plaintext highlighter-rouge">echo "##vso[task.uploadsummary]...</code>.</p>

<p>In a second task, you can then publish the JUnit file as test results.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">CmdLine@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Run</span><span class="nv"> </span><span class="s">tfsec"</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">workingDirectory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$(System.DefaultWorkingDirectory)/Infrastructure/Terraform"</span>
    <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">echo "Download tfsec v1.28.1 ..."</span>
      <span class="s">wget -q https://github.com/aquasecurity/tfsec/releases/download/v1.28.1/tfsec-linux-amd64 -O tfsec</span>
      <span class="s">chmod +x tfsec</span>
      <span class="s">mkdir ./tfsec-results</span>

      <span class="s">./tfsec . --concise-output \</span>
          <span class="s">--format lovely,junit,markdown \</span>
          <span class="s">--out ./tfsec-results/results</span>

      <span class="s">echo "##vso[task.uploadsummary]$(System.DefaultWorkingDirectory)/Infrastructure/Terraform/tfsec-results/results.markdown"</span>

<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PublishTestResults@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Publish</span><span class="nv"> </span><span class="s">tfsec</span><span class="nv"> </span><span class="s">results"</span>
  <span class="na">condition</span><span class="pi">:</span> <span class="s">succeededOrFailed()</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">testResultsFormat</span><span class="pi">:</span> <span class="s">JUnit</span>
    <span class="na">searchFolder</span><span class="pi">:</span> <span class="s">$(System.DefaultWorkingDirectory)/Infrastructure/Terraform/tfsec-results</span>
    <span class="na">testResultsFiles</span><span class="pi">:</span> <span class="s2">"</span><span class="s">results.junit"</span>
    <span class="na">failTaskOnFailedTests</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">testRunTitle</span><span class="pi">:</span> <span class="s">tfsec</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">tfsec</code> results are included in the test statistics.</p>

<picture><source srcset="/generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-400-a1a077f1f.webp 400w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-600-a1a077f1f.webp 600w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-800-a1a077f1f.webp 800w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-1000-a1a077f1f.webp 1000w" type="image/webp" /><source srcset="/generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-400-bd4220f1b.png 400w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-600-bd4220f1b.png 600w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-800-bd4220f1b.png 800w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-1000-bd4220f1b.png 1000w" type="image/png" /><img src="/generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-tests-800-bd4220f1b.png" alt="Azure Pipeline Test" /></picture>

<p>The result of the Markdown file is displayed in the <code class="language-plaintext highlighter-rouge">Extensions</code> tab.</p>

<picture><source srcset="/generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-400-caf8c32a3.webp 400w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-600-caf8c32a3.webp 600w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-800-caf8c32a3.webp 800w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-1000-caf8c32a3.webp 1000w" type="image/webp" /><source srcset="/generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-400-de8f0d24c.png 400w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-600-de8f0d24c.png 600w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-800-de8f0d24c.png 800w, /generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-1000-de8f0d24c.png 1000w" type="image/png" /><img src="/generated/2023-04-29-analyse-terraform-files-with-tfsec/screenshot-tfsec-azurepipeline-extensions-800-de8f0d24c.png" alt="Azure Pipeline Extensions Tab" /></picture>

<p>With this, you have integrated automatic and continuous validation of Terraform files into your CI pipeline.</p>]]></content><author><name>David</name></author><category term="terraform" /><category term="security" /><category term="tfsec" /><category term="en" /><summary type="html"><![CDATA[Static code analysis is a crucial component of quality assurance for software projects. It allows errors to be detected early in a short feedback loop and addressed promptly. Source code is examined against defined rules to identify errors and potential vulnerabilities, and depending on the tool used, possible solutions are often suggested. With the emergence of “Infrastructure as Code” approaches, infrastructure descriptions are increasingly being stored in the form of code or at least structured text files. This enables the advantages of static code analysis to be applied to infrastructure definitions as well.]]></summary></entry><entry xml:lang="en"><title type="html">AKS Kubernetes Update</title><link href="https://www.production-ready.de/2023/04/20/aks-kubernetes-update-en.html" rel="alternate" type="text/html" title="AKS Kubernetes Update" /><published>2023-04-20T00:00:00+02:00</published><updated>2023-04-20T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/04/20/aks-kubernetes-update-en</id><content type="html" xml:base="https://www.production-ready.de/2023/04/20/aks-kubernetes-update-en.html"><![CDATA[<p><a href="https://kubernetes.io">Kubernetes</a> is the de facto standard for container orchestration. If you don’t want to manage the operations yourself, you can opt for a managed solution like Microsoft’s <a href="https://azure.microsoft.com/en-us/services/kubernetes-service/">Azure Kubernetes Service</a> (AKS). With AKS, many operational aspects are handled for you. Even the updates to newer Kubernetes version can be entirely managed by Azure. You can learn how to configure auto-upgrades in AKS from <a href="https://learn.microsoft.com/en-us/azure/aks/auto-upgrade-cluster">this article</a>.</p>

<p>However, if you want to determine the version and timing of a cluster upgrade yourself, you can achieve this using the <a href="https://learn.microsoft.com/en-us/cli/azure/">Azure CLI</a> or even better, through <a href="https://www.terraform.io">Terraform</a>. Of course, this assumes that you have provisioned your Kubernetes infrastructure using Terraform.</p>

<!--more-->

<h2 id="azure-kubernetes-versions">Azure Kubernetes Versions</h2>

<p>Microsoft provides a list of supported Kubernetes versions along with their support periods in the <a href="https://docs.microsoft.com/en-us/azure/aks/supported-kubernetes-versions">documentation</a>.</p>

<p>It’s important to note that updates are only possible within a minor version or to the next minor version. For example, if you want to update from version 1.24 to 1.26, you need to do it through version 1.25 in two steps.</p>

<p>The available versions depend on the region where the cluster is located. You can use the Azure CLI to query the available versions for a specific region.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az aks get-versions <span class="se">\</span>
  <span class="nt">--location</span> westeurope <span class="se">\</span>
  <span class="nt">--output</span> table
</code></pre></div></div>

<p>The output provides a list of available versions and the possible upgrades.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>KubernetesVersion    Upgrades
<span class="nt">-------------------</span>  <span class="nt">-----------------------</span>
1.26.3               None available
1.26.0               1.26.3
1.25.6               1.26.0, 1.26.3
1.25.5               1.25.6, 1.26.0, 1.26.3
1.24.10              1.25.5, 1.25.6
1.24.9               1.24.10, 1.25.5, 1.25.6
</code></pre></div></div>

<p>You can also query the available upgrades for a specific cluster.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az aks get-upgrades <span class="se">\</span>
  <span class="nt">--resource-group</span> rg-aks-test <span class="se">\</span>
  <span class="nt">--name</span> aks-test <span class="se">\</span>
  <span class="nt">--output</span> table
</code></pre></div></div>

<p>The result will show the available upgrades precisely.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Name     ResourceGroup    MasterVersion    Upgrades
<span class="nt">-------</span>  <span class="nt">---------------</span>  <span class="nt">---------------</span>  <span class="nt">--------------</span>
default  rg-aks-test      1.24.10          1.25.5, 1.25.6
</code></pre></div></div>

<h2 id="terraform">Terraform</h2>

<p>Configuring an AKS (Azure Kubernetes Service) cluster using Terraform is a straightforward process. Through the <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs">Azure Provider</a>, you create the corresponding resource, <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster">azurerm_kubernetes_cluster</a>.</p>

<p>In this configuration, you set the version of the control plane using <code class="language-plaintext highlighter-rouge">kubernetes_version</code> and the version of the nodes using <code class="language-plaintext highlighter-rouge">default_node_pool.orchestrator_version</code>.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="o">=</span> <span class="s2">"&gt;= 0.13"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">azurerm</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="o">=</span> <span class="s2">"hashicorp/azurerm"</span>
      <span class="nx">version</span> <span class="o">=</span> <span class="s2">"~&gt; 3.48.0"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Configure the Azure Provider via environment variables</span>
<span class="nx">provider</span> <span class="s2">"azurerm"</span> <span class="p">{</span>
  <span class="nx">features</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="c1"># Resource Group</span>
<span class="nx">resource</span> <span class="s2">"azurerm_resource_group"</span> <span class="s2">"rg"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"rg-aks-test"</span>
  <span class="nx">location</span> <span class="o">=</span> <span class="s2">"westeurope"</span>
<span class="p">}</span>

<span class="c1"># AKS Cluster</span>
<span class="nx">resource</span> <span class="s2">"azurerm_kubernetes_cluster"</span> <span class="s2">"aks"</span> <span class="p">{</span>
  <span class="nx">name</span>                              <span class="o">=</span> <span class="s2">"aks-test"</span>
  <span class="nx">location</span>                          <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">rg</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span>               <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">rg</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">dns_prefix</span>                        <span class="o">=</span> <span class="s2">"k8s"</span>
  <span class="nx">kubernetes_version</span>                <span class="o">=</span> <span class="s2">"1.24.10"</span>
  <span class="nx">role_based_access_control_enabled</span> <span class="o">=</span> <span class="kc">true</span>

  <span class="nx">default_node_pool</span> <span class="p">{</span>
    <span class="nx">name</span>                 <span class="o">=</span> <span class="s2">"default"</span>
    <span class="nx">node_count</span>           <span class="o">=</span> <span class="mi">2</span>
    <span class="nx">vm_size</span>              <span class="o">=</span> <span class="s2">"Standard_B2s"</span>
    <span class="nx">orchestrator_version</span> <span class="o">=</span> <span class="s2">"1.24.10"</span>
  <span class="p">}</span>

  <span class="nx">identity</span> <span class="p">{</span>
    <span class="nx">type</span> <span class="o">=</span> <span class="s2">"SystemAssigned"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A simple <code class="language-plaintext highlighter-rouge">terraform apply</code>, and after a few minutes, you’ll have an AKS cluster available in the desired version.</p>

<h3 id="upgrade-with-terraform">Upgrade with Terraform</h3>

<p>To upgrade your AKS cluster to a higher version, simply modify the version numbers in your Terraform configuration file, both for the control plane and the node pools. Then, execute a <code class="language-plaintext highlighter-rouge">terraform apply</code> command to initiate the upgrade process. The upgrade process will remove each node from the cluster one by one and replace them with updated versions. Running pods will be redistributed among the remaining nodes and will remain accessible throughout the process. The duration of the upgrade process depends on the number of nodes and may take several minutes to hours.</p>

<p>If you are using a CI/CD pipeline for infrastructure changes, make sure that the process doesn’t time out.</p>

<p>You can monitor the upgrade progress through the Azure CLI or the Azure Portal.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az aks list <span class="nt">-o</span> table

az aks nodepool list <span class="se">\</span>
  <span class="nt">--resource-group</span> rg-aks-test <span class="se">\</span>
  <span class="nt">--cluster-name</span>  aks-test <span class="se">\</span>
  <span class="nt">--output</span> table
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">ProvisioningState</code> will change to <code class="language-plaintext highlighter-rouge">Upgrading</code>.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Name     OsType    KubernetesVersion    ProvisioningState
<span class="nt">-------</span>  <span class="nt">--------</span>  <span class="nt">-------------------</span>  <span class="nt">-------------------</span>
default  Linux     1.25.6               Upgrading
</code></pre></div></div>

<p>Upon completion of the upgrade, the <code class="language-plaintext highlighter-rouge">ProvisioningState</code> will change back to <code class="language-plaintext highlighter-rouge">Succeeded</code>, and all nodes will be running on the new Kubernetes version.</p>]]></content><author><name>David</name></author><category term="aks" /><category term="kubernetes" /><category term="en" /><summary type="html"><![CDATA[Kubernetes is the de facto standard for container orchestration. If you don’t want to manage the operations yourself, you can opt for a managed solution like Microsoft’s Azure Kubernetes Service (AKS). With AKS, many operational aspects are handled for you. Even the updates to newer Kubernetes version can be entirely managed by Azure. You can learn how to configure auto-upgrades in AKS from this article. However, if you want to determine the version and timing of a cluster upgrade yourself, you can achieve this using the Azure CLI or even better, through Terraform. Of course, this assumes that you have provisioned your Kubernetes infrastructure using Terraform.]]></summary></entry><entry xml:lang="en"><title type="html">Mailgraph Docker Container</title><link href="https://www.production-ready.de/2023/04/15/mailgraph-docker-container-en.html" rel="alternate" type="text/html" title="Mailgraph Docker Container" /><published>2023-04-15T00:00:00+02:00</published><updated>2023-04-15T00:00:00+02:00</updated><id>https://www.production-ready.de/2023/04/15/mailgraph-docker-container-en</id><content type="html" xml:base="https://www.production-ready.de/2023/04/15/mailgraph-docker-container-en.html"><![CDATA[<p><a href="http://www.postfix.org">Postfix</a> is probably still one of the most widely used free SMTP servers. For those who self-host an email server and want to keep track of the volume of incoming and outgoing emails, they quickly come across the tool <a href="https://mailgraph.schweikert.ch">Mailgraph</a> by David Schweikert. Mailgraph consists of a Perl script that analyzes the log files of Postfix, populates an RRDtool database, and a CGI script that presents the data in the form of graphics in a clear manner in the browser.</p>

<!--more-->

<picture><source srcset="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-400-5ef5d4847.webp 400w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-600-5ef5d4847.webp 600w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-800-5ef5d4847.webp 800w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-1000-5ef5d4847.webp 1000w" type="image/webp" /><source srcset="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-400-1fc781bf0.png 400w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-600-1fc781bf0.png 600w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-800-1fc781bf0.png 800w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-1000-1fc781bf0.png 1000w" type="image/png" /><img src="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-last-week-800-1fc781bf0.png" alt="Mailgraph Screenshot" /></picture>

<p>In addition to tracking the number of sent and received emails, there is also a graphic representation for spam and viruses.</p>

<p>Thanks to a <a href="https://www.kernel-error.de/2014/04/22/mailgraph-graphen-um-spf-dmarc-und-dkim-erweitern/">patch by Sebastian van de Meer</a>, Mailgraph also provides information on whether incoming servers support SPF, DMARC, or DKIM. For this to work, it’s necessary to have these mechanisms configured on your Postfix instance.</p>

<picture><source srcset="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-400-f22167113.webp 400w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-600-f22167113.webp 600w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-800-f22167113.webp 800w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-1000-f22167113.webp 1000w" type="image/webp" /><source srcset="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-400-5c3a2682c.png 400w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-600-5c3a2682c.png 600w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-800-5c3a2682c.png 800w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-1000-5c3a2682c.png 1000w" type="image/png" /><img src="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-spf-dmarc-dkim-800-5c3a2682c.png" alt="SPF, DKIM, DMARC" /></picture>

<h2 id="docker-container">Docker Container</h2>

<p>To avoid installing Mailgraph and the corresponding web server directly on your email server, you can certainly run it in a Docker container.</p>

<p>For this purpose, I have created a Dockerfile that includes all the dependencies and can be used immediately. You can specify the path to the mail log file and a folder for storing the RRD files as volumes. Alternatively, you can also use a Docker volume for this purpose.</p>

<p>You can use the <code class="language-plaintext highlighter-rouge">-v</code> flag to mount host paths or volumes into the container: <code class="language-plaintext highlighter-rouge">-v [Host-Path]:[Container-Path]</code>.</p>

<p>The log file is expected within the container at the path <code class="language-plaintext highlighter-rouge">/var/log/mail/mail.log</code>, and the RRD files are located at <code class="language-plaintext highlighter-rouge">/var/www/mailgraph/rrd/</code>.</p>

<p>The image defaults to serving the Mailgraph website on port 80 and the path <code class="language-plaintext highlighter-rouge">/mailgraph</code>.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">-v</span> /var/log/mail/mail.log:/var/log/mail/mail.log <span class="se">\</span>
  <span class="nt">-v</span> /var/data/mailgraph/rrd/:/var/www/mailgraph/rrd/ <span class="se">\</span>
  davidullrich/mailgraph:latest
</code></pre></div></div>

<p>If you now access <a href="http://localhost:80/mailgraph/">http://localhost:80/mailgraph/</a> in your browser, you will see the initial graphs displayed.</p>

<h3 id="docker-compose">Docker Compose</h3>

<p>If you’re using Docker Compose for orchestration, a corresponding configuration might look something like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3'</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">mailgraph</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">davidullrich/mailgraph:latest</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">mail.example.com</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/log/mail/mail.log:/var/log/mail/mail.log</span>
      <span class="pi">-</span> <span class="s">/var/data/mailgraph/rrd/:/var/www/mailgraph/rrd/</span>
      <span class="pi">-</span> <span class="s">/etc/localtime:/etc/localtime:ro</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
</code></pre></div></div>

<h3 id="reverse-proxy-with-traefik">Reverse Proxy with Traefik</h3>

<p>If you don’t want your Mailgraph webpage to be publicly accessible, you can set up authentication using a reverse proxy. An easy way to do this is by using <a href="https://traefik.io/traefik/">Traefik</a>, a reverse proxy that integrates well with Docker Compose. Once you have Traefik set up on your system, configuring Mailgraph through labels becomes straightforward.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3'</span>

<span class="na">services</span><span class="pi">:</span>
  <span class="na">mailgraph</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">davidullrich/mailgraph:latest</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">mail.example.com</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/log/mail/mail.log:/var/log/mail/mail.log</span>
      <span class="pi">-</span> <span class="s">/var/data/mailgraph/rrd/:/var/www/mailgraph/rrd/</span>
      <span class="pi">-</span> <span class="s">/etc/localtime:/etc/localtime:ro</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.enable=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.mailgraph-router.rule=Host(`mail.example.com`)</span><span class="nv"> </span><span class="s">&amp;&amp;</span><span class="nv"> </span><span class="s">PathPrefix(`/mailgraph`)"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.mailgraph-router.entryPoints=websecure"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.mailgraph-router.service=mailgraph-service"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.mailgraph-service.loadBalancer.server.scheme=http"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.mailgraph-service.loadBalancer.server.port=80"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.mailgraph-router.middlewares=mailgraph-middleware-auth"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.middlewares.mailgraph-middleware-auth.basicauth.users=user:[password-hash]"</span>
</code></pre></div></div>

<h2 id="dovecot-extension">Dovecot Extension</h2>

<p>As I use <a href="https://www.dovecot.org">Dovecot</a> as my IMAP server, I’ve extended Mailgraph to display the number of successful and failed IMAP logins.</p>

<picture><source srcset="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-400-3c823a2b9.webp 400w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-600-3c823a2b9.webp 600w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-800-3c823a2b9.webp 800w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-1000-3c823a2b9.webp 1000w" type="image/webp" /><source srcset="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-400-99b4b64fa.png 400w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-600-99b4b64fa.png 600w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-800-99b4b64fa.png 800w, /generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-1000-99b4b64fa.png 1000w" type="image/png" /><img src="/generated/2023-04-13-mailgraph-docker-container/screenshot-mailgraph-dovecot-logins-800-99b4b64fa.png" alt="Dovecot Logins" /></picture>

<h2 id="source-code-and-docker-image">Source Code and Docker Image</h2>

<p>You can find the source code for the Docker image on <a href="https://github.com/davull/MailgraphContainer">GitHub</a>, and the pre-built image is available on <a href="https://hub.docker.com/r/davidullrich/mailgraph">Docker Hub</a>.</p>]]></content><author><name>David</name></author><category term="mailgraph" /><category term="docker" /><category term="en" /><summary type="html"><![CDATA[Postfix is probably still one of the most widely used free SMTP servers. For those who self-host an email server and want to keep track of the volume of incoming and outgoing emails, they quickly come across the tool Mailgraph by David Schweikert. Mailgraph consists of a Perl script that analyzes the log files of Postfix, populates an RRDtool database, and a CGI script that presents the data in the form of graphics in a clear manner in the browser.]]></summary></entry></feed>