<?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/de.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/de.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="de"><title type="html">KI: Coding ist nicht das Bottleneck</title><link href="https://www.production-ready.de/2025/12/17/coding-is-not-the-bottleneck.html" rel="alternate" type="text/html" title="KI: Coding ist nicht das 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</id><content type="html" xml:base="https://www.production-ready.de/2025/12/17/coding-is-not-the-bottleneck.html"><![CDATA[<p>Künstliche Intelligenz hat mit dem Release von ChatGPT im November 2022 einen riesigen noch immer andauernden Hype erfahren. Vor allem im Bereich der Softwareentwicklung wurde und wird ihr viel Potenzial zugeschrieben. Allerdings merkt man beim reinen Vibe Coding sehr schnell, wo die Grenzen liegen und dass nachhaltige Lösungen, vor allem im Enterprise-Umfeld, doch mehr als nur generierten Quellcode benötigen. Wo <abbr title="Künstliche Intelligenz">KI</abbr> hier wirklich Mehrwert liefert und wo eher nicht, speziell für die tägliche Arbeit von Senior Entwickler:innen, soll der folgende Erfahrungsbericht zeigen.</p>

<!--more-->

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

<p>Hier soll es explizit nicht um <a href="https://en.wikipedia.org/wiki/Vibe_coding">Vibe Coding</a> gehen. Bei <code class="language-plaintext highlighter-rouge">AI Assisted Coding</code> geht es darum, Programmiertätigkeiten zu automatisieren und dem Entwickler Arbeit abzunehmen. Dieser bleibt im Lead und gibt genau vor, was das erwartete Ergebnis ist und wie dieses zu erreichen ist.</p>

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

<p>Eine einfache Form von <abbr title="Künstliche Intelligenz">KI</abbr>-gestützter Entwicklung ist die Möglichkeit der Codevervollständigung. Diese geht deutlich über die zuvor bereits etablierten Varianten der Autovervollständigung von einzelnen Zeilen hinaus. Dadurch, dass dem <abbr title="Künstliche Intelligenz">KI</abbr>-Modell Kontextwissen über die Codebasis zugänglich ist, ist das automatische Generieren von ganzen Codeblöcken problemlos möglich. Bei repetitiven Aufgaben wie dem Herunterschreiben von Mapping-Methoden oder CRUD-Operationen erspart dies viel Tipparbeit. Je generischer die Anforderung ist, desto besser sind die Ergebnisse. Viel wiederkehrender Code findet sich in den Trainingsmengen der <abbr title="Künstliche Intelligenz">KI</abbr>-Modelle.</p>

<p>Auch das Umsetzen von bekannten Algorithmen funktioniert aufgrund der Trainingsdaten sehr gut. Häufig muss man nur eine Methodensignatur schreiben und erhält umgehend den Autovervollständigungs-Vorschlag.</p>

<h3 id="repetitive-task">Repetitive Task</h3>

<p>Aufgaben, die in einer Codebasis immer wieder anfallen, lassen sich gut mit <abbr title="Künstliche Intelligenz">KI</abbr>-gestützter Entwicklung umsetzen. So kann man den Code etwa für CRUD-Operationen und Datenbankzugriffe problemlos generieren lassen. Die initiale Umsetzung sollte man von Hand ausführen. Hier arbeitet man alle für seinen Bedarf notwendigen technischen Details aus. Bei nachfolgenden Aufgaben kann man dem <abbr title="Large Language Model">LLM</abbr> diese Blaupause als Referenz mitgeben, an der es sich orientieren soll.</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>Auch Namen für Klassen oder Methoden kann man in seinem Prompt definieren. Je mehr Vorgaben man einem <abbr title="Large Language Model">LLM</abbr> gibt, desto geringer ist die Wahrscheinlichkeit für Halluzinationen.</p>

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

<p>Bei Refactorings, die über reines Renaming hinausgehen, kann eine <abbr title="Künstliche Intelligenz">KI</abbr> viele Aufgaben übernehmen. Mit klaren Vorgaben, was anzupassen ist und was nicht und einem überschaubaren Scope, trifft man sehr schnell das gewünschte Ergebnis. Durch eine hohe Testabdeckung bleibt sichergestellt, das die Funktionalität nach dem Refactoring nicht gebrochen ist.</p>

<h2 id="vorgehen">Vorgehen</h2>

<p>Um erfolgreich Arbeit an einen <abbr title="Künstliche Intelligenz">KI</abbr>-Agenten abgeben zu können, müssen die Anforderungen klar formuliert sein. Neben den reinen fachlichen Anforderungen, die z.B. in einer Userstory festgehalten werden, muss man auch technische Details beschreiben. Was zuvor möglicherweise nur implizites Wissen aller Beteiligter war, die an einem Softwareprojekt mitwirken, muss nun explizit aufgeschrieben und dem <abbr title="Large Language Model">LLM</abbr> zugänglich gemacht werden.</p>

<p>Auch Entwickler müssen mehr Zeit in das Verfassen von Task investieren, um diese sinnvoll von einer <abbr title="Künstliche Intelligenz">KI</abbr> umsetzen zu lassen. Auch hier empfiehlt es sich, sehr kleinschrittig vorzugehen. Userstories werden in viele kleine technische Task heruntergebrochen, die einen sehr beschränkten Scope haben. Diese kann man seiner <abbr title="Künstliche Intelligenz">KI</abbr> dann zum Abarbeiten übergeben. Durch das Herunterbrechen auf kleine Task reduziert man auch den Blast-Radius, den eine <abbr title="Künstliche Intelligenz">KI</abbr> hinterlässt, wenn sie anfängt, in dutzenden Dateien Änderungen vorzunehmen. Nachdem die <abbr title="Künstliche Intelligenz">KI</abbr> dann alle Task einen nach dem anderen umgesetzt und zu jedem Task dedizierte Tests geschrieben hat, kann sie einen Pull-Request stellen. Als Entwicklungs-Team muss man danach die <abbr title="Pull-Requests">PRs</abbr> reviewen und vor allem die Qualität des generierten Codes sicherstellen. Auch hier gilt weiterhin die Anforderung, dass der Code nicht nur korrekt sondern auch wartbar sein muss. Auch zukünftig werden wir mehr Quellcode lesen als schreiben, vielleicht in noch viel größerem Maße als bisher.</p>

<p>Neben den Task kann man einer <abbr title="Künstliche Intelligenz">KI</abbr> technische wie fachliche Informationen über ein Softwaresystem in einer <a href="https://agents.md">AGENTS.md</a>-Datei zur Verfügung stellen. Diese wird bei jedem Prompt implizit eingelesen und bei der Generierung der Antwort berücksichtigt.</p>

<h2 id="grenzen-von-ai-coding">Grenzen von <abbr title="Artificial Intelligence">AI</abbr> Coding</h2>

<p>Was nicht funktioniert sind One-Shots zum Umsetzen ganzer Features oder gar das Generieren ganzer Systeme. Auch wenn dabei Ergebnisse herauskommen, sind diese zu weit weg von dem gewünschten Ziel, der Architektur oder auch nur den Coding-Guidelines, die man im Team verfolgt.</p>

<p>Mitunter ist es auch zeitlich kein Vorteil, eine <abbr title="Künstliche Intelligenz">KI</abbr> auf ein Problem zu werfen. Im Agentic Mode kann eine <abbr title="Künstliche Intelligenz">KI</abbr> durchaus eine Stunde mit einer einzelnen Aufgabe verbringen, die ein erfahrener Entwickler mit dem entsprechenden Wissen über das Projekt in wenigen Minuten problemlos hätte umsetzen können. Auch die Zeit, die das formulieren von guten Prompts einnimmt, sollte man nicht vernachlässigen.</p>

<h3 id="coding-ist-nicht-das-bottleneck">Coding ist nicht das Bottleneck</h3>

<p>Die reine Schreiben von Quellcode ist nicht der limitierende Faktor bei der Entwicklung von Software. Bereits ohne <abbr title="Künstliche Intelligenz">KI</abbr> liegt die Coding-Zeit von Senior Entwicklern bei 30-40%. Daher ist der Einsatz von künstlicher Intelligenz nicht der Heilsbringer, den manche vielleicht in ihr sehen. Aber sie verschiebt die Arbeit von Entwicklern, noch weiter weg vom Coding in Richtung Planung, Vorbereitung und Review.</p>

<p>Für erfahrene Entwickler sehe ich in <abbr title="Artificial Intelligence">AI</abbr> Developer Tools ein gutes Werkzeug, um einfachere und repetitive Aufgaben automatisiert umsetzen zu lassen. Für Junior Devs ist es daher umso wichtiger, sich nicht auf reines Prompting und Vibe Coding zu verlassen, sondern eben jene Erfahrung und Expertise aufzubauen, um sinnvoll mit Hilfe von <abbr title="Künstliche Intelligenz">KI</abbr> Software zu entwickeln.</p>]]></content><author><name>David</name></author><category term="ai" /><category term="ki" /><category term="coding" /><category term="development" /><category term="de" /><summary type="html"><![CDATA[Künstliche Intelligenz hat mit dem Release von ChatGPT im November 2022 einen riesigen noch immer andauernden Hype erfahren. Vor allem im Bereich der Softwareentwicklung wurde und wird ihr viel Potenzial zugeschrieben. Allerdings merkt man beim reinen Vibe Coding sehr schnell, wo die Grenzen liegen und dass nachhaltige Lösungen, vor allem im Enterprise-Umfeld, doch mehr als nur generierten Quellcode benötigen. Wo KI hier wirklich Mehrwert liefert und wo eher nicht, speziell für die tägliche Arbeit von Senior Entwickler:innen, soll der folgende Erfahrungsbericht zeigen.]]></summary></entry><entry xml:lang="de"><title type="html">Snapshot Testing in C#</title><link href="https://www.production-ready.de/2025/12/01/snapshot-testing-in-csharp.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</id><content type="html" xml:base="https://www.production-ready.de/2025/12/01/snapshot-testing-in-csharp.html"><![CDATA[<p><code class="language-plaintext highlighter-rouge">Testing</code> war bereits des Öfteren Thema auf diesem Blog. Neben <a href="/2023/06/10/property-based-testing-in-csharp.html">Property-based testing</a> habe ich <a href="/2024/04/27/integration-testing-with-testcontainers.html">Integrationstest in .NET mit Testcontainern</a> und <a href="/2023/12/10/architecture-refactoring-with-archunitnet.html">Architektur-Test mit ArchUnitNET</a> vorgestellt.</p>

<p>Ein weiteres nützliches Tool im Testing-Werkzeugkasten sind <code class="language-plaintext highlighter-rouge">Snapshot Tests</code>. <code class="language-plaintext highlighter-rouge">Snapshot Tests</code> machen genau das, was der Name vermuten lässt: Sie erstellen einen <code class="language-plaintext highlighter-rouge">Snapshot</code> von einem Testergebnis. Damit lässt sich das Verhalten eines Systems einfrieren und gegen unerwünschte Änderungen und Regression absichern.</p>

<!--more-->

<blockquote>
  <p>Der Beispielcode zu diesem Blog-Post ist auf Github verfügbar:</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>Die hier gezeigten Beispiele verwenden das Nuget-Package <a href="https://swisslife-oss.github.io/snapshooter/">Snapshooter</a>. Das Vorgehen ist aber problemlos auf andere Libraries übertragbar.</p>

<h2 id="warum-snapshot-testing">Warum Snapshot Testing?</h2>

<p>Snapshot Tests frieren das Verhalten eines Systems zum Zeitpunkt der Testausführung ein. Beim ersten Ausführen eines Snapshot Tests wird das Ergebnis einer zu testenden Funktionalität in eine Datei geschrieben und dient nun als Referenz für spätere Testausführungen. Weit das Ergebnis von dem Inhalt der Referenzdatei ab, gilt der Test als fehlgeschlagen.</p>

<p>Die Unterschiede kann man sich optisch sehr einfach in einem Diff-Tool anschauen und schnell entscheiden, ob es sich um erwartete oder unerwartete Änderungen handelt. Hat man die Änderungen geprüft und sieht diese als valide an, übernimmt man die aktualisierte Datei als neue Referenz.</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>Ein weiterer Vorteil ist, dass man den Inhalt großer Datenobjekte einfach visuell überprüfen kann. Möchte man mehrere Properties eines Rückgabeobjektes überprüfen, schreibt man klassischerweise mehrere <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>Nutzt man hingegen eine Snapshot-Test, kann man mit einem Aufruf von <code class="language-plaintext highlighter-rouge">MatchSnapshot()</code> sämtliche Properties des Objektes festhalten.</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 der entsprechenden Snapshot-Datei <code class="language-plaintext highlighter-rouge">__snapshots__/WeatherForecastProviderTests.GetForecast_Should_MatchSnapshot.snap</code> findet man die JSON-Repräsentation des Objektes und kann mit einem Blick sehen, ob die Properties die erwarteten Werte enthalten.</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-im-backend">Snapshot Testing im Backend</h2>

<p>Im Bereich der <a href="https://jestjs.io/docs/snapshot-testing">JavaScript Frontend-Frameworks</a> ist Snapshot Testing bereits seit geraumer Zeit verbreitet, etwas um den HTML-Output von Render-Funktionen zu prüfen.</p>

<p>Aber auch Webseiten mit <abbr title="Server Side Rendering">SSR</abbr> oder API-Endpunkte lassen sich mittels Snapshot Testing effektiv überprüfen.</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>Stellt ein API-Aufruf keine <a href="/2023/09/28/refactor-to-purity.html">Pure Function</a> dar, ändern sich manche Return-Werte, unabhängig vom gegebenen Input. So zum Beispiel das heutige Datum. Kann man diese in seinem Testsetup, etwas über <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.time.testing.faketimeprovider">FakeTimeProvider</a>, nicht steuern, so kann man die Werte aus dem Snapshot-Resultat herausfiltern.</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-Seiten lassen sich auf die gleiche Weise testen. Hier bietet es sich an, den HTML Output vor dem Snapshot-Vergleich etwas aufzuräumen, um Änderungen an zufällige Werten nicht zu berücksichtigen. Dazu zählen etwa dynamische IDs, <a href="https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery">Anti Forgery Tokens</a> oder automatisch generierte CSS-Klassen.</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>Erstellt man auf einer derartigen Höhe Snapshots von ganzen Webseiten, bekommt man unerwünschte Änderungen am generierten Output zuverlässig mit und erhält effektive <code class="language-plaintext highlighter-rouge">Regressionstests</code>.</p>

<h2 id="arbeiten-mit-legacy-code">Arbeiten mit Legacy Code</h2>

<p>Code ohne Tests ist bekanntlich <code class="language-plaintext highlighter-rouge">Legacy Code</code>. Um nun aber schnell und effektiv mit einer Legacy Codebasis arbeiten zu können, bieten sich Snapshot Tests förmlich an. Sie ermöglichen es, das bestehende Verhalten der Anwendung festzuhalten. Somit ist ein Sicherheitsnetz aufgespannt, dass uns das Ändern der Codebasis erlaubt und unerwartet Änderungen aufdeckt. Für Webanwendungen ist das Testen von HTTP- und API-Endpunkten einfach umzusetzen. Bei Desktop-Anwendungen oder native Mobile Apps muss man eine Ebene unter der UI ansetzen. So kann man etwa vor dem Umsetzen von Codeänderungen einen Snapshot eines <a href="https://learn.microsoft.com/de-de/dotnet/architecture/maui/mvvm">ViewModels</a> erstellen.</p>

<p>Hat man die Möglichkeit, wiederholbare Integrationstests mit externen Ressourcen wie einer Datenbank zu erstellen, kann auch deren Inhalt das Ziel eines Snapshot Tests sein.</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="de" /><summary type="html"><![CDATA[Testing war bereits des Öfteren Thema auf diesem Blog. Neben Property-based testing habe ich Integrationstest in .NET mit Testcontainern und Architektur-Test mit ArchUnitNET vorgestellt. Ein weiteres nützliches Tool im Testing-Werkzeugkasten sind Snapshot Tests. Snapshot Tests machen genau das, was der Name vermuten lässt: Sie erstellen einen Snapshot von einem Testergebnis. Damit lässt sich das Verhalten eines Systems einfrieren und gegen unerwünschte Änderungen und Regression absichern.]]></summary></entry><entry xml:lang="de"><title type="html">Walk the Board: Effektives Arbeiten mit Task-Boards in Software-Projekten</title><link href="https://www.production-ready.de/2025/10/24/walk-the-board.html" rel="alternate" type="text/html" title="Walk the Board: Effektives Arbeiten mit Task-Boards in Software-Projekten" /><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</id><content type="html" xml:base="https://www.production-ready.de/2025/10/24/walk-the-board.html"><![CDATA[<p>In Software-Projekten, die mit Tools wie <a href="https://www.atlassian.com/software/jira">Jira</a> oder <a href="https://azure.microsoft.com/en-us/products/devops/">Azure DevOps</a> verwaltet werden, ist es üblich, Aufgaben und User Stories auf einem <em>Board</em> zu visualisieren. Dieses Board hilft dem ganzen Team, den Stand der Arbeit zu verfolgen und eventuelle Engpässe im Workflow zu identifizieren. In täglichen Standup-Meetings wird aufgrund des Boards der Fortschritt der aktuellen Entwicklungs-Iteration besprochen.</p>

<p>Wie man in einem agilen Projektumfeld mit einem solchen Task-Board arbeitet, um nicht nur <a href="https://mooncamp.com/blog/output-vs-outcome"><em>Output</em> sondern <em>Outcome</em></a> zu liefern und nicht in ein blosses Status-Reporting verfällt, zeige ich in diesem Blogbeitrag.</p>

<!--more-->

<p>Die meisten Software-Teams, die in Iterationen arbeiten, nutzen ein Task-Board in irgendeiner Form. Diese verfügen über mehrere Spalten, die den aktuellen Status einer Aufgabe repräsentieren. Im einfachsten Fall genügen drei Spalten <code class="language-plaintext highlighter-rouge">TODO</code>, <code class="language-plaintext highlighter-rouge">DOING</code> und <code class="language-plaintext highlighter-rouge">DONE</code>. Je nach Projekt-Setup können am Anfang noch weitere Spalten für die Design- und Refinement-Phasen stehen. Am anderen Ende können Spalten für das Testing oder die Abnahme durch den/die Product Owner oder andere Personen eingefügt werden. Zusätzlich hat man die Möglichkeit, jedem Item auf dem Board eine Person zuzuweisen, die gerade an der entsprechenden Aufgabe arbeitet.</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>Das gesamte Team ist für die Erfüllung der Aufgaben zuständig. Daher werden User Stories (oder Bugs oder Issues) keiner einzelnen Person zugewiesen. Das Team committed sich zu Begin einer Iteration auf einen Arbeitsumfang, den es bewältigen will und kann. Sobald eine neue Aufgabe angegangen wird, wird diese auf konkrete zu erledigende Task heruntergebrochen. Auch während der Umsetzen können weitere Task hinzukommen, sobald sich herausstellt, das weitere Arbeiten notwendig sind. Diese Sub-Task werden nun von einzelnen Personen bearbeitet und diesen auch im Board zugeordnet. An einer Story können so durchaus mehrere Personen parallel arbeiten. So behält man den Überblick, welche Aufgaben wie viele Personen bindet.</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>Damit ist jeder im Team für die Erfüllung der aktuell aktiven Aufgaben verantwortlich. Es gibt im gesamten Prozess keine “meine” und “deine” Stories. Ist man mit seinem aktuellen Task fertig, kann man sich einer anderen Story anschliessen. Erst, wenn kein weiteres parallels Arbeiten sinnvoll möglich ist, beginnt man mit der Umsetzung einer neuen Story.</p>

<h2 id="priorisieren">Priorisieren</h2>

<p>Wie entscheide ich, welchen Task ich als nächstes angehe? Durch die <em>Shared Ownership</em> für den gesamten Iterations-Umfang, habe ich keine “eigenen” Stories, die ich abzuarbeiten habe. Um nicht wahllos mit der nächsten Story zu beginnen, ist es wichtig, die aktuell auf dem Board stattfindenden Arbeiten zu priorisieren. Stories, die weiter rechts auf dem Board liegen, sind wichtiger als solche weiter links. Diese sind also immer von höherer Priorität. Befindet sich eine Story in der Spalte <code class="language-plaintext highlighter-rouge">TESTING</code>, nehme ich mir diese vor und teste sie. Anstatt einer Spalte <code class="language-plaintext highlighter-rouge">TESTING</code> kann man auch einfach einen Sub-Task aufs Board legen. Befindet sich eine Story in der Spalte <code class="language-plaintext highlighter-rouge">DOING</code>, arbeite ich an dieser Story mit. Erst wenn keine Stories mehr auf der rechten Seite des Boards übrig sind und kein paralleles Arbeiten an aktiven Stories möglich ist, wird mit der Arbeit an einer neuen Aufgabe begonnen.</p>

<p>Im Kanban-Framework gibt es dafür das <a href="https://www.atlassian.com/agile/kanban/wip-limits">Work in Progress Limit</a>.</p>

<p>Der Hintergrund dieses Vorgehens ist einfach: User Stories, die nicht fertig, also im Status <code class="language-plaintext highlighter-rouge">DONE</code> sind, haben für das Produkt keinen Mehrwert. Eine abgeschlossene Story ist wertvoller als fünf angefangene. Auch ein “fast fertig” ist kein “fertig”.</p>

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

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

<p>Oben haben wir gesehen, dass wir unser Task-Board von rechts nach links abarbeiten. Dies gilt auch für unsere täglichen Sync-Meetings mit dem/der Product Owner. Wir gehen das Board von rechts nach links durch, klären welche Arbeiten umgesetzt sind und abgenommen werden können. Hier genügt es, wenn eine Person über das Board führt. Es muss sich nicht jeder zu seinen aktuellen Tätigkeiten äussern. Es geht nicht darum, zu rechtfertigen, womit man den letzten Arbeitstag verbracht hat oder zu zeigen, wie weit man mit seiner “eigenen” Story gekommen ist. Treten Probleme auf, wird als Team diskutiert, wie man zu einer Lösung kommt.</p>

<p>Mit diesem Vorgehen schafft man es, auch in stressigen Situationen den Fokus zu bewahren und für eine kontinuierliche Weiterentwicklung des Produktes zu sorgen.</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="de" /><summary type="html"><![CDATA[In Software-Projekten, die mit Tools wie Jira oder Azure DevOps verwaltet werden, ist es üblich, Aufgaben und User Stories auf einem Board zu visualisieren. Dieses Board hilft dem ganzen Team, den Stand der Arbeit zu verfolgen und eventuelle Engpässe im Workflow zu identifizieren. In täglichen Standup-Meetings wird aufgrund des Boards der Fortschritt der aktuellen Entwicklungs-Iteration besprochen. Wie man in einem agilen Projektumfeld mit einem solchen Task-Board arbeitet, um nicht nur Output sondern Outcome zu liefern und nicht in ein blosses Status-Reporting verfällt, zeige ich in diesem Blogbeitrag.]]></summary></entry><entry xml:lang="de"><title type="html">RSS Feed automatisch auf Mastodon veröffentlichen</title><link href="https://www.production-ready.de/2024/11/24/feed-to-mastodon.html" rel="alternate" type="text/html" title="RSS Feed automatisch auf Mastodon veröffentlichen" /><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</id><content type="html" xml:base="https://www.production-ready.de/2024/11/24/feed-to-mastodon.html"><![CDATA[<p>Viele Blogs und News-Webseite informieren über neue Artikel und Beiträge auch auf den vielen Social Media Plattformen wie Twitter, Facebook oder Threads. Leider ist Mastodon oft noch kein Ziel dieser Cross-Postings.</p>

<p>Da ich Mastodon selbst gerne als News-Quelle und Aggregator verschiedener Blogs nutze, habe ich eine Anwendung geschrieben, die RSS-Feeds automatisch im Fediverse veröffentlicht.</p>

<!--more-->

<blockquote>
  <p>Der Quellcode sowie die Anleitung  zur Verwendung sind auf Github verfügbar: <a href="https://github.com/davull/FeedToMastodon">https://github.com/davull/FeedToMastodon</a></p>
</blockquote>

<blockquote>
  <p>Eine laufende Instanz der Anwendung veröffentlicht Posts von einigen News-Feeds auf <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> ist eine .NET Anwendung, die neue Feed-Einträge automatisch auf Mastodon postet. Unterstützt werden dabei <a href="https://en.wikipedia.org/wiki/RSS">RSS</a>, <a href="https://en.wikipedia.org/wiki/Atom_(web_standard)">Atom</a> und <a href="https://en.wikipedia.org/wiki/Resource_Description_Framework">RDF</a>-Feeds. Die Anwendung kann lokal kompiliert und ausgeführt oder als vorkonfiguriertes <a href="https://hub.docker.com/r/davidullrich/feed-to-mastodon">Docker-Image</a> genutzt werden.</p>

<p>Die Anwendung kann beliebig viele Feeds behandeln. Für jeden Feed kann ein separater Mastodon-Account verwendet werden.</p>

<p>Damit der jeweilige Mastodon Account nicht mit alten Feed-Einträgen überflutet wird, werden beim ersten Synchronisieren eines neuen Feeds die bisherigen Einträge übersprungen und nur neue Einträge veröffentlicht.</p>

<h3 id="konfiguration">Konfiguration</h3>

<p>Die Konfiguration der zu überwachenden Feeds erfolgt in einer <code class="language-plaintext highlighter-rouge">.ini</code>-Datei.</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>Neben den URLs zum Feed und zum Mastodon-Server, auf dem der Ziel-Account registriert ist, ist ein Access-Token notwendig.</p>

<p>Der Parameter <code class="language-plaintext highlighter-rouge">summary_separator</code> ist optional und kann angegeben werden, um Feed-Einträge zu kürzen. Der Eintrag wird ab dem ersten Vorkommen der Trennzeichen abgeschnitten.</p>

<p>Wenn ein Post etwa ein <code class="language-plaintext highlighter-rouge">[…]</code> enthält, kann der Eintrag hier gekürzt werden:</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>Die Konfiguration von <code class="language-plaintext highlighter-rouge">summary_separator = […]</code> führt zu dem gekürzten Eintrag:</p>

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

<p>Gepostete Einträge werden in einer SQLite-Datenbank gespeichert, um doppelte Post zu vermeiden.</p>

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

<p>Ein Access-Token kann über die Mastodon-Webseite unter <code class="language-plaintext highlighter-rouge">Einstellungen</code> -&gt; <code class="language-plaintext highlighter-rouge">Entwicklung</code> -&gt; <code class="language-plaintext highlighter-rouge">Neue Anwendung</code> angelegt werden. Unter <code class="language-plaintext highlighter-rouge">Name</code> kann ein beliebiger Text eingetragen werden, z.B. <code class="language-plaintext highlighter-rouge">Feed to Mastodon</code>. Unter <code class="language-plaintext highlighter-rouge">Weiterleitungs-URI</code> kann der Standardwert <code class="language-plaintext highlighter-rouge">urn:ietf:wg:oauth:2.0:oob</code> beibeibehalten werden. Unter <code class="language-plaintext highlighter-rouge">Befugnisse</code> muss nur <code class="language-plaintext highlighter-rouge">write:statuses</code> ausgewählt sein, <code class="language-plaintext highlighter-rouge">profile</code> kann abgewählt werden.</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>Nach dem Speichern kann man erneut auf die neu erstellte Anwendung klicken und das Access-Token <code class="language-plaintext highlighter-rouge">Dein Zugriffstoken</code> kopieren.</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="parameter">Parameter</h4>

<table>
  <thead>
    <tr>
      <th>Parameter</th>
      <th>Beschreibung</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>feed_url</td>
      <td>Gesamte URL des Feeds, notwendig</td>
    </tr>
    <tr>
      <td>summary_separator</td>
      <td>Trennzeichen, um Feed-Einträge zu kürzen, optional</td>
    </tr>
    <tr>
      <td>mastodon_server</td>
      <td>URL des Mastodon-Servers, notwendig</td>
    </tr>
    <tr>
      <td>mastodon_access_token</td>
      <td>Access-Token des Mastodon-Accounts, notwendig</td>
    </tr>
  </tbody>
</table>

<p>Die Dateipfade zu den Konfigurations- und Datenbankdateien werden als Umgebungsvariablen hinterlegt.</p>

<table>
  <thead>
    <tr>
      <th>Umgebungsvariable</th>
      <th>Beschreibung</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FTM_CONFIG_FILE_NAME</td>
      <td>Absoluter oder relativer Pfad zur Konfigurationsdatei</td>
    </tr>
    <tr>
      <td>FTM_DATABASE_NAME</td>
      <td>Absoluter oder relativer Pfad zur SQLite-Datenbank</td>
    </tr>
  </tbody>
</table>

<h3 id="ausführung">Ausführung</h3>

<p>Um <code class="language-plaintext highlighter-rouge">Feed to Mastodon</code> via <a href="https://www.docker.com">Docker</a> auszuführen, genügt folgender Befehl:</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>Eine <a href="https://docs.docker.com/compose/">Docker Compose</a> Konfiguration sieht wie folgt aus:</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="statistiken">Statistiken</h4>

<p>Für die Interessierten gibt <code class="language-plaintext highlighter-rouge">Feed to Mastodon</code> täglich eine Statistik auf der Console aus. Diese zeigt die Anzahl der geposteten Feeds in den letzten 24 Stunden und den letzten sieben Tagen.</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-beispiel">Live-Beispiel</h3>

<p>Unter <a href="https://feedmirror.social/public/local">https://feedmirror.social</a> ist <code class="language-plaintext highlighter-rouge">Feed to Mastodon</code> mit ein paar konfigurierten Feeds in Betrieb und bringt die neuesten Artikel ins 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="de" /><summary type="html"><![CDATA[Viele Blogs und News-Webseite informieren über neue Artikel und Beiträge auch auf den vielen Social Media Plattformen wie Twitter, Facebook oder Threads. Leider ist Mastodon oft noch kein Ziel dieser Cross-Postings. Da ich Mastodon selbst gerne als News-Quelle und Aggregator verschiedener Blogs nutze, habe ich eine Anwendung geschrieben, die RSS-Feeds automatisch im Fediverse veröffentlicht.]]></summary></entry><entry xml:lang="de"><title type="html">Integrationstest mit Docker Compose und Azure Service-Containers</title><link href="https://www.production-ready.de/2024/05/10/integration-testing-with-docker.html" rel="alternate" type="text/html" title="Integrationstest mit Docker Compose und 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</id><content type="html" xml:base="https://www.production-ready.de/2024/05/10/integration-testing-with-docker.html"><![CDATA[<p>In einem <a href="/2024/04/27/integration-testing-with-testcontainers.html">vorherigen Blog-Post</a> habe ich gezeigt, wie man mit Hilfe von <a href="https://testcontainers.com">Testcontainers</a> Integrationstests in einer .NET-Anwendung umsetzen kann.</p>

<p>Möchte man die Docker-Container nicht innerhalb des Test-Codes verwalten, bietet es sich an, die Container unabhängig von der Test-Ausführung zu starten und zu stoppen. <a href="https://docs.docker.com/compose/">Docker Compose</a> und das Azure DevOps-Feature <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/service-containers">Service-Containers</a> bieten sich hier als Alternativen an.</p>

<!--more-->

<blockquote>
  <p>Der Beispielcode zu diesem Blog-Post ist auf Github verfügbar: <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> erlaubt es, mehrere Docker-Container in einer Datei zu definieren und zu verwalten. Diese können dann mit nur einem Befehl gestartet und gestoppt werden. Zudem lassen sich Netzwerke zwischen den Containern definieren, Speicher-Volumes erstellen und Umgebungsvariablen setzen. Für die hier genutzte Demo-Anwendung nutze ich eine <a href="https://mariadb.org">MariaDB</a>-Datenbank und einen <a href="https://www.phpmyadmin.net">phpMyAdmin</a>-Container. Etwas verkürzt sieht das entsprechende Docker Compose File so aus (die gesamte Konfiguration findet sich im <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>Mittels <code class="language-plaintext highlighter-rouge">docker compose up</code> bzw. <code class="language-plaintext highlighter-rouge">docker compose down</code> startet und stoppt man nun alle Container.</p>

<p>Möchte man einen MariaDB-Container für die Integrations-Tests seiner Anwendung nutzen, gibt es ein paar Dinge zu beachten. Man muss die Datenbank initialisieren und mit entsprechenden Testdaten befüllen. Dies kann man für <a href="https://xunit.net">xUnit</a> etwa mittels <a href="https://xunit.net/docs/shared-context">Shared Context</a> innerhalb seiner Test-Suite durchführen und vor jeden Test-Run ein <a href="https://en.wikipedia.org/wiki/Database_seeding">Seeding</a> der Datenbank durchführen. Nach dem Test-Run wird die Datenbank dann einfach wieder gelöscht.</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>Alternativ kann man auf eine Funktion des <a href="https://hub.docker.com/_/mariadb">MariaDB-Containers</a> zurückgreifen, die es ermöglicht, beliebiger SQL-Scripte beim Starten des Containers auszuführen. Dazu mountet man einen Ordner in den Container unter <code class="language-plaintext highlighter-rouge">/docker-entrypoint-initdb.d</code>. Alle SQL-Dateien in diesem Ordner werden dann beim Starten des Containers ausgeführt.</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>Neben dem initialen Befüllen der Datenbank muss man nach dem Start des Containers feststellen können, ob der Datenbankserver bereits Anfragen entgegennehmen kann. Für das Ausführen der Integrationstests in einer CI/CD-Pipeline ist dies relevant, damit die Tests erst ausgeführt werden, wenn der Container auch voll funktionsfähig ist. Für Docker-Container bietet sich hier die Option der <a href="https://docs.docker.com/reference/dockerfile/#healthcheck">healthchecks</a> an. MariaDB liefert bereits ein entsprechendes <a href="https://mariadb.com/kb/en/using-healthcheck-sh/">healthcheck.sh-Script</a> mit aus.</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-einer-azure-pipeline">Docker Compose in einer Azure Pipeline</h2>

<p>Mit dem zuvor angelegten Docker Compose File kann man die Integrations-Tests lokal und auch in einer Azure DevOps CI/CD-Pipeline ausführen. Zunächst starten wir mittels des <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/docker-compose-v0">DockerCompose@0</a>-Tasks unsere Container:</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>Anschliessend muss man noch mittels <code class="language-plaintext highlighter-rouge">healthcheck</code> auf die Betriebsbereitschaft des Datenbank-Containers warten.</p>

<p>Führt man seine Azure-Pipeline in einem <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/container-phases">Container-Job</a> aus, muss man den Datenbank-Container noch mit dem Netzwerk des Pipeline-Containers verbinden. Die Umgebungsvariable <code class="language-plaintext highlighter-rouge">$(Agent.ContainerNetwork)</code> enthält den Namen des Netzwerks, in dem der Pipeline-Container läuft.</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>Anschliessend können die Tests ausgeführt werden. Da die Konfiguration der Datenbankverbindung in der CI-Pipeline anders ist als auf dem lokalen PC oder in der Produktivumgebung, kann man diese etwa über ein <a href="https://learn.microsoft.com/en-us/visualstudio/test/configure-unit-tests-by-using-a-dot-runsettings-file">.runsettings-File</a> entsprechend setzen.</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 dem <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/dotnet-core-cli-v2">DotNetCoreCLI@2</a>-Task gibt man das File dann per Argument an.</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>Nach dem Test-Run werden die Container wieder gestoppt.</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>Als Alternative zum Aufsetzen und Starten von Docker-Containern mittels <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/docker-v2">Docker@2</a>- oder <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/docker-compose-v0">DockerCompose@0</a>-Task bietet Microsoft die Möglichkeit, Container-Ressourcen als <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/service-containers">Service-Container</a> innerhalb einer Pipeline bereitzustellen. Diese Container laufen parallel zu den Build- und Test-Jobs und können von diesen angesprochen werden. Damit eignen sich auch Service-Container zum Bereitstellen von etwa Datenbanken für Integrationstests.</p>

<p>Die Konfiguration der Service-Container erfolgt direkt in der <code class="language-plaintext highlighter-rouge">.yaml</code>-Datei der Pipeline-Definition; Für <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/release/define-multistage-release-process">klassische Azure Pipelines</a> steht das Feature nicht zur Verfügung. Die Syntax ähnelt sehr der von Docker Compose, wer sich hiermit auskennt, wird sich schnell zurechtfinden.</p>

<p>Container werden unter dem Abschnitt <code class="language-plaintext highlighter-rouge">resources</code> definiert und anschließend unter <code class="language-plaintext highlighter-rouge">services</code> exponiert (Container-Resources können auch für <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/container-phases">Container Jobs</a> genutzt werden).</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>Damit ist der MariaDB-Server auch schon unter der Adresse <code class="language-plaintext highlighter-rouge">localhost:3306</code> erreichbar.</p>

<p>Nutzt man zur lokalen Entwicklung keine Docker-Container, deren Konfiguration für Integrationstests innerhalb der CI-Pipeline wiederverwenden kann, bieten Service-Container eine einfache Möglichkeit, die benötigten Ressourcen bereitzustellen.</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="de" /><summary type="html"><![CDATA[In einem vorherigen Blog-Post habe ich gezeigt, wie man mit Hilfe von Testcontainers Integrationstests in einer .NET-Anwendung umsetzen kann. Möchte man die Docker-Container nicht innerhalb des Test-Codes verwalten, bietet es sich an, die Container unabhängig von der Test-Ausführung zu starten und zu stoppen. Docker Compose und das Azure DevOps-Feature Service-Containers bieten sich hier als Alternativen an.]]></summary></entry><entry xml:lang="de"><title type="html">Integrationstest in .NET mit Testcontainern</title><link href="https://www.production-ready.de/2024/04/27/integration-testing-with-testcontainers.html" rel="alternate" type="text/html" title="Integrationstest in .NET mit Testcontainern" /><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</id><content type="html" xml:base="https://www.production-ready.de/2024/04/27/integration-testing-with-testcontainers.html"><![CDATA[<p>In der Software-Entwicklung ist es essentiell, den geschriebenen Code ausreichend zu testen. Neben <a href="/2023/06/10/property-based-testing-in-csharp.html">Unit-Tests</a>, die einzelne Komponenten isoliert prüfen, betrachten Integrationstests das Zusammenspiel mehrerer Komponenten. Hat man nun aber externe Abhängigkeiten wie z.B. einen Datenbank oder einen Message Broker, wird das Testen komplexer.</p>

<p>Damit die externen Systeme auch in den Integrations-Tests reproduzierbar und vorhersehbar zur Verfügung stehen, kann man sie in Docker-Containern ausführen. Ein einfaches Setup solcher Service-Container ermöglicht die Bibliothek <a href="https://testcontainers.com">Testcontainers</a>.</p>

<!--more-->

<blockquote>
  <p>Der Beispielcode zu diesem Blog-Post ist auf Github verfügbar: <a href="https://github.com/davull/demo-docker-testcontainers">https://github.com/davull/demo-docker-testcontainers</a></p>
</blockquote>

<p>In einem <a href="/2024/05/10/integration-testing-with-docker.html">anschließenden Blog-Post</a> zeige ich, wie man Integrationstests mit Hilfe von <a href="https://docs.docker.com/compose/">Docker Compose</a> und Azure <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/service-containers">Service-Containers</a> umsetzen kann.</p>

<h2 id="integrationstests">Integrationstests</h2>

<p>Integrationstest sollen das Zusammenspiel von mehreren Komponenten eines Softwaresystems prüfen. Dies kann einzelne funktionale Einheiten betreffen aber auch einen gesamten Durchstich durch die Anwendung. So kann man für eine Web-API etwa einen HTTP-Aufruf an die Anwendung schicken, Daten in einer Datenbank manipulieren und das in einem Redis-Cache zwischengespeicherte Ergebnis wieder zurück an den Aufrufer liefern.</p>

<p>Das Verhalten von externen Systeme kann man nun zwar mittels <a href="https://en.wikipedia.org/wiki/Mock_object">Mocks</a> versuchen nachzubilden, einen wirklichen Mehrwert liefern Integrationstests aber erst dann, wenn sie auch mit echten Systemen interagieren. Damit bewegt man sich näher an einer Produktivumgebung und kann so auch Probleme finden, die für Mocks nicht sichtbar wären.</p>

<p>Die meisten Systeme kann man mittlerweile in einem <a href="https://www.docker.com/#build">Docker-Container</a> betreiben. Sollte mal kein vorkonfiguriertes Image zur Verfügung stehen, ist es zumeist einfach möglich, ein eigenes zu erstellen.</p>

<p>Diese Containerisierung von Anwendungen kann man sich beim Testen zu Nutze machen und vor jeden Testdurchlauf, etwas in einer CI/CD-Pipeline, die benötigten Systeme mit einem definierten Zustand starten. Nach dem Ausführen der Tests werden die Container einfach wieder gestoppt und gelöscht.</p>

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

<p>Das Projekt <a href="https://testcontainers.com">Testcontainers</a> bietet für viele Programmiersprachen Bibliotheken an, um das Aufsetzen und Ausführen von Docker Containern in Tests zu vereinfachen. Für .NET steht ein entsprechendes <a href="https://www.nuget.org/packages/Testcontainers">Nuget-Paket</a> zur Verfügung: <a href="https://dotnet.testcontainers.org">Testcontainers for .NET</a>.</p>

<p>Ein Testcontainer wird einfach vor der Ausführung der Test-Methoden definiert und gestartet und danach wieder gestoppt und gelöscht. Die <code class="language-plaintext highlighter-rouge">ContainerBuilder</code>-Klasse bietet eine Fluent-API, um die Container zu konfigurieren.</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>Um die Tests lokal ausführen zu können, muss Docker auf dem Rechner installiert sein und der Docker-Dienst laufen.</p>

<p>Best practice ist es, Container-Ports auf zufällige Host-Ports zu mappen, um Konflikte mit anderen laufenden Services und Containern zu vermeiden.</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>Um nach dem Start auf die Verfügbarkeit eines Dienstes innerhalb des Containers zu warten, stellt Testcontainers eine Reihe von <code class="language-plaintext highlighter-rouge">Wait</code>-Strategien zur Verfügung. So kann man etwa auf den Health-Status des Containers warten oder aber einen beliebigen TCP-Port auf Erreichbarkeit prüfen. Auch das Warten auf einen HTTP-Endpunkt ist möglich.</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>Viele Container-Images lassen sich per Umgebungsvariablen konfigurieren. Einem Testcontainer kann man diese einfach über die Methode <code class="language-plaintext highlighter-rouge">WithEnvironment(env)</code> mitgeben.</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="vorkonfigurierte-container">Vorkonfigurierte Container</h3>

<p>Testcontainers bietet eine Reihe von <a href="https://www.nuget.org/profiles/Testcontainers">vorkonfigurierten Containern</a> an, die die Nutzung unterschiedlicher Services vereinfacht. Hier entfallen dann etwa die Angabe des Images oder ein Port-Mapping. Ein <code class="language-plaintext highlighter-rouge">MySqlContainer</code> bietet direkt die Möglichkeit, den passenden Connection-String für die Datenbank zu erhalten.</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>Um die Konfiguration von Testcontainern zu zentralisieren und nicht für jeden Aufruf einer Test-Methode einen neuen Container zu erzeugen und zu starten, kann man die Möglichkeiten der unterschiedlichen Test-Frameworks nutzen, sogenannte Test-Fixtures zu erstellen.</p>

<p>Für <a href="https://xunit.net">xUnit</a> kann man etwa eine Klasse <code class="language-plaintext highlighter-rouge">DatabaseFixture</code> erstellen, welche das Initialisieren und Starten des Containers übernimmt. Eine Klasse, die das Interface <code class="language-plaintext highlighter-rouge">ICollectionFixture&lt;DatabaseFixture&gt;</code> implementiert, kann dann steuern, welche Tests sich eine Container-Instanz teilen.</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>Die entsprechenden Tests annotiert man mit dem <code class="language-plaintext highlighter-rouge">Collection</code>-Attribut und dem Collection-Namen, das den Fixture-Container bereitstellt.</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>Das Projekt in dem Github-Repository zu diesem Post zeigt ein vollständiges Beispiel: <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 lassen sich in einer CI/CD-Pipeline auf die gleiche Art ausführen wie lokal. Voraussetzung ist lediglich, dass auf dem Build Server ebenfalls Docker und die Docker CLI installiert sind. Im Falle von Azure DevOps ist dies für von Microsoft gehosteten Build-Agents bereits der Fall. Nutzt man eigene Build-Container, lässt sich die Docker CLI über den Task <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/docker-installer-v0">DockerInstaller@0</a> installieren.</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>Beim Ausführen des Test-Steps werden dann die benötigten Images heruntergeladen, falls noch nicht vorhanden, und die Container gestartet.</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="de" /><summary type="html"><![CDATA[In der Software-Entwicklung ist es essentiell, den geschriebenen Code ausreichend zu testen. Neben Unit-Tests, die einzelne Komponenten isoliert prüfen, betrachten Integrationstests das Zusammenspiel mehrerer Komponenten. Hat man nun aber externe Abhängigkeiten wie z.B. einen Datenbank oder einen Message Broker, wird das Testen komplexer. Damit die externen Systeme auch in den Integrations-Tests reproduzierbar und vorhersehbar zur Verfügung stehen, kann man sie in Docker-Containern ausführen. Ein einfaches Setup solcher Service-Container ermöglicht die Bibliothek Testcontainers.]]></summary></entry><entry xml:lang="de"><title type="html">Leichtgewichtige Architekturdokumentation mit ADRs</title><link href="https://www.production-ready.de/2023/12/28/lightweight-architecture-documentation-adr.html" rel="alternate" type="text/html" title="Leichtgewichtige Architekturdokumentation mit 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</id><content type="html" xml:base="https://www.production-ready.de/2023/12/28/lightweight-architecture-documentation-adr.html"><![CDATA[<p>Erreicht eine Software-Anwendung eine gewisse Größe, ist eine Dokumentation der einzelnen Komponenten und deren Zusammenspiel sinnvoll. Auch das Festhalten von Architektur-Entscheidungen ist wichtig, um auch Monate oder Jahre später noch nachvollziehen zu können, warum eine bestimmte Lösung gewählt wurde. Zukünftige Kolleg:innen und euer zukünftiges Ich werden es euch danken.</p>

<p>Eine leichtgewichtige Methode, um Architektur-Entscheidungen zu dokumentieren, stellen <a href="https://adr.github.io">Architectural Decision Records</a>, kurz ADRs, dar.</p>

<!--more-->

<h2 id="architektur-dokumentation">Architektur-Dokumentation</h2>

<p>Jedes Stück Software hat auch eine Software-Architektur. Ob diese aktiv herbeigeführt wird oder während der Entwicklung “passiert” ist, hängt von den Umständen und Anforderungen ab. Um Entscheidungen, die man auf dem Weg zum aktuellen Stand der Software getroffen hat, auch im Nachhinein noch nachvollziehen und ggf. bewerten zu können, ist eine Architektur-Dokumentation, oder zumindest eine Dokumentation der wichtigsten Entscheidungen, sinnvoll. Häufig werden solche Entscheidungen in einem Wiki, Confluence oder Word-Dokument festgehalten. Die Dokumentation erfolgt damit außerhalb des eigentlichen Quellcodes und unstrukturiert, was die Pflege und Aktualisierung erschwert.
Mit <a href="https://www.arc42.de">arc42</a> oder <a href="https://structurizr.com">Structurizr</a> existieren Frameworks, um unterschiedliche Aspekte einer Software und deren Architektur strukturiert zu dokumentieren. Für Projekte, die keine derart umfangreiche Dokumentation benötigen, bieten sich ADRs als leichtgewichtige Lösung an.</p>

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

<p>Das Ziel von <code class="language-plaintext highlighter-rouge">Architectural Decision Records</code> ist es nicht, die gesamte Architektur einer Software zu beschreiben oder widerzuspiegeln, sondern Entscheidungen, die zu der aktuellen Architektur geführt haben, festhalten.</p>

<p><code class="language-plaintext highlighter-rouge">ADRs</code> halten Architektur-Entscheidungen in reinen Textdateien fest und geben lediglich das Format vor. Jede Entscheidung erhält eine eigene Datei und eine fortlaufende Nummer. Für eine einfache Formatierungen wird <a href="https://www.markdownguide.org">Markdown</a> verwendet. Die Dateien sind Teil des Quellcode-Repositories und unterliegen damit auch der Versionskontrolle. ADRs können damit auch per Pull-Request eingebracht und diskutiert werden.</p>

<p>Auch wenn der Name den Kontext <code class="language-plaintext highlighter-rouge">Architektur</code> setzt, lassen sich ADRs für eine Vielzahl von technischen und nicht-technischen Entscheidungs-Dokumentationen nutzen.</p>

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

<p>Nach dem Erfinder <a href="https://cognitect.com/authors/MichaelNygard.html">Michael Nygard</a> bestehen ADRs aus den <a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions">folgenden Feldern</a>:</p>

<p><strong>Titel</strong>: Kurze Beschreibung der Entscheidung.</p>

<p><strong>Context</strong>: Beschreibung des Kontexts, in dem die Entscheidung getroffen wurde inkl. der technologischen, politischen oder sozialen Rahmenbedingungen. Die Formulierung ist neutral und beschreibt die vorliegenden Fakten.</p>

<p><strong>Decision</strong>: Beschreibt die im zuvor dargestellten Kontext getroffene Entscheidung. Die Formulierung ist aktiv und beschreibt, was getan wird: “Wir werden …”.</p>

<p><strong>Status</strong>: Der Status der Entscheidung. Mögliche Werte sind <code class="language-plaintext highlighter-rouge">proposed</code>, <code class="language-plaintext highlighter-rouge">accepted</code>, <code class="language-plaintext highlighter-rouge">deprecated</code> oder <code class="language-plaintext highlighter-rouge">superseded</code>.</p>

<p><strong>Consequences</strong>: Beschreibt den resultierenden Zustand nach der Entscheidung. Es werden sowohl positive als auch negative Konsequenzen aufgeführt.</p>

<p>Ein solches ADR-Dokument hat eine Länge von ein bis zwei Seiten. Es stellt eine Kommunikation mit zukünftigen Entwickler:innen oder uns selber dar. Daher ist es wichtig, dass die Dokumentation verständlich und nachvollziehbar ist und nicht nur eine Stichpunktliste enthält. Für den Dateinamen schlägt Nygard ein Format vor: <code class="language-plaintext highlighter-rouge">doc/arch/adr-NNN.md</code>. Über den eindeutigen Bezeichner kann man einzelne ADRs referenzieren, etwa wenn spätere Entscheidungen auf einer vorherigen aufbauen oder diese ersetzen.</p>

<p>Ob man für sein Projekt dem hier gezeigten Format folgt oder eine eigene Variante erarbeitet, ist zweitrangig. Wichtig ist, dass das Format über den Projekt-Verlauf hinweg konsistent eingehalten wird. <a href="https://www.linkedin.com/in/joelparkerhenderson/">Joel Parker Henderson</a> hat in einem GitHub-Repository <a href="https://github.com/joelparkerhenderson/architecture-decision-record/tree/main/locales/en/templates">eine Reihe von ADR-Templates</a> zusammengetragen.</p>

<h3 id="beispiel">Beispiel</h3>

<p>Der ADR für die Entscheidung über den Einsatz von <a href="https://www.rabbitmq.com">RabbitMQ</a> als Message Broker könnte wie folgt aussehen:</p>

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

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

Wir benötigen einen Message Broker, um asynchrone Nachrichten
zwischen den einzelnen Komponenten unserer Anwendung auszu-
tauschen. Die Lösung soll ausfallsicher betrieben werden können
und Nachrichten persistent speichern. Die Lösung soll im eigenen
Rechenzentrum gehostet und von unserem Service-Team betrieben
werden. RabbitMQ ist bereits in anderen Projekten im Einsatz
und das Service-Team mit dem Betrieb vertraut.

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

Wir werden RabbitMQ als Message Broker einsetzen. RabbitMQ 
erfüllt unsere Anforderungen und ist bereits im Unternehmen
etabliert. Dem Service-Team wird die Verantwortung für den
Betrieb übertragen. Wir teilen dem Service-Team das erwartete
Nachrichten-Aufkommen mit.

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

accepted

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

Aufgrund der Entscheidung muss im Software-Team Know-How für
den Einsatz von RabbitMQ aufgebaut werden. Das Service-Team
unterstützt das Software-Team bei der Integration. Der 
angedachte Einsatz von asynchronen Nachrichten zwischen den
Komponenten der Anwendung kann umgesetzt werden.
</code></pre></div></div>

<p>Mit einem solchen Dokument ist auch im Nachhinein nachvollziehbar, warum <code class="language-plaintext highlighter-rouge">RabbitMQ</code> und nicht etwa <code class="language-plaintext highlighter-rouge">Azure Service Bus</code> eingesetzt wird.</p>

<h2 id="werkzeuge">Werkzeuge</h2>

<p>Da es sich bei ADRs um eine strukturierte Variante von Dokumentation handelt, haben sich eine <a href="https://adr.github.io/#decision-capturing-tools">Reihe von Werkzeugen</a> etabliert.</p>

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

<p><a href="http://www.natpryce.com">Nat Pryce </a> hat mit <a href="https://github.com/npryce/adr-tools">adr-tools</a> ein Command Line Tool zum Arbeiten mit ADRs entwickelt. Damit ist es möglich, neue ADRs im von Michael Nygard beschriebenen Format anzulegen.</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>Neue ADRs können mit <code class="language-plaintext highlighter-rouge">adr new</code> angelegt werden.</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>Über weitere Befehle können ADRs verknüpft, aufgelistet und Inhaltsverzeichnisse generiert werden.</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> generiert aus ADR-Dateien eine HTML-Seite. Damit kann man ADRs etwa in seine CI/CD-Pipeline integrieren und die Dokumentation automatisch aktualisieren.</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>Die generierte Seite bietet eine Übersicht über alle ADRs und ermöglicht die Navigation zwischen den einzelnen Dokumenten. Die unterschiedlichen Status werden farblich hervorgehoben.</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="de" /><summary type="html"><![CDATA[Erreicht eine Software-Anwendung eine gewisse Größe, ist eine Dokumentation der einzelnen Komponenten und deren Zusammenspiel sinnvoll. Auch das Festhalten von Architektur-Entscheidungen ist wichtig, um auch Monate oder Jahre später noch nachvollziehen zu können, warum eine bestimmte Lösung gewählt wurde. Zukünftige Kolleg:innen und euer zukünftiges Ich werden es euch danken. Eine leichtgewichtige Methode, um Architektur-Entscheidungen zu dokumentieren, stellen Architectural Decision Records, kurz ADRs, dar.]]></summary></entry><entry xml:lang="de"><title type="html">Architektur-Refactoring mit ArchUnitNET</title><link href="https://www.production-ready.de/2023/12/10/architecture-refactoring-with-archunitnet.html" rel="alternate" type="text/html" title="Architektur-Refactoring mit 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</id><content type="html" xml:base="https://www.production-ready.de/2023/12/10/architecture-refactoring-with-archunitnet.html"><![CDATA[<p>Neben den funktionalen Anforderungen an ein Software-Produkt, welche sich mittels <a href="/2023/06/10/property-based-testing-in-csharp.html">Unit- und Integrationstests</a> absichern lassen, existieren auch eine Reihe von nicht-funktionalen Anforderungen. Aspekte der Sicherheit oder der Performance einer Anwendung lassen sich durch End-to-End Tests abdecken. Um die Wartbarkeit und Erweiterbarkeit einer Software sicherzustellen, kann man neben <a href="/2023/04/29/analyse-terraform-files-with-tfsec.html">statischer Code-Analyse</a> auch automatisierte Architektur-Tests einsetzen. Damit lassen sich Abhängigkeiten zwischen Komponenten oder die Einhaltung von Namenskonventionen überprüfen.</p>

<p>Auch beim Refactoring von bestehendem Code können sich solche Architektur-Tests als sinnvoll erweisen.</p>

<p>Mit <a href="https://archunitnet.readthedocs.io/en/latest/">ArchUnitNET</a> lassen sich Architektur-Regeln als Code definieren und automatisiert testen.</p>

<!--more-->

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

<p>ArchUnitNET ist ein .NET Port der in der Java-Welt etablierten Bibliothek <a href="https://www.archunit.org">ArchUnit</a>. Mit ArchUnitNET ist es möglich, Architektur-Regeln als Unit-Test zu hinterlegen und kontinuierlich und automatisiert zu überprüfen. Die Regeln werden in C# Code geschrieben und können in einem beliebigen Test-Framework ausgeführt werden. Um ArchUnitNET zu nutzen, genügt es, seinem Projekt das entsprechende <a href="https://www.nuget.org/packages/TngTech.ArchUnitNET">Nuget-Paket</a> hinzuzufügen. Für eine einfache Integration in die unterschiedlichen Testframeworks stehen Pakete für <a href="https://www.nuget.org/packages/TngTech.ArchUnitNET.xUnit">xUnit</a>, <a href="https://www.nuget.org/packages/TngTech.ArchUnitNET.NUnit">NUnit</a> und <a href="https://www.nuget.org/packages/TngTech.ArchUnitNET.MSTestV2">MS-Test</a> bereit.</p>

<h3 id="architektur-regeln">Architektur-Regeln</h3>

<p>ArchUnitNET-Test sind wie andere Unit-Tests auch aufgebaut. Es existiert eine Test-Klasse mit einer Reihe von Test-Methoden. Der besseren Performance wegen legt man eine statische Klassen-Property mit der Definition der Architektur an.</p>

<p>Hierzu macht man dem <code class="language-plaintext highlighter-rouge">ArchLoader</code> eine Reihe von Assemblies bekannt, welche den Code enthalten, der zu unserer Architektur gehören soll. Dazu kann man z.B. Marker-Klassen nutzen. <code class="language-plaintext highlighter-rouge">ArchLoader</code> kennt aber auch eine Reihe weiterer Methoden, etwa um Assemblies aus einem Verzeichnis zu laden.</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>Innerhalb der Architektur lassen sich nun Mengen von Klassen, Interfaces oder Methoden bilden, z.B. um Klassen in Schichten zu gruppieren. Nutzt man ein konsistentes Namensschema, kann man auch nach Namensmustern filtern und etwa alle Repository-Klassen zusammenfassen.</p>

<p>Bei der Definition hilft die eingängige Fluent-API von ArchUnitNET.</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>Um nun zu testen, ob alle Repository-Klassen auch wirklich im Data-Layer angesiedelt sind, schreiben wir einen Test, der eine entsprechende Regel definiert und unsere Architektur gegen diese prüft. Zur einfachen Verwendung kann man <code class="language-plaintext highlighter-rouge">ArchRuleDefinition</code> als <code class="language-plaintext highlighter-rouge">static using</code> einbinden.</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>Zum Überprüfen von gewollten oder ungewollten Abhängigkeiten zwischen Modulen genügen ebenfalls wenige Zeilen Code. Der nachfolgende Test prüft, ob irgendein Typ aus dem Desktop-Layer auf einen Typ aus dem Data-Layer zugreift.</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>Stellt ArchUnitNET eine Verletzung unserer Regeln fest, schlägt der Test fehl und liefert eine entsprechende Fehlermeldung.</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="nutzung-von-code-verhindern">Nutzung von Code verhindern</h3>

<p>Nicht nur die Organisation von Quellcode lässt sich so erzwingen, auch die Verwendung von bestimmten Klassen oder Methode kann man regulieren.
Um sicherzustellen, dass nur aus dem Data-Layer auf eine Datenbank zugegriffen wird, schränken wir den Zugriff auf Klassen aus der Assembly <code class="language-plaintext highlighter-rouge">Microsoft.Data.SqlClient</code> ein, in der sich unter anderem die Klasse <code class="language-plaintext highlighter-rouge">SqlConnection</code> befindet. Von unseren eigenen Assemblies (<code class="language-plaintext highlighter-rouge">ProjectAssemblies</code>) dürfen nur aus <code class="language-plaintext highlighter-rouge">DataLayer</code> heraus Zugriffe auf die Datenbank-Klassen erfolgen. Damit ArchUnitNET die Assembly <code class="language-plaintext highlighter-rouge">Microsoft.Data.SqlClient</code> kennt, müssen wir diese explizit in unsere Architektur-Definition mit aufnehmen.</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="architektur-refactoring">Architektur-Refactoring</h2>

<p>Hat man es mit einer bestehenden Code-Basis zu tun und möchte Änderungen an dieser vornehmen, kann man mittels ArchUnitNET den gewünschten Zielzustand vor den entsprechenden Maßnahmen definieren. Anschließend hat man die Möglichkeit, die bestehende Architektur Stück für Stück in die angestrebte Richtung zu ändern. Zu Beginn des Refactoring-Prozesses zeigen die jetzt noch roten Tests die Stellen an, an denen gearbeitet werden muss. Regeln, die man noch nicht umgesetzt hat, kann man derweil überspringen. Sobald alle Tests wieder aktiviert und grün sind, ist das Refactoring abgeschlossen.</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="system-ressourcen-finden">System-Ressourcen finden</h3>

<p>Die Verwendung von nicht-deterministischen System-Ressourcen wie der aktuellen Uhrzeit kann das Schreiben von Tests erschweren. Möchte man nicht direkt in Richtung <a href="/2023/09/28/refactor-to-purity.html">Pure Functions</a> rafactorn, kann es helfen, eine Abstraktion wie den seit <a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#time-abstraction">.NET 8</a> hinzugekommenen <a href="https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider">TimeProvide</a> zu nutzen.</p>

<p>Auch hier kann ein Test uns helfen, alle Zugriffe auf <code class="language-plaintext highlighter-rouge">DateTime.Now</code> oder <code class="language-plaintext highlighter-rouge">DateTime.UtcNow</code> zu finden.</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>Ein fehlschlagender Test zeigt uns auch hier wieder an, an welcher Stelle wir ansetzen müssen.</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 gehen hier an <a href="https://www.linkedin.com/in/andreas-lausen/">Andreas Lausen</a> für seinen <a href="https://github.com/andreaslausen/seacon23">Beispiel-Code</a> von der <a href="https://www.sea-con.de/seacon2023">SEACON 2023</a>.</p>

<h2 id="fazit">Fazit</h2>

<p>ArchUnitNET bietet die mächtige Möglichkeit, Architektur- und Code-Verwendungs-Regeln in Form von Unit-Tests zu definieren und automatisiert und kontinuierlich zu überprüfen. Nach kurzer Einarbeitung ist das Schreiben von neuen Regeln sehr einfach. Da sich diese Architektur-Regeln wie alle anderen Unit-Test verhalten, lassen sie sich auch in bestehende Codebasen einfügen. Das beste Vorgehen ist natürlich, sie so früh wie möglich zu definieren und zu nutzen.</p>

<blockquote>
  <p>Der Beispiel-Code zu diesem Post findet sich auf 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="de" /><summary type="html"><![CDATA[Neben den funktionalen Anforderungen an ein Software-Produkt, welche sich mittels Unit- und Integrationstests absichern lassen, existieren auch eine Reihe von nicht-funktionalen Anforderungen. Aspekte der Sicherheit oder der Performance einer Anwendung lassen sich durch End-to-End Tests abdecken. Um die Wartbarkeit und Erweiterbarkeit einer Software sicherzustellen, kann man neben statischer Code-Analyse auch automatisierte Architektur-Tests einsetzen. Damit lassen sich Abhängigkeiten zwischen Komponenten oder die Einhaltung von Namenskonventionen überprüfen. Auch beim Refactoring von bestehendem Code können sich solche Architektur-Tests als sinnvoll erweisen. Mit ArchUnitNET lassen sich Architektur-Regeln als Code definieren und automatisiert testen.]]></summary></entry><entry xml:lang="de"><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.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</id><content type="html" xml:base="https://www.production-ready.de/2023/11/11/kuma-azure-web-app-containers.html"><![CDATA[<p><a href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a> ist ein self-hosted Monitoring System, das es erlaubt, die Verfügbarkeit von unterschiedlichen Services zu überwachen. Es bietet eine Vielzahl von Monitoring-Typen an, darunter für verschiedene Arten von HTTP-Endpunkte wie Websites oder REST-APIs.</p>

<p>Da es auch als fertiges Docker Image zur Verfügung steht, kann man es einfach als Azure App Service deployen. Für ein Ausrollen mittels Terraform genügt eine einfache Konfiguration.</p>

<!--more-->

<blockquote>
  <p>Der Terraform-Code zu diesem Post findet sich auf 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>Um einen Docker Container in Azure als <a href="https://azure.microsoft.com/en-us/products/app-service/containers/">Web App for Container</a> zu betreiben, benötigen wir einen <a href="https://azure.microsoft.com/en-us/products/app-service/">App Service</a>, welcher in einem <a href="https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans">App Service Plan</a> beheimatet ist. Alternativ kann man Container auch in <code class="language-plaintext highlighter-rouge">Azure Container App</code> oder <code class="language-plaintext highlighter-rouge">Azure Kubernetes Service</code> betreiben, eine Übersicht bietet Microsoft <a href="https://azure.microsoft.com/en-us/products/category/containers/">hier</a>.</p>

<p>Damit die von Uptime Kuma gesammelten Daten (eine SQLite Datenbank) nicht bei jedem Neustart des Containers verloren gehen, persistieren wir sie in einem <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>Um die Ressourcen in Azure zu organisieren, legen wir eine neue Resource Group an. Dazu suchen wir im <a href="https://portal.azure.com/#view/Microsoft_Azure_Marketplace/MarketplaceOffersBlade">Azure Marketplace</a> nach <code class="language-plaintext highlighter-rouge">Recource Group</code> und klicken <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>Wir vergeben einen Namen, wählen eine Region und klicken <code class="language-plaintext highlighter-rouge">Review + Create</code>. Anschließend können wir unsere Services innerhalb der Resource Group anlegen.</p>

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

<p>Beim Anlegen des <code class="language-plaintext highlighter-rouge">Storage Accounts</code> wählen wir die zuvor erstellte Resource Group aus, legen Namen und Region fest und das Redundanz-Level. Für nicht-kritische Anwendungen genügt die günstigste Variante <code class="language-plaintext highlighter-rouge">Local-redundant storage (LRS)</code>. Die übrigen Einstellungen können beibehalten werden.</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>Sobald unser Storage Account provisioniert ist, können wir ein <code class="language-plaintext highlighter-rouge">File share</code> anlegen. Als Tier wählen wir <code class="language-plaintext highlighter-rouge">Hot</code>, die Backup-Option können wir nach Bedarf an- oder abschalten.</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>Um einen <code class="language-plaintext highlighter-rouge">App Service Plan</code> anzulegen, gehen wir analog vor. Als <code class="language-plaintext highlighter-rouge">Operation System</code> wählen wir <code class="language-plaintext highlighter-rouge">Linux</code>, der kleinste nicht kostenlose Pricing Plan <code class="language-plaintext highlighter-rouge">Basic B1</code> genügt. Dieser schlägt mit ca. 13 € / Monat zu Buche. Er bietet 1.75 GB Arbeitsspeicher und 1 vCPU. Je nach Anzahl der zu überwachenden Services kann es sinnvoll sein, einen <a href="https://azure.microsoft.com/en-us/pricing/details/app-service/linux/#pricing">größeren Plan</a> zu wählen. Ein Scale-Up ist aber auch später jederzeit möglich.</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>Nachdem nun ein App Service Plan und Storage zur Verfügung stehen, können den eigentlichen App Service anlegen. Um den Wizard zu starten, suchen wir im Azure Portal nach <code class="language-plaintext highlighter-rouge">Web App for Containers</code> oder <code class="language-plaintext highlighter-rouge">Web App</code>. Im Marketplace werden diese als unterschiedliche Einträge geführt, es handelt sich aber um den gleichen Service.</p>

<p>Als Publish-Methode wählen wir <code class="language-plaintext highlighter-rouge">Docker</code> und als Operating System <code class="language-plaintext highlighter-rouge">Linux</code>. Der hier gewählte Name ist auch zugleich die Subdomain, unter der der Service später verfügbar ist (&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>Im nächsten Reiter <code class="language-plaintext highlighter-rouge">Docker</code> wechseln wir die Image Source auf <code class="language-plaintext highlighter-rouge">Docker Hub</code> und tragen das <a href="https://hub.docker.com/r/louislam/uptime-kuma">gewünschte Image</a> inkl. Tag ein, z.B. <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>Nachdem wir den Wizard abgeschlossen haben, ruft Azure das Docker Image aus dem Hub ab und starten unseren Container. Beim ersten Start von Uptime Kuma dauert es einige Zeit, bis der Service läuft und die Webseite verfügbar ist. Unter dem Eintrag <code class="language-plaintext highlighter-rouge">Deployment Center</code> im Reiter <code class="language-plaintext highlighter-rouge">Logs</code> kann man den Prozess verfolgen.</p>

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

<p>Damit die Datenbank von Uptime Kuma nicht bei jedem Neustart des Containers verloren geht, mounten wir das zuvor angelegte File Share in den Container. Dazu wählen wir den Punkt <code class="language-plaintext highlighter-rouge">Configuration</code> und wechseln in den Reiter <code class="language-plaintext highlighter-rouge">Path mappings</code>. Dort fügen wir einen neuen Eintrag hinzu. Hier wählen wir den zuvor angelegten Storage Account und den für das File Share erstellten Storage Container aus. Als Storage Type wählen wir <code class="language-plaintext highlighter-rouge">Azure File</code>. Der Pfad, unter dem das File Share im Container gemountet wird, muss <code class="language-plaintext highlighter-rouge">/app/data</code> lauten. Hier legt Uptime Kuma seine Anwendungsdaten ab. Nach dem Speichern wird der Container neu gestartet die Daten landen nun in unserem 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>Rufen wir nun die Website der Web App auf, begrüßt uns Uptime Kuma mit dem Setup-Dialog, in dem wir den Admin-Nutzer anlegen können.</p>

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

<p>Als Alternative zum Azure Portal bietet es sich an, die Konfiguration unserer Anwendung in bester Infrastructure as Code-Manier mittels Terraform zu verwalten. Hierzu bemühen wir den <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest">azurerm Provider</a>. Die verschiedenen Arten zur <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs">Authentifizierung</a> sind in der Dokumentation beschrieben. Wir legen die gleichen Ressourcen an, wie wir es im Portal getan haben.</p>

<p>Die gesamte Konfigurationsdatei sieht wie folgt aus:</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>Die Variablen werden in einer separaten Datei definiert.</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 einer <code class="language-plaintext highlighter-rouge">.tfvar</code>-Datei werde die Werte für die Variablen gesetzt.</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>Nun können wir mit einem einzigen Befehl die gesamte Umgebung erzeugen und auch wieder löschen.</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="de" /><summary type="html"><![CDATA[Uptime Kuma ist ein self-hosted Monitoring System, das es erlaubt, die Verfügbarkeit von unterschiedlichen Services zu überwachen. Es bietet eine Vielzahl von Monitoring-Typen an, darunter für verschiedene Arten von HTTP-Endpunkte wie Websites oder REST-APIs. Da es auch als fertiges Docker Image zur Verfügung steht, kann man es einfach als Azure App Service deployen. Für ein Ausrollen mittels Terraform genügt eine einfache Konfiguration.]]></summary></entry><entry xml:lang="de"><title type="html">Refactor to Purity</title><link href="https://www.production-ready.de/2023/09/28/refactor-to-purity.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</id><content type="html" xml:base="https://www.production-ready.de/2023/09/28/refactor-to-purity.html"><![CDATA[<p><code class="language-plaintext highlighter-rouge">Pure Functions</code> sind Program-Methoden, die ohne Seiteneffekte auszulösen ausgeführt werden können. In der funktionalen Programmierung sind sie eher die Regel als die Ausnahme. In den meisten objektorientierten Sprachen hingegen begegnet man ihnen aber eher weniger, oder zumindest werden sie häufig nicht als das Mittel der Wahl in Betracht gezogen. Im <a href="https://dotnet.microsoft.com/en-us/">dotnet</a>-Umfeld wird viel über <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection">Dependency Injection</a> und mehr oder weniger umfangreiche Abstraktionen mittels Interfaces abgehandelt.</p>

<p>Wie man von einer Codebasis mit vielen solchen Indirektionen zu einer einfacheren Variante kommt, die ein Vieles an überflüssiger Komplexität entfernt, soll der folgende Artikel zeigen.</p>

<!--more-->

<h2 id="ausgangslage">Ausgangslage</h2>

<p>Als Ausgangspunkt für unser Refactoring nehmen wir ein fiktives Beispiel eines Online-Shops an. Der <a href="https://github.com/davull/demo-refactor-to-purity">Quellcode</a> ist auf GitHub verfügbar, für jeden Refactoring-Schritt gibt es eine eigene Branch im Repository.</p>

<blockquote>
  <p>Quellcode auf 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>Die Anwendung besteht aus einem Applikations-Projekt und einem für dazugehörige Tests. Der Aufbau folgt nicht streng einem Architektur-Style sondern soll lediglich zeigen, welche Komponenten man bei einem solchen System erwarten könnte. Zudem beschränken wir uns hier auf die Backend-Seite unseres Online-Shops. Die Ordner-Struktur sieht wie folgt aus:</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>Die Anwendung ist in C# geschrieben und verwendet ASP.NET <code class="language-plaintext highlighter-rouge">Controller</code>. Business-Logik ist in <code class="language-plaintext highlighter-rouge">Service</code>-Klassen implementiert, Domain-Modelle liegen im Ordner <code class="language-plaintext highlighter-rouge">Models</code>. Der Zugriff auf eine Datenbank erfolgt mittels <code class="language-plaintext highlighter-rouge">Repository</code>-<a href="https://medium.com/@pererikbergman/repository-design-pattern-e28c0f3e4a30">Pattern</a>. Die <a href="https://en.wikipedia.org/wiki/Plain_old_CLR_object">POCO</a>-Klassen für die Datenbank sind im Ordner <code class="language-plaintext highlighter-rouge">Data</code> untergebracht. Für die Kommunikation zwischen Controllern und Services kommt das <a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs">CQRS (Command Query Responsibility Segregation)</a>-Pattern zum Einsatz.</p>

<p>Zusammengehalten werden unsere einzelnen Komponenten mittels <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection">Dependency Injection</a>.</p>

<h3 id="abstraktion">Abstraktion</h3>

<p>In der Softwareentwicklung arbeitet man gerne mit Abstraktion. Ihrem eigentlichen Ziel, Komplexität und Wartungsaufwand zu reduzieren, wird sie aber oft nicht gerecht. Zudem werden häufig Abstraktionsschichten eingezogen, ohne das diese einen konkreten Nutzen bringen, aber “man macht es halt so”. Dadurch leidet nicht nur die Lesbarkeit des Codes, auch das Laufzeitverhalten kann erst nach einer Analyse der Abhängigkeiten verstanden werden. Die Abstraktionen in unserem Beispiel mögen für eine kleine Demo vielleicht künstlich erzwungen wirken, sind in realen Projekten aber durchaus immer wieder anzutreffen.</p>

<h4 id="basisklassen-und-marker-interfaces">Basisklassen und Marker-Interfaces</h4>

<p>Unsere Model-Klassen erben allesamt von einer abstrakten Basisklasse bzw. Record <code class="language-plaintext highlighter-rouge">ModelBase</code> welche keinerlei Implementierung mitbringt. Die Datenbank-POCOs implementieren ein Interface <code class="language-plaintext highlighter-rouge">IData</code>, welches zumindest eine Property <code class="language-plaintext highlighter-rouge">Id</code> definiert.</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>Im Ordner <code class="language-plaintext highlighter-rouge">Repositories</code> finden sich sowohl ein generisches Interface <code class="language-plaintext highlighter-rouge">IRepository&lt;T&gt;</code> als auch spezifische Interfaces je Datenbank-Tabelle bzw. POCO-Klasse, z.B. <code class="language-plaintext highlighter-rouge">ICustomerRepository</code>. Dazu eine abstrakte Basisklasse <code class="language-plaintext highlighter-rouge">AbstractRepository&lt;T&gt;</code>, welche nur eins-zu-eins die Methoden des generischen Interfaces implementiert.</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>Die Abstraktion des konkreten Datenbank-Zugriffs über ein Interface <code class="language-plaintext highlighter-rouge">IDatabase</code> kann sinnvoll sei, da man beim Testen externe Systeme wie eine Datenbank durch ein <a href="https://en.wikipedia.org/wiki/Mock_object">Mock-Objekt</a> ersetzen kann. Im Weiteren werden wir aber eine andere Lösung für dieses Problem finden.</p>

<p>Die konkreten Implementierungen der Repositories sehen dann zumeist nur so aus, dass die Aufrufe an die Basisklasse oder ein <code class="language-plaintext highlighter-rouge">IDatabase</code>-Objekt weitergeleitet werden.</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-und-cqrs">Services und CQRS</h4>

<p>Unsere Services sind allesamt als Interfaces und genau einer Implementierung ausgeführt; der Bedarf, mehrere Implementierungen zu haben und evtl. sogar zur Laufzeit auszutauschen besteht nicht.</p>

<p>Das Beispiel eines <code class="language-plaintext highlighter-rouge">ITaxService</code> zeigt den inflationären Einsatz von Interfaces. Das Interface definiert nur eine einzelne Methode, diese hat keinerlei Abhängigkeiten ausser ihrer direkten Methoden-Parameter.</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>Wie sieht nun ein (Unit-)Test für solchen Code aus? Das Testen der Methode <code class="language-plaintext highlighter-rouge">GetOrderItems()</code> des <code class="language-plaintext highlighter-rouge">OrderItemService</code> zeigt, wie viel Setup-Code bereits jetzt notwendig ist, um die Abhängigkeiten zu mocken und mit Daten zu füttern. Im Falle des <code class="language-plaintext highlighter-rouge">ITaxService</code>-Interfaces wird sogar die Businesslogik im Mock-Objekt implementiert.</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>Wie man bereits mit geringem Aufwand zu erheblich weniger Setup-Code für Test kommen kann, zeigt <a href="#schritt-1-test-dummies">Schritt 1</a> unseres Refactorings.</p>

<h3 id="code-analyse">Code-Analyse</h3>

<p>Die Exploration unsere Anwendung mittels <a href="https://www.hello2morrow.com/products/sonargraph/architect9">Sonargraph</a> zeigt, mit wie vielen Abhängigkeiten zwischen den einzelnen Klassen wir bereits jetzt zu tun haben.</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>Die Codebasis besteht zu diesem Zeitpunkt aus 917 Zeilen Quellcode in 53 Dateien und weist eine <a href="https://5c-design.info/metrics.html#connect">Average Component Dependency</a> <code class="language-plaintext highlighter-rouge">ACD</code> von 5,3 auf.</p>

<h2 id="schritt-1-test-dummies">Schritt 1: Test-Dummies</h2>

<p>Im ersten Schritt nehmen wir uns der Test-Klassen an. Eine gute Test-Suite ist die Grundlage eines sicheren Refactorings, deshalb wollen wir hier beginnen.</p>

<p>Getreu dem Motto <code class="language-plaintext highlighter-rouge">new is glue</code> verlagern wir das Instanziieren von Testdaten aus den Testmethoden heraus in <code class="language-plaintext highlighter-rouge">Dummies</code> aus. Zu dem Thema Dummy-Factories existiert ein dedizierter Blog Post <a href="/2023/09/14/test-setup-with-dummy-factories.html">Einfaches Test-Setup mit Dummy-Factories</a>, daher soll hier nur kurz auf Änderungen an unserem Beispiel-Code eingegangen werden.</p>

<blockquote>
  <p>Quellcode auf 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>Wir fügen eine Klasse <code class="language-plaintext highlighter-rouge">DataDummies</code> hinzu, welche uns das Instanziieren von Daten-Objekten abnimmt. Zudem definieren wir ein paar statische Instanzen von <code class="language-plaintext highlighter-rouge">Customer</code>-Objekten, die wir in unseren Tests verwenden können.</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>Für unsere Domain-Objekte verfahren wir ebenso. Hier können wir uns zunutze machen, dass POCO-Klassen und Domain-Modelle zumeist sehr ähnlich aufgebaut sind und wir die Daten-Objekte aus <code class="language-plaintext highlighter-rouge">DataDummies</code> verwenden können.</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>Unser Test-Setup wird damit schon etwas einfacher und vor allem resilienter gegenüber Änderungen an den Daten-Objekten, da wir diese nur noch an einer Stelle anpassen müssen.</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>Nach diesen Vorbereitungen können wir uns dem Refactoring des Produktivcodes zuwenden.</p>

<h2 id="schritt-2-interfaces-entfernen">Schritt 2: Interfaces entfernen</h2>

<p>Im zweiten Schritt wollen überflüssige Abstraktionen durch Interfaces und Basisklassen entfernen. Häufig wird damit argumentiert, dass Test-Code sich diese zunutze machen und Abhängigkeiten, die als Interfaces ausgeführt sind, einfach durch <a href="https://www.educative.io/answers/what-is-faking-vs-mocking-vs-stubbing">Mocks, Stubs oder Fakes</a> ersetzen kann. Bei externen Abhängigkeiten, etwa zu Datenbanken oder Email-Servern, ist dies sicher richtig; für selbst geschaffene Abstraktionen führt es aber zumeist nur zu unnötiger Komplexität und hohem Aufwand für das Test-Setup. Test-Mocks sind aufwendig zu pflegen und müssen Internas der echten Implementierung kennen und diese nachbauen.</p>

<p>Unser erster Ansatzpunkt ist der <code class="language-plaintext highlighter-rouge">TaxService</code>. Die Methode <code class="language-plaintext highlighter-rouge">CalculateTax()</code> ist bereits eine pure Funktion. Daher können wir das Interface <code class="language-plaintext highlighter-rouge">ITaxService</code> löschen, die Klasse und die Methode <code class="language-plaintext highlighter-rouge">static</code> machen und diese einfach direkt aufrufen. Es ist keine Dependency-Injection notwendig und der Test-Mock fällt ebenfalls weg.</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>Der entsprechende <a href="https://github.com/davull/demo-refactor-to-purity/commit/01283dc">Git-Commit</a> zeigt uns 43 gelöschte Zeilen.</p>

<p>Wenden wir uns nun den Service-Klassen <code class="language-plaintext highlighter-rouge">OrderService</code> und <code class="language-plaintext highlighter-rouge">OrderItemService</code> zu. Von den per <a href="https://freecontent.manning.com/understanding-constructor-injection/">Constructor Injection</a> bereitgestellten Abhängigkeiten (e.g. <code class="language-plaintext highlighter-rouge">ICustomerRepository</code>) benötigen wir nur einzelne Methoden oder gar nur den Rückgabewert einer Methode. Statt nun Repository-Klassen zu injizieren, übergeben wir Methoden-Pointer (<a href="https://learn.microsoft.com/de-de/dotnet/csharp/programming-guide/delegates/using-delegates">Delegates</a>) an die Service-Klassen. Dadurch entfallen die privaten Properties, die Klassen werden zustandslos und <code class="language-plaintext highlighter-rouge">static</code> und die Interfaces können entfernt werden.</p>

<p>Die Klasse <code class="language-plaintext highlighter-rouge">OrderService</code> hatte zuvor drei Abhängigkeiten.</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>Nach dem Refactoring sieht die Klasse so aus:</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>Aufgerufen wird die Methode <code class="language-plaintext highlighter-rouge">GetOrder()</code> nun einfach, indem wir die entsprechenden Methoden der Repositories als Parameter übergeben.</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>Unterscheiden sich die Signaturen der Methoden, können wir diese einfach per <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions">Lambda-Expression</a> anpassen.</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>Die entsprechenden Unit-Tests vereinfachen sich ebenfalls; wir müssen keine Mock-Objekte mehr zusammenbauen, sondern müssen lediglich Methoden definieren. Als lokale Lambda-Expressions sind dies Einzeiler.</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>Alternativ kann man bei puren Funktionen anstatt einer Methode den Rückgabewert eben dieser als Parameter übergeben (<a href="https://de.wikipedia.org/wiki/Referenzielle_Transparenz">Referenzielle Transparenz</a>). Für Methoden, die Seiteneffekte haben (z.B. Datenbank-Update) oder große Datenmengen filtern, ist dies aber nicht immer sinnvoll.</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>Dadurch, dass wir Abhängigkeiten nun nicht mehr per Dependency Injection in eine Klasse bringen, sondern per Methoden-Parameter übergeben, verschieben wir die Verantwortung für das Erzeugen und Verwalten der Abhängigkeiten an den aufrufenden Code.</p>

<h2 id="schritt-3-cqrs-entfernen">Schritt 3: CQRS entfernen</h2>

<p>Als nächstes entfernen wir das mittels <a href="https://github.com/jbogard/MediatR">MediatR</a> umgesetzte CQRS-Pattern aus unserer Codebasis. Die <a href="https://www.nuget.org/packages/MediatR">Library</a> ist großartig und CQRS ist ein mächtiges Werkzeug, wenn man tatsächlich den Bedarf hat, Commands und Queries zu trennen. In unserem Beispiel soll es aber zeigen, dass dies häufig nicht benötigt wird und vielleicht nur <a href="https://stackify.com/premature-optimization-evil/">Premature Optimization</a> ist, die nie zum Tragen kommt.</p>

<p>Statt den Quellcode, der Controller und Domain-Logik verbindet, über mehrere <code class="language-plaintext highlighter-rouge">IRequest</code> und <code class="language-plaintext highlighter-rouge">IRequestHandler&lt;&gt;</code> zu verteilen, fassen wir ihn in wenigen Integrations-Klassen zusammen.</p>

<p>Statt eines <code class="language-plaintext highlighter-rouge">AddOrderHandler</code> inkl. entsprechendem <code class="language-plaintext highlighter-rouge">AddOrderRequest</code> haben wir nun eine einzelne Methode, welche die benötigten Abhängigkeiten als Parameter übergeben bekommt und den Aufruf der Service-Klassen orchestriert.</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 einem weiteren Schritt können wir, wie zuvor für die Services, von dem injizieren von Repositories auf Methoden-Delegates umstellen. Dadurch kommen wir in die Lage, sämtliche <code class="language-plaintext highlighter-rouge">IRepository</code>-Interfaces entfernen zu können, da wir diese in unseren Tests nicht mehr substituieren müssen. Ein <a href="https://github.com/davull/demo-refactor-to-purity/commit/220178b">Git-Commit</a> zeigt das exemplarisch für das <code class="language-plaintext highlighter-rouge">IOrderRepository</code>.</p>

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

<p>Nachdem wir ein paar weitere <a href="https://github.com/davull/demo-refactor-to-purity/commit/9ebbd15">abstrakte Basisklassen</a> und Interfaces entfernt haben, schauen wir uns noch einmal die Repository-Klassen an. Sie weisen allesamt lediglich eine Abhängigkeit zu <code class="language-plaintext highlighter-rouge">IDatabase</code> auf. Die Umstellung von <a href="https://freecontent.manning.com/understanding-constructor-injection/">Constructor Injection</a> auf <a href="https://freecontent.manning.com/understanding-method-injection/">Method Injection</a> ist schnell gemacht.</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>Wenn wir auch hier noch einen Schritt weiter gehen und statt einer Instanz von <code class="language-plaintext highlighter-rouge">IDatabase</code> lediglich Delegates als Methoden-Parameter erwarten, können wir die Abhängigkeit von <code class="language-plaintext highlighter-rouge">IDatabase</code> ganz aus unseren Repositories entfernen.</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>Alternativ kann es sich hier anbieten, anstatt der Methoden bereits die Rückgabewerte dieser an unsere Service-Methode zu übergeben. Damit sind sämtliche Seiteneffekte eliminiert und wir haben eine <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>Die aufrufende Methode ist nun für das zusammensammeln der Daten verantwortlich.</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>Für externe Datenquellen wie Datenbank oder Dateien ist dieser Ansatz aufgrund des späten Filterns zumeist nicht geeignet, da er zu viele Daten lädt. Wir wollen ja nicht die gesamte Datenbank in den Speicher laden, nur um dann ein paar Datensätze zu verwenden. Für kleine Datenmengen oder Daten, die bereits im Arbeitsspeicher liegen, ist dies aber eine gute Möglichkeit, um die <a href="https://github.com/davull/demo-refactor-to-purity/commit/49cde56">Komplexität zu reduzieren</a>.</p>

<h2 id="ergebnis">Ergebnis</h2>

<p>Was haben wir nun mit den Refactoring-Schritten erreicht? Unsere Codebasis ist erheblich kleiner geworden, fast alle Interfaces konnten entfernt werden.</p>

<p>Der Dependency Graph zeigt deutlich weniger Linien. Die Anzahl der Codezeilen hat sich auf 715 (ca. 25 % weniger) reduziert, die Zahl der Dateien auf 34 (ca. 35 % weniger). Die Average Component Dependency ist von 5,3 auf 3,6 gesunken.</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>Neben den reinen Zahlen ist aber wichtiger, dass der Code nun einfacher zu verstehen und nachzuvollziehen ist. Man muss nicht mehr nach Interfaces und potenziellen Implementierungen suchen, um das Laufzeitverhalten nachvollziehen zu können.</p>

<p>Der gesamt Quellcode ist auf <a href="https://github.com/davull/demo-refactor-to-purity">GitHub verfügbar</a>.</p>]]></content><author><name>David</name></author><category term="c#" /><category term="refactoring" /><category term="purity" /><category term="de" /><summary type="html"><![CDATA[Pure Functions sind Program-Methoden, die ohne Seiteneffekte auszulösen ausgeführt werden können. In der funktionalen Programmierung sind sie eher die Regel als die Ausnahme. In den meisten objektorientierten Sprachen hingegen begegnet man ihnen aber eher weniger, oder zumindest werden sie häufig nicht als das Mittel der Wahl in Betracht gezogen. Im dotnet-Umfeld wird viel über Dependency Injection und mehr oder weniger umfangreiche Abstraktionen mittels Interfaces abgehandelt. Wie man von einer Codebasis mit vielen solchen Indirektionen zu einer einfacheren Variante kommt, die ein Vieles an überflüssiger Komplexität entfernt, soll der folgende Artikel zeigen.]]></summary></entry><entry xml:lang="de"><title type="html">Einfaches Test-Setup mit Dummy-Factories</title><link href="https://www.production-ready.de/2023/09/14/test-setup-with-dummy-factories.html" rel="alternate" type="text/html" title="Einfaches Test-Setup mit 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</id><content type="html" xml:base="https://www.production-ready.de/2023/09/14/test-setup-with-dummy-factories.html"><![CDATA[<p>Zur sinnvollen Verifizierung der Korrektheit von Software gehören aussagekräftige Tests. Eine immer wiederkehrende Tätigkeit ist das Schreiben von Quellcode, der <em>Testdaten</em> erzeugt oder beschreibt. Dies kann je nach Umfang der Daten mühsam sein und den Entwicklungsprozess ausbremsen und im schlimmsten Fall dazu führen, dass auf Tests ganz verzichtet wird. Eine elegante Möglichkeit, Testdaten einfach und schnell zu erzeugen, sind Dummy-Factories.</p>

<!--more-->

<h2 id="test-daten">Test-Daten</h2>

<p>Für das Aufrufen von Methoden aus Tests heraus, müssen diese zumeist mit definierten Eingabeparametern befüttert werden. Je weiter unten in der <a href="https://martinfowler.com/articles/practical-test-pyramid.html">Test-Pyramide</a> wir uns befinden, desto synthetischer werden diese Daten sein. Die Struktur der Daten kann von einfachen primitiven Datentype wie Strings, Boolean- oder Zahlwerten bis hin zu komplexen Objektstrukturen reichen.</p>

<p>Sehen wir uns eine imaginäre E-Commerce-Software mit folgenden Daten-Klassen an. Die Beispiele sind in C# gehalten, lassen sich aber auf andere Programmiersprachen übertragen.</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>Für eine Methode <code class="language-plaintext highlighter-rouge">OrderService.PlaceOrder(..)</code> könnte ein Unit-Test etwa so beginnen:</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>Bis wir aber an den Punkt kommen, unsere zu testende Methode mit den richtigen Parametern aufrufen zu können, müssen wir einige Zeilen an Setup-Code schreiben, nur um eine gültige <code class="language-plaintext highlighter-rouge">Order</code> zu erzeugen. Eine <code class="language-plaintext highlighter-rouge">Order</code> enthält einen <code class="language-plaintext highlighter-rouge">Customer</code> mit einer <code class="language-plaintext highlighter-rouge">Address</code> und eine Menge an <code class="language-plaintext highlighter-rouge">OrderItems</code> mit zugehörigen <code class="language-plaintext highlighter-rouge">Products</code>.</p>

<p>Dieser Code wird nicht nur sehr schnell redundant, sondern auch fehleranfällig. Wenn wir uns nun vorstellen, dass wir für jede Test-Methode eine neue <code class="language-plaintext highlighter-rouge">Order</code> erzeugen müssen, wird schnell klar, dass wir hier eine Menge Zeit verlieren können.</p>

<p>Ein weiterer Nachteil: Wenn sich an unseren Daten-Klassen etwas ändern, etwa weil ein <code class="language-plaintext highlighter-rouge">Customer</code> nun auch eine Telefonnummer haben muss, und wir Nicht-optionale Parameter hinzufügen, müssen wir die Constructor-Aufrufen in allen Testmethoden, die diese Daten-Instanzen erzeugen, anpassen.</p>

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

<p>Abhilfe schafft das Anlegen von Dummy-Factories. Dies sind Klassen und Methoden, die uns nach einmaligem Schreiben in allen Tests zur Verfügung stehen und das Erzeugen von Testdaten erleichtern. Dazu wird für jede Daten-Klasse eine Dummy-Methode angelegt und wo möglich mit Default-Parametern ausgestattet. Diese können bei Bedarf in den Testmethoden für den jeweiligen Anwendungsfall passend überschrieben werden. Für verschachtelte Datenstrukturen können etwa <a href="https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references">Nullable reference types
</a> verwendet werden, welche bei Nicht-vorhandensein wiederum durch Dummies ersetzt werden.</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>Mit kleinen Hilfsmethoden kann man sich das Leben weiter erleichtern.</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>Die oben gezeigte Testmethode kann nun wie folgt aussehen:</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>Durch das Verwenden eines <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive#static-modifier">using static</a> Statements lassen sich die Dummy-Methode direkt aufrufen, ohne den Klassennamen voranstellen zu müssen.</p>

<p>Damit ist unser Test-Setup auf wenige relevante Zeilen reduziert.</p>

<p>Sollte sich nun etwas an unseren Daten-Klassen ändern, müssen wir nur noch die Dummy-Methode anpassen und die Testmethoden bleiben unberührt.</p>

<h2 id="alternativen">Alternativen</h2>

<p>Zum generieren von mehr oder weniger zufälligen Test- oder Fake-Daten, existieren natürlich Libraries wie <a href="https://github.com/bchavez/Bogus">Bogus</a> oder <a href="https://fscheck.github.io/FsCheck/">FsCheck</a>. Hierbei gibt man natürlich etwas Kontrolle ab, das verwenden von Dummies ist wesentlich expliziter. Je nach Anwendungsfall kann das aber auch ein Vorteil sein und die zusätzliche Komplexität rechtfertigen.</p>]]></content><author><name>David</name></author><category term="c#" /><category term="testing" /><category term="dummy" /><category term="dummies" /><category term="de" /><summary type="html"><![CDATA[Zur sinnvollen Verifizierung der Korrektheit von Software gehören aussagekräftige Tests. Eine immer wiederkehrende Tätigkeit ist das Schreiben von Quellcode, der Testdaten erzeugt oder beschreibt. Dies kann je nach Umfang der Daten mühsam sein und den Entwicklungsprozess ausbremsen und im schlimmsten Fall dazu führen, dass auf Tests ganz verzichtet wird. Eine elegante Möglichkeit, Testdaten einfach und schnell zu erzeugen, sind Dummy-Factories.]]></summary></entry><entry xml:lang="de"><title type="html">Docker Scale-to-Zero mit Traefik und Sablier</title><link href="https://www.production-ready.de/2023/08/20/docker-scale-to-zero-with-traefik-sablier.html" rel="alternate" type="text/html" title="Docker Scale-to-Zero mit Traefik und 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</id><content type="html" xml:base="https://www.production-ready.de/2023/08/20/docker-scale-to-zero-with-traefik-sablier.html"><![CDATA[<p>Wenn man Web-Anwendungen oder -Services betreibt, die nur sporadisch genutzt werden, kann es sinnvoll sein, diese nur bei einem konkreten Bedarf zu starten. Im Umfeld von Docker-Containern und Kubernetes existiert für solche Anwendungsfälle das Konzept des Scale-to-Zero, also das Herunterskalieren von Workloads auf Null.</p>

<p>Für HTTP-Services in Verbindung mit einem Reverse-Proxy bietet <a href="https://acouvreur.github.io/sablier/">Sablier</a> eine einfache Möglichkeit, Scale-to-Zero in der eigenen Infrastruktur umzusetzen.</p>

<!--more-->

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

<p>Scale-to-Zero bezeichnet das vollständige Herunterskalieren von Workloads auf Null, so dass keine Ressourcen mehr verbraucht werden. Im Umfeld von Kubernetes existieren Lösungen wie <a href="https://keda.sh">Keda</a>, die aber eher auf Event-basierte Anwendungsfälle abzielen und nicht primär für HTTP-Services geeignet sind. Das <a href="https://github.com/kedacore/http-add-on">HTTP-Addon</a> für Keda befindet sich aktuell im Beta-Stadium.</p>

<p>Das vollständige Herunterfahren von Anwendungen kann einen Vorteil bringen, wenn man sehr viele Services mit jeweils wenigen Aufrufen betreibt. Damit sinkt die Anzahl der zeitgleich laufenden Container und der Ressourcenverbrauch. Setzt man hier auf einen Cloud-Anbieter und zahlt pro Ressource oder Zeiteinheit, lassen sich so Kosten sparen.</p>

<blockquote>
  <p>Der Beispiel-Code für diesen Post findet sich auf 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-und-traefik">Sablier und Traefik</h2>

<p><a href="https://acouvreur.github.io/sablier/">Sablier</a> ist eine leichtgewichtige Lösung, um HTTP-Services nach Bedarf zu starten und zu stoppen. Es unterstützt verschiedene <a href="https://acouvreur.github.io/sablier/#/providers/overview">Container-Provider</a>, aktuell Docker, Docker Swarm und Kubernetes. Um auf Basis von eingehenden Anfragen die entsprechenden Container hoch- und runterzufahren, existieren <a href="https://acouvreur.github.io/sablier/#/plugins/overview">Plugins</a> für die Reverse-Proxies Traefik, Nginx und Caddy. Da Sablier als eine API definiert ist, kann es prinzipiell auch in andere Systeme integriert werden.</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>Quelle <a href="https://acouvreur.github.io/sablier/#/plugins/overview">https://acouvreur.github.io/sablier/</a></p>
</blockquote>

<p>Nachfolgend wird der Einsatz von Sablier mit <a href="https://traefik.io/traefik/">Traefik</a> und Docker beschrieben. Für andere Reverse-Proxies und Container-Provider erfolgt die Konfiguration aber analog.</p>

<p>Ausgangspunkt ist eine Traefik-Instanz und ein Workload, der über Sablier gesteuert werden soll. Die Definition der Services erfolgt mittels Docker Compose.</p>

<p>Das Konfigurationsfile <code class="language-plaintext highlighter-rouge">docker-compose-traefik.yaml</code> für Traefik enthält eine einzige Service-Beschreibung und ein Netzwerk:</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>Der Workload besteht aus drei Services (hier alle durch traefik/whoami-Images abgebildet), welche eine verteilte Anwendung simulieren sollen. Die Datei <code class="language-plaintext highlighter-rouge">docker-compose-whoami.yaml</code> hat folgenden Inhalt:</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="sablier-einrichten">Sablier einrichten</h2>

<p>Sablier selber kann als Docker-Container oder als Binary ausgeführt werden. Wir verwenden hier den Container und erweitern die Datei <code class="language-plaintext highlighter-rouge">docker-compose-traefik.yaml</code> um eine weitere Service-Beschreibung:</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 benötigt Zugriff auf den Docker-Host-Service, daher mounten wir den Pfad <code class="language-plaintext highlighter-rouge">/var/run/docker.sock</code> an die gleiche Stelle im Container. Über den Parameter <code class="language-plaintext highlighter-rouge">--provider.name</code> geben wir den verwendeten Container-Provider <code class="language-plaintext highlighter-rouge">docker</code> an.</p>

<h3 id="traefik-plugin-hinzufügen">Traefik-Plugin hinzufügen</h3>

<p>Damit Traefik von Sablier erfährt und mit der Sablier-API kommunizieren kann, wird das <a href="https://plugins.traefik.io/plugins/633b4658a4caa9ddeffda119/sablier">Traefik-Plugin</a> für Sablier konfiguriert. Dazu wird die Konfigurationsdatei <code class="language-plaintext highlighter-rouge">traefik.yml</code> um folgende Zeilen erweitert:</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="konfiguration-vorbereiten">Konfiguration vorbereiten</h3>

<p>Damit Sablier nun die Container unseres Workloads starten und stoppen kann, müssen zwei Anpassungen vorgenommen werden.</p>

<p>Da Traefik keinen Zugriff mehr auf Container-Labels hat, wenn ein Container nicht mehr läuft (auf Null skaliert wurde), müssen wir zunächst die Konfiguration des Workload-Services von Container-Labels auf einen Konfigurations-Datei umstellen. Dies ist natürlich nur notwendig, wenn für die Traefik-Konfiguration auch Container-Labels genutzt wurden.</p>

<p>Die Labels aus dem Workload-Service werden in eine Datei <code class="language-plaintext highlighter-rouge">dynamic_config/whoami.yaml</code> übertragen.</p>

<p>Aus</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>wird</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>Anschliessend können wir mit dem Einbringen von Sablier fortfahren.</p>

<h3 id="sablier-konfigurieren">Sablier konfigurieren</h3>

<p>Sablier kennt zwei Arten (sogenannte <a href="https://acouvreur.github.io/sablier/#/strategies">Stategien</a>), auf eine eingehende HTTP-Anfrage zu reagieren, wenn der entsprechende Container nicht läuft:</p>
<ul>
  <li>Dynamic Strategy: Sablier liefert eine Status-Webseite aus, die dem Benutzer anzeigt, dass sein angefragter Service gerade gestartet wird und leitet ihn nach dem Start automatisch weiter bzw. lädt die Seite neu.</li>
  <li>Blocking Strategy: Sablier blockiert die Anfrage solange, bis der dahinterliegende Container gestartet ist und liefert dann die Antwort aus.</li>
</ul>

<p>Die Blocking Strategy eignet sich vor allem für APIs, so dass der aufrufende Client beim ersten Zugriff auf einen heruntergefahrenen Service lediglich eine längere Antwortzeit erhält, nicht aber mit Retries und Redirects umgehen muss.</p>

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

<p>Um unseren Workload mit der Dynamic Strategy zu konfigurieren, legen wir zunächst eine Traefik-Middelware für Sablier an. Diese wird in der Datei <code class="language-plaintext highlighter-rouge">dynamic_config/sablier.yaml</code> definiert:</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> zeigt auf unseren Sablier-Container, welchen wir in der Datei <code class="language-plaintext highlighter-rouge">docker-compose-traefik.yaml</code> definiert haben, Port 10.000 ist der Standardport.</p>

<p>Der Parameter <code class="language-plaintext highlighter-rouge">sessionDuration</code> gibt an, wie lange ein Container gestartet bleiben soll, nachdem der letzte Zugriff auf ihn erfolgt ist. Danach wird der Container wieder heruntergefahren.</p>

<p>Mit <code class="language-plaintext highlighter-rouge">names</code> geben wir die Namen der Container an, die Sablier steuern soll. Diese Namen müssen mit den Namen der Container in unserem <code class="language-plaintext highlighter-rouge">docker-compose-whoami.yaml</code>-File übereinstimmen (Parameter <code class="language-plaintext highlighter-rouge">container_name</code>).</p>

<p>Unterhalb des Keys <code class="language-plaintext highlighter-rouge">dynamic</code> wird das Verhalten der Dynamic Strategy konfiguriert. Wir können einen Anzeigenamen (<code class="language-plaintext highlighter-rouge">displayName</code>) für die Statusseite angeben, die Aktualisierungsrate (<code class="language-plaintext highlighter-rouge">refreshFrequency</code>) der Statusseite, ob Details (<code class="language-plaintext highlighter-rouge">showDetails</code>) angezeigt werden sollen und welches Theme (<code class="language-plaintext highlighter-rouge">theme</code>) verwendet werden soll. Themes stehen mehrere <a href="https://acouvreur.github.io/sablier/#/themes">zur Auswahl</a>, es können aber auch eigene Themes definiert werden.</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>Nun konfigurieren wir unseren Workload so, dass er die Sablier-Middleware auch nutzt. In der Datei <code class="language-plaintext highlighter-rouge">dynamic_config/whoami.yaml</code> ändern wir die Definition des whoami-Routers ab und fügen die Middleware hinzu:</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>Damit ist Sablier bereit, unseren Request entgegenzunehmen und den dahinterliegenden Container für uns zu starten und nach einer Minute Leerlauf wieder herunterzufahren.</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>Ein Hinweis zur Dynamic Strategy: Der Safari Browser auf iOS-Devices zeigt beim ersten Zugriff leider nicht die Status-Page an sondern liefert einen <code class="language-plaintext highlighter-rouge">This side can't be reached</code>-Fehler.</p>
</blockquote>

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

<p>Die Blocking Strategy wird analog zur Dynamic Strategy konfiguriert. Wir legen zunächst eine weitere Traefik-Middelware für Sablier an. Diese wird in ebenfalls der Datei <code class="language-plaintext highlighter-rouge">dynamic_config/sablier.yaml</code> definiert:</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><code class="language-plaintext highlighter-rouge">defaultTimeout</code> gibt an, wie lange auf das Starten des Ziel-Containers gewartet werden soll, bevor die Anfrage abgebrochen wird.</p>

<p>In der Datei <code class="language-plaintext highlighter-rouge">dynamic_config/whoami.yaml</code> nutzen wir die neue 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>Wenn wir unseren Workload nun mit der Blocking Strategy aufrufen, erhalten wir beim ersten Zugriff eine längere Antwortzeit, da Sablier den dahinterliegenden Container erst starten muss.</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="fazit">Fazit</h2>

<p>Sablier bietet eine einfache Möglichkeit, Scale-to-Zero für HTTP-Services umzusetzen. Wenn man sich einmal die notwendigen Informationen aus den verschiedenen Dokumentationen für Sablier, Traefik und dem Traefik-Plugin zusammengesucht hat, ist die Konfiguration nicht komplex. Allerdings ist die Dokumentation mitunter recht dünn und man trifft immer mal wieder auf leere Seiten.</p>

<p>Der Code der Beispiele mit Commits zu den einzelnen Konfigurations-Schritten findet sich auf 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="de" /><summary type="html"><![CDATA[Wenn man Web-Anwendungen oder -Services betreibt, die nur sporadisch genutzt werden, kann es sinnvoll sein, diese nur bei einem konkreten Bedarf zu starten. Im Umfeld von Docker-Containern und Kubernetes existiert für solche Anwendungsfälle das Konzept des Scale-to-Zero, also das Herunterskalieren von Workloads auf Null. Für HTTP-Services in Verbindung mit einem Reverse-Proxy bietet Sablier eine einfache Möglichkeit, Scale-to-Zero in der eigenen Infrastruktur umzusetzen.]]></summary></entry><entry xml:lang="de"><title type="html">DNS-Automatisierung mit DNSControl</title><link href="https://www.production-ready.de/2023/07/02/dnscontrol.html" rel="alternate" type="text/html" title="DNS-Automatisierung mit 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</id><content type="html" xml:base="https://www.production-ready.de/2023/07/02/dnscontrol.html"><![CDATA[<p>Die Konfiguration von DNS-Zonen gehört bei den meisten (Web-)Software-Projekte dazu. Ob beim initialen Setup oder im Laufe der Zeit müssen immer wieder Domains registriert und DNS-Einträge eingerichtet oder angepasst werden. Das kann man zwar zumeist über das Webinterface des entsprechenden Anbieters von Hand erledigen, was aber fehleranfällig, schlecht nachvollziehbar und nicht zu automatisieren ist.</p>

<p>Nutzt man zur Provisionierung seiner Cloud-Infrastruktur Infrastructure-as-Code Tools wie Terraform, kann man die DNS-Konfiguration natürlich auch darüber steuern. Allerdings ist man damit bei der Auswahl des DNS-Providers auf die großen Hyperscaler beschränkt.</p>

<p>Einfacher und mit weniger Konfigurationsaufwand erlaubt es <a href="https://docs.dnscontrol.org">DNSControl</a>, die DNS-Konfiguration für dutzende große und kleine Provider als Javascript-Code zu definieren. Dadurch lässt sich die DNS-Umgebung automatisiert und reproduzierbar aufsetzen und verwalten. Die Konfiguration ist versioniert in einem Git-Repository abgelegt und kann einem Code-Review unterzogen werden, was das Vorgehen perfekt in die DevOps-Toolchain integriert.</p>

<!--more-->

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

<p>Die <a href="https://docs.dnscontrol.org/getting-started/getting-started">Installation</a> von DNSControl erfolgt über einen Paket-Manager wie <code class="language-plaintext highlighter-rouge">brew</code> oder einfach als Download des Binaries von der <a href="https://github.com/StackExchange/dnscontrol/releases">Github-Seite</a> des Projekts. Das in Go geschrieben Program ist für Windows, Linux und MacOS verfügbar. Darüber hinaus existiert ein fertiges Docker-Image.</p>

<h2 id="konfiguration">Konfiguration</h2>

<p>DNSControl kennt DNS-Registrars und DNS-Service-Provider (DSP). Bei einem Registrar lassen sich neue Domains registrieren und die zuständigen Nameserver festlegen. Ein DSP verwaltet die DNS-Zonen und bedient DNS-Anfragen. Häufig dient ein Registrar auch zugleich als DSP. Die Konfiguration muss dennoch für beide Entitäten erfolgen.</p>

<h3 id="credentials-hinterlegen">Credentials hinterlegen</h3>

<p>In der Datei <code class="language-plaintext highlighter-rouge">creds.json</code> werden die genutzten Provider und Registrars hinterlegt. Die meisten Provider benötigen Zugangsdaten in Form eines API-Key oder Ähnlichem, die genaue Syntax lässt sich der <a href="https://docs.dnscontrol.org/service-providers/providers">Dokumentation</a> des jeweiligen Providers entnehmen. Einigen Provider lassen sich nur als DSP nutzen, andere nur als Registrar. Zum Erzeugen eines lokalen Zone-Files für eine BIND-Konfiguration genügt die Angabe des Typs <code class="language-plaintext highlighter-rouge">BIND</code>. Nutzt man keinen Registrar, kann man den Typ <code class="language-plaintext highlighter-rouge">NONE</code> verwenden.</p>

<p>Um die Zugangsdaten nicht im Klartext in der Konfigurationsdatei zu hinterlegen, kann man hier Umgebungsvariablen nutzen. Diese werden in der Form <code class="language-plaintext highlighter-rouge">$NAME</code> in der Konfigurationsdatei referenziert.</p>

<p>Das folgende Beispiel konfiguriert BIND, einen Dummy-Eintrag und Hetzner als 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>Ob die Konfiguration korrekt ist, kann DNSControl mittels <code class="language-plaintext highlighter-rouge">check-creds</code> prüfen. Über den Command <code class="language-plaintext highlighter-rouge">get-zones</code> lassen sich die DNS-Zonen, die beim DNS-Provider bereits hinterlegt sind, ausgeben. Als Formate stehen unter anderem <code class="language-plaintext highlighter-rouge">tsv</code> (tab separated value) und das BIND-Format <code class="language-plaintext highlighter-rouge">zone</code> zur Verfügung. <code class="language-plaintext highlighter-rouge">nameonly</code> zeigt nur die Namen der Zonen an.</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="dns-zonen-konfigurieren">DNS-Zonen konfigurieren</h2>

<p>Die Konfiguration der DNS-Zonen erfolgt per Javascript-Code, standardmäßig in der Datei <code class="language-plaintext highlighter-rouge">dnsconfig.js</code>. Dadurch ist man sehr flexibel was wiederkehrende Einstellungen wie DNS- oder Mail-Server angeht.</p>

<p>Über die Funktionen <code class="language-plaintext highlighter-rouge">NewRegistrar()</code> und <code class="language-plaintext highlighter-rouge">NewDnsProvider()</code> liesst man die konfigurierten Provider aus der <code class="language-plaintext highlighter-rouge">creds.json</code> Datei ein, die Einträge identifiziert man über den Namen.</p>

<p>Die Funktion <code class="language-plaintext highlighter-rouge">D()</code> definiert eine DNS-Zone. Als Parameter werden der Name der Zone, der Registrar, der DSP und eine Liste von DNS-Einträge übergeben. Für die unterschiedlichen Typen von Einträgen stehen jeweils Funktionen, sogenannte <a href="https://docs.dnscontrol.org/language-reference/domain-modifiers">Domain Modifiers</a> bereit, etwa <code class="language-plaintext highlighter-rouge">A()</code> und <code class="language-plaintext highlighter-rouge">AAAA()</code> für A- bzw. AAAA-Records.</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>Zur Konstruktion von komplexeren Records stehen Builder-Funktionen bereit. Das Resultat kann einfach in die Liste der DNS-Einträge aufgenommen werden.</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>Hat man mehrere Domains zu verwalten, bietet es sich an, die Konfiguration auf mehrere Dateien aufzuteilen. Diese können in der Hauptdatei <code class="language-plaintext highlighter-rouge">dnsconfig.js</code> importiert werden.</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="vorhandene-dns-zonen-importieren">Vorhandene DNS-Zonen importieren</h3>

<p>Möchte man bereits vorhandene DNS-Zonen in DNSControl überführen, kann man sich etwas Tipparbeit sparen, indem man <code class="language-plaintext highlighter-rouge">get-zones</code> mit dem Output-Format <code class="language-plaintext highlighter-rouge">js</code> nutzt und den Inhalt mittels <code class="language-plaintext highlighter-rouge">--out [file].js</code> in eine Datei schreiben lässt. Den generierten Code kann man nun als Ausgangspunkt für die eigene Konfiguration nutzen.</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>Um in seinen Javascript-Dateien ein Type-Checking und Intellisense ala TypeScript zu erhalten, bietet DNSControl die nette Möglichkeit, ein TypeScript Declaration File zu generieren.</p>

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

<p>Am Anfang der <code class="language-plaintext highlighter-rouge">dnsconfig.js</code> referenziert man die Datei.</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="änderungen-prüfen-und-anwenden">Änderungen prüfen und anwenden</h2>

<p>Zur Validierung der Konfiguration bietet DNSControl den Befehl <code class="language-plaintext highlighter-rouge">check</code>. Dieser prüft die Javascript-Dateien auf Fehler, greift aber noch auf keinen DNS-Provider zu. Der Befehl <code class="language-plaintext highlighter-rouge">preview</code> vergleicht die aktuelle Konfiguration mit dem Stand der DNS-Zonen beim Provider und zeigt die potenziellen Änderungen an. Im folgenden Beispiel wird ein CNAME-Record hinzugefügt und ein A-Record geändert.</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>Entsprechen die Änderungen dem erwarteten Ergebnis, kann man sie mittels <code class="language-plaintext highlighter-rouge">push</code> anwenden.</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>Damit ist man in der Lage, eine gesamte DNS-Landschaft mit einem Infrastructure-as-Code-Ansatz zuverlässig und nachvollziehbar zu verwalten.</p>

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

<p>Um Änderungen an der DNS-Konfiguration nun nicht lokal ausführen zu müssen, bietet es sich an, eine Build-Pipeline zu erstellen. Diese kann auch zum Validieren von Pull-Requests mit DSN-Änderungen genutzt werden. Der hier gezeigte Code nutzt Azure DevOps, für andere CI/CD-Systeme sollte sich aber ein ähnlicher Ansatz finden lassen.</p>

<p>Hat man DNSControl auf seinem Build-Agent nicht installiert, z.B. weil man Hosted Agents von Microsoft oder eigene Docker-Basierte Agents nutzt, kann man dies mit einem einfachen CmdLine-Task erledigen. Ordner erzeugen, Datei herunterladen und entpacken und das Binary ausführbar machen.</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>Anschließend kann man die Konfiguration validieren und die Änderungen anzeigen lassen. Den API-Key für den Provider kann man als Secret-Variable in der Pipeline-Konfiguration hinterlegen oder etwa aus einem Azure KeyVault beziehen. Bei der Verwendung einer Variable ist das explizite Mapping auf eine Umgebungsvariable wichtig, ansonsten ist der API-Key im Script nicht verfügbar.</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>Entsprechen die Änderungen dem gewünschten Ergebnis, kann man diese in einem Deployment-Step anwenden.</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="de" /><summary type="html"><![CDATA[Die Konfiguration von DNS-Zonen gehört bei den meisten (Web-)Software-Projekte dazu. Ob beim initialen Setup oder im Laufe der Zeit müssen immer wieder Domains registriert und DNS-Einträge eingerichtet oder angepasst werden. Das kann man zwar zumeist über das Webinterface des entsprechenden Anbieters von Hand erledigen, was aber fehleranfällig, schlecht nachvollziehbar und nicht zu automatisieren ist. Nutzt man zur Provisionierung seiner Cloud-Infrastruktur Infrastructure-as-Code Tools wie Terraform, kann man die DNS-Konfiguration natürlich auch darüber steuern. Allerdings ist man damit bei der Auswahl des DNS-Providers auf die großen Hyperscaler beschränkt. Einfacher und mit weniger Konfigurationsaufwand erlaubt es DNSControl, die DNS-Konfiguration für dutzende große und kleine Provider als Javascript-Code zu definieren. Dadurch lässt sich die DNS-Umgebung automatisiert und reproduzierbar aufsetzen und verwalten. Die Konfiguration ist versioniert in einem Git-Repository abgelegt und kann einem Code-Review unterzogen werden, was das Vorgehen perfekt in die DevOps-Toolchain integriert.]]></summary></entry><entry xml:lang="de"><title type="html">Property-based testing in C# mit FsCheck</title><link href="https://www.production-ready.de/2023/06/10/property-based-testing-in-csharp.html" rel="alternate" type="text/html" title="Property-based testing in C# mit 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</id><content type="html" xml:base="https://www.production-ready.de/2023/06/10/property-based-testing-in-csharp.html"><![CDATA[<p>Zum Handwerkszeug bei der Entwicklung von Software gehört es, (sinnvolle) automatisierte Tests zu schreiben. Mittels Unit-, Integration- und End-to-end-Tests wird die korrekte Funktionalität unseres Codes sichergestellt und wir erhalten die Sicherheit, dass wird beim Umsetzen neuer Anforderungen und beim Refactoring bestehenden Codes keine vorhandene Funktionalität kaputt machen. Michael Feathers definiert Legacy Software sogar als Code, der nicht getestet ist.</p>

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

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

<p>Neben den verbreiteten Ansätzen von Example-based testing oder Snapshot testing gibt es noch weitere Möglichkeiten, die uns helfen, die Korrektheit unseres Codes zu überprüfen. Einer dieser Ansätze ist Property-based testing.</p>

<p>Property-based testing prüft nicht einzelne Werte auf Übereinstimmung, sondern zielt auf die Verifikation von Eigenschaften ab, die unsere Implementierung aufweisen müssen. So ist für die mathematische Addition die Kommutativität eine Eigenschaft, die wir überprüfen können, ohne uns konkrete Zahlen anzuschauen. Für die Addition zweier Zahlen <code class="language-plaintext highlighter-rouge">a</code> und <code class="language-plaintext highlighter-rouge">b</code> gilt immer:</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> oder <code class="language-plaintext highlighter-rouge">Property-based testing</code> (PBT) ist ein Testverfahren, dass sich in der funktionalen Programmierung etabliert hat, etwa durch das Haskell-Package <a href="https://hackage.haskell.org/package/QuickCheck">QuickCheck</a>. Prädestiniert ist es für Problemstellungen, die wesentlich einfacher zu verifizieren als zu lösen sind. Aber auch beim Testen von alltäglicheren Problemen kann es gute Dienste leisten.</p>

<p>Beim Property-based testing definiert man Eigenschaften, die der zu testende Code für eine Menge von Eingabedaten erfüllen muss. Eine solche Eigenschaft ist dabei nicht wie eine <code class="language-plaintext highlighter-rouge">Property</code>, eine Klassen-Eigenschaft, in C# zu verstehen.</p>

<p>Die Eigenschaft <code class="language-plaintext highlighter-rouge">Kommutativität</code> der mathematischen Addition lässt sich in C# als Funktion beschreiben.</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>Die Definition der <code class="language-plaintext highlighter-rouge">Addition</code> besagt, dass das <code class="language-plaintext highlighter-rouge">Kommutativgesetz</code> für sämtliche Kombinationen von <code class="language-plaintext highlighter-rouge">a</code> und <code class="language-plaintext highlighter-rouge">b</code> gilt, unsere Funktion <code class="language-plaintext highlighter-rouge">commutativity</code> also für alle Integer-Werte <code class="language-plaintext highlighter-rouge">true</code> zurück liefern muss.</p>

<p>Wenn wir unsere eigene <code class="language-plaintext highlighter-rouge">Addition</code>-Funktion implementieren, können wir diese Eigenschaft nutzen, um die Korrektheit unserer Implementierung zu überprüfen.</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>Ein naiver Ansatz, die Korrektheit unserer <code class="language-plaintext highlighter-rouge">Addition</code>-Funktion zu testen, ist es, die Funktion mit ein paar Beispiel-Werten aufzurufen und die Rückgabewerte zu überprüfen, hier mittels <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>Da uns die konkreten Werte von <code class="language-plaintext highlighter-rouge">a</code> und <code class="language-plaintext highlighter-rouge">b</code> aber gar nicht interessieren, können wir sie auch zufällig generieren lassen. Dies können wir für beliebig viele Kombinationen aus <code class="language-plaintext highlighter-rouge">a</code> und <code class="language-plaintext highlighter-rouge">b</code> machen, hier sind es 1.000 Durchläufe. Damit haben wir statt der drei konkreten Werte nun 1.000 zufällig generierte Werte getestet und können uns bereits ein Stück sicherer sein, dass unsere <code class="language-plaintext highlighter-rouge">Addition</code>-Funktion korrekt arbeitet.</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>Allerdings bringt dieses Vorgehen auch ein paar Nachteile mit sich: Offensichtlich ist, dass es mehr Code bedarf und unser Testfall unübersichtlicher wird. Zudem sind die generierten Werte willkürlich gewählt und decken vielleicht nicht den gesamten Wertebereich ab.
Ein größeres Problem ist aber, dass wir bei einem Fehler nicht wissen, mit welchen Eingabewerten der Fehler aufgetreten ist, da sie bei jedem Durchlauf zufällig generiert werden.</p>

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

<p>Die in F# geschriebene Bibliothek <a href="https://fscheck.github.io/FsCheck/">FsCheck</a> schafft hier Abhilfe und lässt sich auch in C# verwenden. Die gute <a href="https://fscheck.github.io/FsCheck//Properties.html">Dokumentation</a> verwendet zumeist F#-Syntax aber zeigt auch Beispiele in C#.</p>

<p>Sie bietet Funktionen, um uns das Testen von Eigenschaften zu erleichtern. Ein Test in <code class="language-plaintext highlighter-rouge">FsCheck</code> besteht im Grunde aus folgenden drei Teilen:</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>So können wir die Eigenschaft <code class="language-plaintext highlighter-rouge">Kommutativität</code> mit FsCheck wie folgt testen.</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>Noch einfacher wird es, wenn wir uns der Erweiterung <a href="https://www.nuget.org/packages/FsCheck.NUnit">FsCheck.NUnit</a> bedienen (für XUnit existiert äquivalent <a href="https://www.nuget.org/packages/FsCheck.Xunit/">FsCheck.Xunit</a>). Anstatt <code class="language-plaintext highlighter-rouge">[Test]</code> bzw. <code class="language-plaintext highlighter-rouge">[Fact]</code> annotieren wir unsere Testmethode mit <code class="language-plaintext highlighter-rouge">[Property]</code> und können die Eigenschaften direkt als Parameter übergeben. FsCheck generiert automatisch Eingabewerte des entsprechenden Typs.</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>Möchten wir etwas über die Verteilung der generierten Eingabewerte erfahren, können wir sie nach bestimmten Bedingungen klassifizieren. Durch den Aufruf von <code class="language-plaintext highlighter-rouge">Classify()</code> erhalten wir einen Wert vom Typ <code class="language-plaintext highlighter-rouge">Property</code>, so dass wir den Rückgabewert der Testmethode entsprechend anpassen müssen.</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>Die entsprechende Ausgabe liefert in etwa folgendes Ergebnis:</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>Möchte man zu jedem einzelnen Testcase eine Ausgabe haben, hilft die <code class="language-plaintext highlighter-rouge">Collect()</code>-Methode.</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>Die Ausgabe liefert für jede Kombination eine Zeile inkl. Verteilung.</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="bedingungen">Bedingungen</h3>

<p>Kommen für einen bestimmten Testfall nicht alle Werte des Datentyps eines Eingangsparameters in Frage, können wir mittels <code class="language-plaintext highlighter-rouge">When()</code> eine Vorbedingung definieren. Um bei der Prüfung der <code class="language-plaintext highlighter-rouge">Division</code> nicht durch 0 zu teilen, schließen wir diesen Fall aus.</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>Wichtig hierbei: Die Definition der <code class="language-plaintext highlighter-rouge">property</code> ist als Methode ausgeführt, um eine <code class="language-plaintext highlighter-rouge">lazy evaluation</code> durch FsCheck zu ermöglichen.</p>

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

<p>Um passende <a href="https://fscheck.github.io/FsCheck//TestData.html">Testdaten</a> zu generieren, nutzt FsCheck <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>. Die Klasse <code class="language-plaintext highlighter-rouge">Gen</code> bietet mit den drei Funktionen <code class="language-plaintext highlighter-rouge">Choose()</code>, <code class="language-plaintext highlighter-rouge">Constant()</code> und <code class="language-plaintext highlighter-rouge">OneOf()</code> die Basis für die Generierung von Werten. Über die Methoden <code class="language-plaintext highlighter-rouge">Select()</code> und <code class="language-plaintext highlighter-rouge">Where()</code> können die Werte transformiert und gefiltert werden.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Generiert eine Liste von Zahlen zwischen 0 und 100, die durch 2 teilbar sind</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">// Wählt zufällig einen der beiden Werte "ja" oder "nein" aus</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">"ja"</span> <span class="p">:</span> <span class="s">"nein"</span><span class="p">);</span>
</code></pre></div></div>

<p>Über die Methode <code class="language-plaintext highlighter-rouge">Sample()</code> liefert ein Generator eine Liste von konkreten Werten. Der erste Parameter <code class="language-plaintext highlighter-rouge">size</code> wirkt sich je nach Generator unterschiedliche aus, z.B. kann er den Wertebereich bestimmen. Im Fall von <code class="language-plaintext highlighter-rouge">Gen.Choose()</code> hat er keinen Effekt. Der zweite Parameter <code class="language-plaintext highlighter-rouge">numberOfSamples</code> legt die Anzahl der Elemente in der generierten Liste fest.</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">Shrinker</code> dienen dazu, um Fehler einfacher zu finden. Sie versuchen für einen fehlgeschlagenen Testlauf den Wert zu finden, ab welchem die zu testende Property nicht mehr gilt. Der folgende Test schlägt für jeden Wert &gt;= 20 fehl, mittels Shrinking kann FsCheck den ersten fehlschlagenden Wert 20 ausmachen.</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>Ausgabe:</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> kombinieren <code class="language-plaintext highlighter-rouge">Generators</code> und <code class="language-plaintext highlighter-rouge">Shrinker</code> und dienen als Eingabe für Property-Tests. Die Klasse <code class="language-plaintext highlighter-rouge">Arb</code> definiert eine Reihe von Standardimplementierungen für unterschiedliche Datentypen. Auch <code class="language-plaintext highlighter-rouge">Generators</code> für komplexe Typen lassen sich über sie erzeugen.</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>Über Eigenschaften des <code class="language-plaintext highlighter-rouge">Property</code>-Attributs lässt sich die Verteilung der Werte steuern.</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>Die Werte in der Mitte des Wertebereichs (hier 0, weil positive wie negative Zahlen generiert werden) werden häufiger generiert als die Werte am Rand. Für welche konkreten Werte <code class="language-plaintext highlighter-rouge">StartSize</code> und <code class="language-plaintext highlighter-rouge">EndSize</code> stehen, hängt vom Generator ab.</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 führt standardmäßig 100 Testdurchläufe aus. Über die Eigenschaft <code class="language-plaintext highlighter-rouge">MaxTest</code> lässt sich die Anzahl der Durchläufe steuern. Schränkt man die validen Optionen zu sehr ein, kann es passieren, dass FsCheck keine Werte mehr generieren kann und eine Exception geworfen. Hierdurch wird vermieden, dass die Laufzeit für einzelnen Tests zu stark ansteigt.</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>Der Test schlägt mit einer Meldung fehl:</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>Nun hat man die Möglichkeit, die Anzahl der Testdurchläufe zu reduzieren. Allerdings sollte man sich auch überlegen, ob der Bedingung den gültigen Wertebereich nicht zu sehr einschränken muss. Im Beispiel ist ein <code class="language-plaintext highlighter-rouge">int</code>-Wert kein gut gewählter Datentyp.</p>

<h2 id="beispiel-addition">Beispiel Addition</h2>

<p>Um das bereits genannten Beispiel der <a href="https://de.wikipedia.org/wiki/Addition">Addition</a> zu vervollständigen, testen wir nun noch die weiteren Eigenschaften <code class="language-plaintext highlighter-rouge">Assoziativität</code>, <code class="language-plaintext highlighter-rouge">Identität</code> und <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>Bei dem bekannten <a href="https://codingdojo.org/kata/FizzBuzz/">FizzBuzz-Kata</a> geht es darum, für eine positive Zahl einen der folgenden Strings zu erzeugen:</p>

<ul>
  <li>“Fizz”, wenn die Zahl durch 3 teilbar ist</li>
  <li>“Buzz”, wenn die Zahl durch 5 teilbar ist</li>
  <li>“FizzBuzz”, wenn die Zahl durch 3 und 5 teilbar ist</li>
  <li>die Zahl selbst, sonst</li>
</ul>

<p>Ein Property-based Test zur Überprüfung einer möglichen Lösung könnte wie folgt aussehen:</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>Ein kleines Beispielprojekt mit dem hier gezeigten Quellcode findet sich auf <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="de" /><summary type="html"><![CDATA[Zum Handwerkszeug bei der Entwicklung von Software gehört es, (sinnvolle) automatisierte Tests zu schreiben. Mittels Unit-, Integration- und End-to-end-Tests wird die korrekte Funktionalität unseres Codes sichergestellt und wir erhalten die Sicherheit, dass wird beim Umsetzen neuer Anforderungen und beim Refactoring bestehenden Codes keine vorhandene Funktionalität kaputt machen. Michael Feathers definiert Legacy Software sogar als Code, der nicht getestet ist. legacy code is code without tests – Michael Feathers Neben den verbreiteten Ansätzen von Example-based testing oder Snapshot testing gibt es noch weitere Möglichkeiten, die uns helfen, die Korrektheit unseres Codes zu überprüfen. Einer dieser Ansätze ist Property-based testing. Property-based testing prüft nicht einzelne Werte auf Übereinstimmung, sondern zielt auf die Verifikation von Eigenschaften ab, die unsere Implementierung aufweisen müssen. So ist für die mathematische Addition die Kommutativität eine Eigenschaft, die wir überprüfen können, ohne uns konkrete Zahlen anzuschauen. Für die Addition zweier Zahlen a und b gilt immer: a + b = b + a]]></summary></entry><entry xml:lang="de"><title type="html">Devcontainer mit Visual Studio Code</title><link href="https://www.production-ready.de/2023/05/06/devcontainer-with-vscode.html" rel="alternate" type="text/html" title="Devcontainer mit 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</id><content type="html" xml:base="https://www.production-ready.de/2023/05/06/devcontainer-with-vscode.html"><![CDATA[<p>Für das Ausführen von Anwendungen hat sich, vor allem im Cloud-Umfeld, die Verwendung von Containern etabliert. Mit dem Aufkommen von Docker wurde es einfach, Software in einer definierten Umgebung auszuführen, welche sich einfach reproduzieren lässt. Sämtliche Runtime-Abhängigkeiten werden in ein Container-Image verpackt und können einfach auf einem anderen System ausgeführt werden. Die Diskrepanz zwischen unterschiedlichen Stages im Entwicklungsprozess (Development, Test, Produktion) wird reduziert, <code class="language-plaintext highlighter-rouge">works on my machine</code> zählt nicht mehr als Ausrede.</p>

<p>Um nun nicht nur die Laufzeit- sondern auch die Entwicklungs-Umgebung reproduzierbar und portierbar zu gestalten, bieten sich <a href="https://containers.dev">Development Containers</a> an.</p>

<!--more-->

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

<p>Um eine Entwicklungsumgebung herzustellen, genügt heutzutage zumeist nicht mehr nur ein Texteditor oder eine IDE. Es werden Runtimes, Compiler, CLIs und verschiedenste Tools benötigt, um Software zu entwickeln. Arbeitet man in mehreren Programmiersprachen oder Projekten, kann das Setup schnell einige Stunden in Anspruch nehmen. Ein weiteres Problem tritt auf, wenn man unterschiedliche Versionen einer Software benötigt. Node.js ist hier ein Beispiel, für das es mit dem <a href="https://github.com/jasongin/nvs">Node Version Switcher</a> mehr einen Workaround als eine gute Lösung gibt.</p>

<p>Zur Lösung dieser Probleme werden schon seit langer Zeit vorkonfigurierte virtuelle Maschinen eingesetzt, die entweder lokal ausgeführt werden oder zu denen man sich über ein Remote-Protokoll wie SSH oder RDP verbinden kann. Die Developer-Experience ist in beiden Fällen aber zumeist nicht optimal.</p>

<p>Nutzt man für die Entwicklung <a href="https://code.visualstudio.com">Visual Studio Code</a>, bietet sich mit der Nutzung von <a href="https://containers.dev">Devcontainern</a> hier eine leichtgewichtige Alternative an. Neben VS Code unterstützt bislang nur Visual Studio den Einsatz von Devcontainern, und auch <a href="https://devblogs.microsoft.com/cppblog/dev-containers-for-c-in-visual-studio/">nur für  C++ Projekte</a>. Lässt sich der eigene Workflow aber mit diesen Einschränkungen umsetzen, sind Devcontainer eine gute Option. Netter Nebeneffekt: Die Devcontainer verhalten sich auf unterschiedlichen Computern und Betriebssystemen gleich und sind somit für die Zusammenarbeit im Team gut geeignet. Die Konfiguration lässt sich neben dem Sourcecode in einem Git-Repository ablegen und versionieren.</p>

<h2 id="devcontainer-in-vs-code">Devcontainer in VS Code</h2>

<p>Voraussetzung für die Nutzung von Devcontainern in VS Code ist eine installierte Version von <a href="https://www.docker.com/products/docker-desktop/">Docker</a> und die Extension <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers">Dev Containers</a>, welche direkt von Microsoft kommt. Sie stellt über die <a href="https://code.visualstudio.com/api/ux-guidelines/command-palette">Command Palette</a> eine Reihe von Anweisungen bereit, um Devcontainer zu erstellen und zu verwalten.</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="VS Code Command Palette mit Befehlen für die Nutzung von Devcontainers" /></picture>

<h3 id="devcontainer-erstellen">Devcontainer erstellen</h3>

<p>Zum Erstellen eines Devcontainers genügt eine JSON-Datei <code class="language-plaintext highlighter-rouge">devcontainer.json</code> im Unterordner <code class="language-plaintext highlighter-rouge">.devcontainer</code>. Die <a href="https://containers.dev/implementors/json_reference/">Definition</a> eines Containers besteht im Grunde aus drei Bereichen: Image, Features und Konfiguration. Wählt man in VS-Code den Command <code class="language-plaintext highlighter-rouge">Dev Containers: Add Dev Container Configuration Files...</code>, wird man durch genau diese drei Bereiche geführt.</p>

<ul>
  <li>
    <p><a href="https://containers.dev/implementors/json_reference/#image-specific">Image</a>: Beschreibt das Docker-Image, welches als Ausgangspunkt für den Devcontainer dient. Hier kann ein <a href="https://containers.dev/templates">vorkonfiguriertes Devcontainer-Image</a> oder jedes beliebige andere Docker-Image nutzen werden. Alternativ kann man ein Dockerfile oder ein Docker Compose-File angeben, um ein eigenes Image zu erstellen.</p>
  </li>
  <li>
    <p><a href="https://containers.dev/implementors/features/">Features</a>: Pakete und Optionen, um Software zum Ausgangsimage hinzuzufügen und zu konfigurieren. So können Tools wie git, verschiedene CLIs oder Docker aber auch ganze Entwicklungs-Stacks für einzelne Programmiersprachen wie Go, .NET oder PHP als Feature einfach hinzugefügt werden. Es existieren <a href="https://containers.dev/features">einige vordefinierte Features</a>, alternativ baut man sich <a href="https://containers.dev/implementors/features/">eigene Features</a> aus Shell-Scripten zusammen.</p>
  </li>
  <li>
    <p><a href="https://containers.dev/implementors/spec/#other-options">Konfiguration</a>: Unter Konfiguration fallen z.B. Port-Forwardings, Lifecycle-Scripte oder Anpassungen für VS Code.</p>
  </li>
</ul>

<p>Die folgende Konfiguration nutzt das Baseimage für Note.js 20 mit TypeScript, installiert zusätzlich die AWS- sowie die GitHub-CLI und führt nach dem Erstellen des Containers den Befehl <code class="language-plaintext highlighter-rouge">yarn install</code> aus.</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>Ruft man über die VS-Code Command Palette den Befehl <code class="language-plaintext highlighter-rouge">Dev Containers: Reopen in Container</code> auf, wird der Devcontainer erstellt und VS Code verbindet sich mit dem Container. Das Erstellen kann einige Minuten dauern, da das Baseimage heruntergeladen und die Features installiert werden müssen. Der Root-Ordner des Projektes wird in den Container gemountet.</p>

<p>Nach dem Start zeigt uns VS Code den gemounteten Ordner im Explorer an und wir können mit der Entwicklung beginnen. Über das +-Icon im Terminal-Fenster kommen wir an eine Shell, die im Container läuft.</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 mit geöffnetem Devcontainer" /></picture>

<h3 id="ein-eigenes-devcontainer-image-erstellen">Ein eigenes Devcontainer-Image erstellen</h3>

<p>Genügt ein vorkonfiguriertes Image in Kombination mit den zur Verfügung stehenden Features nicht, kann man sich mit Hilfe eines Dockerfiles ein maßgeschneidertes Devcontainer-Image bauen.</p>

<p>Für ein einfaches Ruby-Setup sieht das <code class="language-plaintext highlighter-rouge">Dockerfile</code> wie folgt aus:</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>Innerhalb der <code class="language-plaintext highlighter-rouge">devcontainer.json</code>-Datei gibt man statt des Keywords <code class="language-plaintext highlighter-rouge">image</code> unterhalb von <code class="language-plaintext highlighter-rouge">build</code> das Dockerfile sowie den Build-Context an. Über den <code class="language-plaintext highlighter-rouge">Features</code>-Block werden <a href="https://github.com/devcontainers/features/tree/main/src/common-utils">ZSH als Default-Shell</a> und <a href="https://github.com/devcontainers/features/tree/main/src/git">git</a> installiert. Zusätzlich wird VS Code angewiesen, die Extensions für Ruby sowie Github Copilot zu installiert. Über den <code class="language-plaintext highlighter-rouge">postCreateCommand</code>-Eintrag stellt man sicher, dass die benötigten Gems installiert werden.</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>Möchte man zusätzliche Software installieren oder hat Änderungen an dem Container-Image vorgenommen, erstellt man über den Befehl <code class="language-plaintext highlighter-rouge">Dev Containers: Rebuild and Reopen in Container</code> einfach ein neues Image.</p>]]></content><author><name>David</name></author><category term="devcontainer" /><category term="vscode" /><category term="de" /><summary type="html"><![CDATA[Für das Ausführen von Anwendungen hat sich, vor allem im Cloud-Umfeld, die Verwendung von Containern etabliert. Mit dem Aufkommen von Docker wurde es einfach, Software in einer definierten Umgebung auszuführen, welche sich einfach reproduzieren lässt. Sämtliche Runtime-Abhängigkeiten werden in ein Container-Image verpackt und können einfach auf einem anderen System ausgeführt werden. Die Diskrepanz zwischen unterschiedlichen Stages im Entwicklungsprozess (Development, Test, Produktion) wird reduziert, works on my machine zählt nicht mehr als Ausrede. Um nun nicht nur die Laufzeit- sondern auch die Entwicklungs-Umgebung reproduzierbar und portierbar zu gestalten, bieten sich Development Containers an.]]></summary></entry><entry xml:lang="de"><title type="html">Statische Code-Analyse für Terraform</title><link href="https://www.production-ready.de/2023/04/29/analyse-terraform-files-with-tfsec.html" rel="alternate" type="text/html" title="Statische Code-Analyse für 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</id><content type="html" xml:base="https://www.production-ready.de/2023/04/29/analyse-terraform-files-with-tfsec.html"><![CDATA[<p>Statische Code-Analyse ist ein wichtiger Bestandteil der Qualitätssicherung von Software-Projekten. Durch sie können Fehler bereits frühzeitig in einer kurzen Feedback-Schleife erkannt und behoben werden. Der Quellcode wird anhand von definierten Regeln auf Fehler und potenzielle Schwachstellen hin überprüft und je nach verwendetem Tool wird direkt eine mögliche Lösung angeboten.</p>

<p>Mit dem aufkommen von <code class="language-plaintext highlighter-rouge">Infrastructure As Code</code>-Ansätzen werden nun auch zunehmend Infrastruktur-Beschreibungen in Form von Code- oder zumindest von strukturierten Textdateien hinterlegt. Damit lassen sich die Vorteile der statischen Code-Analyse auch auf Infrastruktur-Definitionen anwenden.</p>

<!--more-->

<h2 id="analyse-von-terraform-dateien">Analyse von Terraform-Dateien</h2>

<p>Für die Analyse von Terraform-Definitionsdateien existiert das CLI-Tool <a href="https://tfsec.dev/">tfsec</a> an. Es steht unter der MIT-Lizenz als ausführbare Datei für Windows, Linux und macOS oder als Docker-Image zum <a href="https://github.com/aquasecurity/tfsec/releases">Download</a> bereit.
<code class="language-plaintext highlighter-rouge">tfsec</code> kommt mit einem umfangreichen Set an <a href="https://aquasecurity.github.io/tfsec/v1.28.1/checks/aws/api-gateway/enable-access-logging/">Regeln</a> für die großen Hyperscaler wie AWS, Azure und Google Cloud daher. Für eine selbst-gehostete Kubernetes Umgebung oder einen OpenStack-Cluster sind es allerdings bereits deutlich weniger. Zwar lassen sich über <a href="https://aquasecurity.github.io/tfsec/v1.28.1/guides/configuration/custom-checks/">Custom Checks</a> eigene Regel definieren, am sinnvollsten lässt sich <code class="language-plaintext highlighter-rouge">tfsec</code> aber vermutlich in einer managed Umgebung einsetzen.</p>

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

<p>Im einfachsten Fall führt man <code class="language-plaintext highlighter-rouge">tfsec</code> im Ordner mit den Terraform-Dateien aus bzw. übergibt den Pfad als erstes Aufrufargument. <code class="language-plaintext highlighter-rouge">tfsec</code> durchsucht rekursiv alle Dateien mit der Endung <code class="language-plaintext highlighter-rouge">.tf</code> und <code class="language-plaintext highlighter-rouge">.tfvars</code> und gibt die Ergebnisse in der Konsole aus.</p>

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

<p>Werden keine Regelverletzungen entdeckt, werden ein paar Statistiken ausgegeben und <code class="language-plaintext highlighter-rouge">tfsec</code> gibt Entwarnung.</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>Werden Probleme festgestellt, folgen Details zur beanstandeten Datei und der genauen Position des Problems sowie Hinweise und Links zu weiteren Informationen. Der Parameter <code class="language-plaintext highlighter-rouge">--concise-output</code> reduziert die Ausgabe auf die wichtigsten Informationen.</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 dem Beispiel bemängelt <code class="language-plaintext highlighter-rouge">tfsec</code> eine veraltete TLS-Version für einen Azure-Storage-Account.</p>

<p>Möchte man einzelne Regeln nicht zur Anwendung bringen, schließt man sie über den <a href="https://aquasecurity.github.io/tfsec/v1.28.1/guides/usage/">Parameter</a> <code class="language-plaintext highlighter-rouge">--exclude [RULE-ID]</code> von der Prüfung aus. Auch lassen sich Beanstandungen einzelner Ressourcen oder Optionen innerhalb der Terraform-Definitionen <a href="https://aquasecurity.github.io/tfsec/v1.28.1/guides/configuration/ignores/">per Kommentar</a> ignorieren.</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>Ein nützliches Feature von <code class="language-plaintext highlighter-rouge">tfsec</code> ist, dass es das Ergebnis einer Analyse in unterschiedlichen Formaten ausgeben kann. Über den Parameter <code class="language-plaintext highlighter-rouge">--format</code> wählt man die gewünschten Formate, darunter auch Markdown, HTML und JUnit. Mit <code class="language-plaintext highlighter-rouge">--out</code> legt man den Order (dieser muss existieren) und das Dateiprefix der Ausgabedateien fest. Das erste Format wird zusätzlich auf der Konsole ausgegeben.</p>

<p>Der folgende Aufruf erzeugt in dem Ordner <code class="language-plaintext highlighter-rouge">./terraform/tfsec-results</code> die Dateien <code class="language-plaintext highlighter-rouge">results.lovely</code>, <code class="language-plaintext highlighter-rouge">results.junit</code> und <code class="language-plaintext highlighter-rouge">results.markdown</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>Die Ergebnisse lassen sich nun in einer CI-Pipeline weiterverarbeiten und der Build kann bei Regelverletzungen fehlschlagen.</p>

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

<p>Nutzt man Azure DevOps Pipelines, kann man die JUnit-Datei als Test-Results veröffentlichen. So kann man <code class="language-plaintext highlighter-rouge">tfsec</code> einfach in vorhandene Test- und Build-Workflows integrieren und Fehler direkt in der Pipeline visualisieren.</p>

<p>Ich zeige hier für eine einfachere Adaption an andere CI-Systeme, wie man <code class="language-plaintext highlighter-rouge">tfsec</code> als Shell-Command nutzt. Daneben existiert auch eine Pipeline Extension im <a href="https://marketplace.visualstudio.com/items?itemName=AquaSecurityOfficial.tfsec-official">Visual Studio Marketplace</a> und eine <a href="https://github.com/marketplace/actions/tfsec-action">GitHub Action</a>.</p>

<p>Zuerst lädt man das passende Binary für sein System herunter, setzt ggf. das Execute-Bit und legt den Ausgabe-Ordner an. Danach führt man <code class="language-plaintext highlighter-rouge">tfsec</code> aus. Möchte man das Ergebnis im Markdown-Format als Ergebnis des Pipeline-Laufs in der Weboberfläche sehen, kann man es über ein <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands">Logging-Command</a> <code class="language-plaintext highlighter-rouge">echo "##vso[task.uploadsummary]...</code> hochladen.</p>

<p>In einem zweiten Task veröffentlicht man dann die JUnit-Datei als Test-Result.</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>Die <code class="language-plaintext highlighter-rouge">tfsec</code>-Results gehen mit in die Teststatistik ein.</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>Im <code class="language-plaintext highlighter-rouge">Extensions</code>-Tab wird das Ergebnis der Markdown-Datei angezeigt.</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>Damit hat man eine automatische und kontinuierliche Überprüfung der Terraform-Dateien in seine CI-Pipeline integriert.</p>]]></content><author><name>David</name></author><category term="terraform" /><category term="security" /><category term="tfsec" /><category term="de" /><summary type="html"><![CDATA[Statische Code-Analyse ist ein wichtiger Bestandteil der Qualitätssicherung von Software-Projekten. Durch sie können Fehler bereits frühzeitig in einer kurzen Feedback-Schleife erkannt und behoben werden. Der Quellcode wird anhand von definierten Regeln auf Fehler und potenzielle Schwachstellen hin überprüft und je nach verwendetem Tool wird direkt eine mögliche Lösung angeboten. Mit dem aufkommen von Infrastructure As Code-Ansätzen werden nun auch zunehmend Infrastruktur-Beschreibungen in Form von Code- oder zumindest von strukturierten Textdateien hinterlegt. Damit lassen sich die Vorteile der statischen Code-Analyse auch auf Infrastruktur-Definitionen anwenden.]]></summary></entry><entry xml:lang="de"><title type="html">AKS Kubernetes Update</title><link href="https://www.production-ready.de/2023/04/20/aks-kubernetes-update.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</id><content type="html" xml:base="https://www.production-ready.de/2023/04/20/aks-kubernetes-update.html"><![CDATA[<p><a href="https://kubernetes.io">Kubernetes</a> ist der de-facto Standard für Container-Orchestrierung. Möchte man den Betrieb nicht selber bewerkstelligen, greift man zu einer Managed Lösung wie Microsofts <a href="https://azure.microsoft.com/de-de/services/kubernetes-service/">Azure Kubernetes Service</a> (AKS).
Damit werden einem weite Teile des operativen Betriebs abgenommen. Auch die Aktualisierung der Kubernetes-Version kann komplett von Azure übernommen werden. Wie man auto-upgrade konfiguriert, erklärt Microsoft in <a href="https://learn.microsoft.com/en-us/azure/aks/auto-upgrade-cluster">diesem Artikel</a>.
Möchte man die Version und den Zeitpunkt eines Upgrades seines Cluster aber selber festlegen, kann man dies über die <a href="https://learn.microsoft.com/en-us/cli/azure/">Azure CLI</a> oder noch besser via <a href="https://www.terraform.io">Terraform</a> erledigen. Voraussetzung ist natürlich, dass man seine Kubernetes-Infrastruktur mittels Terraform provisioniert.</p>

<!--more-->

<h2 id="azure-kubernetes-versionen">Azure Kubernetes Versionen</h2>

<p>Die unterstützen Kubernetes-Versionen sowie deren Support-Zeiträume listet Microsoft in der <a href="https://docs.microsoft.com/en-us/azure/aks/supported-kubernetes-versions">Dokumentation</a> auf.</p>

<p>Zu beachten ist, dass ein Update immer nur innerhalb einer Minor-Version oder auf die nächste Minor-Version möglich ist. Möchte man etwa von Version 1.24 auf 1.26 updaten, muss man dies über Version 1.25 in zwei Durchgängen erledigen.</p>

<p>Die verfügbaren Versionen hängen von der Region ab, in der der Cluster angesiedelt ist.  Über die Azure CLI lassen sich die verfügbaren Versionen für eine Region abfragen.</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>Die Ausgabe liefert eine Liste der verfügbaren Versionen und die möglichen 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>Auch lassen sich die verfügbaren Upgrades für einen speziellen Cluster abfragen.</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>Das Ergebnis zeigt genau die verfügbaren Upgrades an.</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>Das Konfigurieren eines AKS Clusters mittels Terraform ist schnell erledigt. Über den <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs">Azure Provider</a> legt man die entsprechende Resource <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster">azurerm_kubernetes_cluster</a> an.</p>

<p>Hier konfiguriert man über <code class="language-plaintext highlighter-rouge">kubernetes_version</code> die Version des Control Plane und über <code class="language-plaintext highlighter-rouge">default_node_pool.orchestrator_version</code> die Version der Nodes.</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>Ein <code class="language-plaintext highlighter-rouge">terraform apply</code> und ein paar Minuten später steht ein AKS Cluster in der gewünschten Version zur Verfügung.</p>

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

<p>Um seinen AKS Cluster nun auf eine höhere Version zu heben, ändern man die Versionsnummern in seinem
Terraform-File, am besten sowohl für das Control Plane als auch die Node Pools. Ein <code class="language-plaintext highlighter-rouge">terraform apply</code>
stößt den Upgrade-Vorgang an. Nun werden nacheinander die einzelnen Knoten aus dem Cluster entfernt
und durch aktualisierte Varianten ersetzt. Die laufenden Pods werden dabei natürlich auf die
verbleibenden Knoten verteilt und bleiben weiterhin erreichbar.
Der Upgrade-Vorgang kann abhängig von der Anzahl der Knoten einige Minuten bis Stunden dauern.</p>

<p>Solltet Ihr für die Infrastruktur-Anpassung eine CI/CD-Pipeline nutzen, achtet darauf, dass der Vorgang nicht in einen Timeout läuft.</p>

<p>Über die Azure CLI und im Azure Portal lässt sich der Prozess verfolgen.</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>Der <code class="language-plaintext highlighter-rouge">ProvisioningState</code> wechselt auf <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>Nach Abschluss des Upgrades wechselt der <code class="language-plaintext highlighter-rouge">ProvisioningState</code> wieder auf <code class="language-plaintext highlighter-rouge">Succeeded</code> und alle Knoten laufen auf der neuen Kubernetes-Version.</p>]]></content><author><name>David</name></author><category term="aks" /><category term="kubernetes" /><category term="de" /><summary type="html"><![CDATA[Kubernetes ist der de-facto Standard für Container-Orchestrierung. Möchte man den Betrieb nicht selber bewerkstelligen, greift man zu einer Managed Lösung wie Microsofts Azure Kubernetes Service (AKS). Damit werden einem weite Teile des operativen Betriebs abgenommen. Auch die Aktualisierung der Kubernetes-Version kann komplett von Azure übernommen werden. Wie man auto-upgrade konfiguriert, erklärt Microsoft in diesem Artikel. Möchte man die Version und den Zeitpunkt eines Upgrades seines Cluster aber selber festlegen, kann man dies über die Azure CLI oder noch besser via Terraform erledigen. Voraussetzung ist natürlich, dass man seine Kubernetes-Infrastruktur mittels Terraform provisioniert.]]></summary></entry><entry xml:lang="de"><title type="html">Mailgraph Docker Container</title><link href="https://www.production-ready.de/2023/04/15/mailgraph-docker-container.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</id><content type="html" xml:base="https://www.production-ready.de/2023/04/15/mailgraph-docker-container.html"><![CDATA[<p><a href="http://www.postfix.org">Postfix</a> gehört vermutlich immer noch zu den am weitesten verbreiteten freien SMTP-Servern. Wer einen Emailserver selber hostet und die Menge an ein- und ausgehenden Emails im Blick haben möchte, stößt schnell auf das Tool <a href="https://mailgraph.schweikert.ch">Mailgraph</a> von David Schweikert. Mailgraph besteht aus einem Perl-Skript, das die Logfiles von Postfix auswertet und eine RRDtool-Datenbank befüllt und einem CGI-Skript, welches die Daten in Form von Grafiken übersichtlich im Browser darstellt.</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>Neben der Anzahl der gesendeten und empfangenen Emails gibt es auch eine Grafik zu Spam und Viren.</p>

<p>Dank <a href="https://www.kernel-error.de/2014/04/22/mailgraph-graphen-um-spf-dmarc-und-dkim-erweitern/">eines Patches von Sebastian van de Meer</a> informiert Mailgraph auch darüber, ob die einliefernden Server SPF, DMARC oder DKIM unterstützen. Hierfür ist natürlich erforderlich, dass diese Mechanismen auf eurer Postfix-Instanz konfiguriert sind.</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>Um Mailgraph und dazu einen entsprechenden Webserver nicht direkt auf seinem Emailserver installieren zu müssen, kann man es natürlich in einem Docker Container betreiben.</p>

<p>Dazu habe ich ein Dockerfile erstellt, welches alle Abhängigkeiten enthält und sofort eingesetzt werden kann.
Als Volumes gibt man den Pfad zum Mail-Logfile sowie einen Ordner zum Speichern der RRD-Dateien an. Alternativ kann man hierzu natürlich auch ein Docker Volume verwenden.</p>

<p>Über das <code class="language-plaintext highlighter-rouge">-v</code> Flag hängt man Host-Pfade oder Volumes in den Container ein: <code class="language-plaintext highlighter-rouge">-v [Host-Path]:[Container-Path]</code></p>

<p>Die Log-Datei wird im Container unter dem Pfad <code class="language-plaintext highlighter-rouge">/var/log/mail/mail.log</code> erwartet, die RRD-Dateien unter <code class="language-plaintext highlighter-rouge">/var/www/mailgraph/rrd/</code>.</p>

<p>Das Image liefert standardmäßig auf Port 80 und Pfad /mailgraph die Mailgraph-Webseite aus.</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>Ruft man nun <a href="http://localhost:80/mailgraph/">http://localhost:80/mailgraph/</a> im Browser auf, bekommt man schon die ersten Grafiken angezeigt.</p>

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

<p>Nutzt man zur Orchestrierung Docker Compose, sieht eine entsprechende Konfiguration etwa so aus:</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-mit-traefik">Reverse Proxy mit Traefik</h3>

<p>Möchte man seine Mailgraph-Webseite nicht öffentlich zugänglich sehen, kann man mittels Reverse Proxy eine Authentifizierung einrichten.
Eine einfache Möglichkeit bietet <a href="https://traefik.io/traefik/">Traefik</a>, ein Reverse Proxy, der sich sehr schön in Docker Compose integrieren lässt. Hat man Traefik auf seinem System eingerichtet, ist die Konfiguration für Mailgraph über Labels sehr einfach.</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-erweiterung">Dovecot-Erweiterung</h2>

<p>Da ich als IMAP-Server <a href="https://www.dovecot.org">Dovecot</a> einsetze, habe ich Mailgraph um die Möglichkeit erweitert, die Anzahl der erfolgreichen und fehlgeschlagenen IMAP-Logins anzuzeigen.</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="sourcecode-und-docker-image">Sourcecode und Docker Image</h2>

<p>Den Sourcecode zu dem Docker Image findet ihr auf <a href="https://github.com/davull/MailgraphContainer">GitHub</a>, das fertige Image im <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="de" /><summary type="html"><![CDATA[Postfix gehört vermutlich immer noch zu den am weitesten verbreiteten freien SMTP-Servern. Wer einen Emailserver selber hostet und die Menge an ein- und ausgehenden Emails im Blick haben möchte, stößt schnell auf das Tool Mailgraph von David Schweikert. Mailgraph besteht aus einem Perl-Skript, das die Logfiles von Postfix auswertet und eine RRDtool-Datenbank befüllt und einem CGI-Skript, welches die Daten in Form von Grafiken übersichtlich im Browser darstellt.]]></summary></entry></feed>