<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.finnpedersenfrance.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.finnpedersenfrance.com/" rel="alternate" type="text/html" /><updated>2026-01-31T18:54:20+01:00</updated><id>https://www.finnpedersenfrance.com/feed.xml</id><title type="html">Finn Pedersen’s Blog</title><subtitle>A blog about technology and development for Microsoft Dynamics 365 Business Central.</subtitle><entry><title type="html">The urgent need to modernize Business Central development</title><link href="https://www.finnpedersenfrance.com/programming/2025/11/30/urgent-need-to-modernize-bc-development.html" rel="alternate" type="text/html" title="The urgent need to modernize Business Central development" /><published>2025-11-30T06:00:00+01:00</published><updated>2026-01-31T06:00:00+01:00</updated><id>https://www.finnpedersenfrance.com/programming/2025/11/30/urgent-need-to-modernize-bc-development</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2025/11/30/urgent-need-to-modernize-bc-development.html"><![CDATA[<h1 id="the-urgent-need-to-modernize-business-central-development">The urgent need to modernize Business Central development</h1>

<p><img src="/assets/images-01/DinosaurAIandBCnew.png" alt="Dinosaur AI and Business Central" /></p>

<h2 id="we-are-at-an-inflection-point">We are at an inflection point</h2>

<p>I am just back from <a href="https://www.directionsforpartners.com/emea2025">Directions EMEA 2025</a> where I have spoken with a number of people who show the way forward.</p>

<p>Maybe the inflection point was there before, but AI has definitely made it more obvious.</p>

<blockquote>
  <p><strong>Artificial Intelligence</strong> is the <em>asteroid</em> hurtling down and hitting our ecosystem.</p>
</blockquote>

<h2 id="business-central-is-fundamentally-different-from-dynamics-nav">Business Central is fundamentally different from Dynamics NAV</h2>

<blockquote>
  <p>It is an error not to make enough sense of urgency.</p>
</blockquote>

<p>This is not just about upgrading from Dynamics NAV to BC. <strong>Business Central development is fundamentally different</strong> from Dynamics NAV development, and this difference is accelerating.</p>

<h3 id="the-upgrade-cycle-has-changed-forever">The upgrade cycle has changed forever</h3>

<p>In Dynamics NAV, you could upgrade every five or ten years. You had time. You could write code that worked, even if it didn’t follow best practices. The upgrade cycle was slow enough that technical debt could accumulate without immediate consequences.</p>

<p><strong>Business Central changes the rules completely:</strong></p>

<ul>
  <li><strong>Monthly updates:</strong> Your code is updated every month, whether you like it or not</li>
  <li><strong>Breaking changes:</strong> With two major releases per year, breaking changes arrive on a regular basis</li>
</ul>

<h3 id="the-problem-isnt-newits-just-more-urgent">The problem isn’t new—it’s just more urgent</h3>

<p>The truth is, Dynamics NAV had programming guidelines. Microsoft provided them. But many developers created their own patterns, and it worked because upgrades were rare.</p>

<p><strong>That approach is much harder to sustain now.</strong></p>

<p>When you’re facing monthly updates and breaking changes, code that doesn’t follow modern practices becomes a challenge. It can break, cost more to fix, and frustrate customers. And it may put you at a competitive disadvantage.</p>

<h2 id="the-customer-perspective">The customer perspective</h2>

<p>Customers are often unaware of the technical debt hidden in their codebase. They don’t know to ask for:</p>
<ul>
  <li>Code that follows Microsoft’s guidelines</li>
  <li>Test coverage</li>
  <li>Modern architecture patterns</li>
  <li>Regular code reviews</li>
</ul>

<p>They trust that “senior developers with a lot of experience” are delivering quality code. But experience without modern practices can sometimes lead to legacy code.</p>

<h2 id="the-ai-acceleration">The AI acceleration</h2>

<p>The world is moving fast. AI tools are making it possible to build better solutions faster. Competitors who adopt modern practices and leverage AI will outcompete those who don’t.</p>

<p>If you’re still writing code the Dynamics NAV way, you may find yourself falling further behind every month.</p>

<h2 id="what-modern-business-central-development-looks-like">What modern Business Central development looks like</h2>

<p>Modern BC development requires:</p>

<ul>
  <li><strong>Investment in training:</strong> Understanding BC’s architecture, not just porting Dynamics NAV code</li>
  <li><strong>Test-driven development:</strong> Writing tests that run in milliseconds (see my articles on <a href="/programming/2025/06/26/environment-interface-part-1.html">Environment Interfaces</a> and <a href="/programming/2025/06/26/temporary-tables-in-tests.html">Temporary Tables in Tests</a>)</li>
  <li><strong>Code review:</strong> Peer review of all changes (see my article on <a href="/programming/2024/01/11/how-to-make-a-code-review.html">How to make a code review</a>)</li>
  <li><strong>Modern tools:</strong> CodeCop, linters, proper settings.json configuration (see my article on <a href="/programming/2024/01/05/BC-Advanced-Rulesets.html">Advanced Rulesets</a>)</li>
  <li><strong>Clean architecture:</strong> Namespaces, interfaces, proper organization (see my <a href="/programming/2023/10/10/BC-Rulesets.html">Clean Code Initiative</a>)</li>
  <li><strong>Engineering management:</strong> Teams that understand modern software development</li>
</ul>

<h2 id="the-cost-of-delay">The cost of delay</h2>

<blockquote>
  <p>We’ll upgrade to BC during the next two years.</p>
</blockquote>

<p>But in two years, everything will have moved so far from where we are now. This year alone has seen rapid changes. Waiting two years means you’ll be even further behind, with even more technical debt to address.</p>

<p><strong>The time to act is now.</strong></p>

<h2 id="how-to-identify-legacy-code-patterns">How to identify legacy code patterns</h2>

<p>After three years as a freelance BC developer, I’ve seen patterns that suggest code may not be ready for BC’s update cycle:</p>

<ul>
  <li>Objects organized by Object Type instead of functionality</li>
  <li>Global variables everywhere</li>
  <li>No test apps</li>
  <li>Minimal or missing settings.json configuration</li>
  <li>No CodeCop or other linters</li>
  <li>No use of interfaces or namespaces</li>
  <li>Inconsistent naming conventions</li>
  <li>No automatic code cleanup</li>
  <li>No peer code review process</li>
</ul>

<p>If this describes your codebase, it is urgent time to get started fixing it.</p>

<h2 id="the-path-forward">The path forward</h2>

<p>I have changed my job title to <a href="https://www.linkedin.com/in/finnpedersen/">Modern Business Central Developer</a>.</p>

<blockquote>
  <p>Prefer to rewrite code using modern patterns rather than patching old code.</p>
</blockquote>

<p>If you have adopted a modern development mindset, then show it by changing your job title to <strong>Modern Business Central Developer</strong>.</p>

<p>And, please, by all means share this article with your network and ask your network to join the movement.</p>

<h2 id="we-need-to-outcompete-legacy-practices">We need to outcompete legacy practices</h2>

<p>The best way to make legacy practices obsolete is to outcompete them.</p>

<blockquote>
  <p>By using the latest technologies and a modern mindset, we will build <strong>better solutions</strong> faster.</p>
</blockquote>

<p>Solutions that are:</p>
<ul>
  <li>Easy to maintain</li>
  <li>Prepared for evolution</li>
  <li>Easy to extend</li>
  <li>Easy to support</li>
  <li>Ready for BC’s update cycle</li>
</ul>

<p>Reach out if you need help with the transition.</p>

<p>Finn Pedersen</p>

<p>Modern BC Developer</p>

<hr />

<blockquote>
  <p>PS: I realize that some people might be offended by this post. That was not the intention. The purpose of this post is not to offend anyone, but to create enough sense of urgency. The technical reality is that BC’s update cycle requires modern practices. The longer we delay, the more expensive and difficult the transition becomes. I want to help, not criticize.</p>
</blockquote>]]></content><author><name>Finn</name></author><category term="Programming" /><summary type="html"><![CDATA[The urgent need to modernize Business Central development]]></summary></entry><entry><title type="html">Vibe Coding with Lovable.dev (No Code)</title><link href="https://www.finnpedersenfrance.com/programming/2025/10/15/vibe-coding-with-lovable-dev.html" rel="alternate" type="text/html" title="Vibe Coding with Lovable.dev (No Code)" /><published>2025-10-15T07:00:00+02:00</published><updated>2025-10-15T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/programming/2025/10/15/vibe-coding-with-lovable-dev</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2025/10/15/vibe-coding-with-lovable-dev.html"><![CDATA[<h1 id="vibe-coding-with-lovabledev">Vibe Coding with Lovable.dev</h1>

<h2 id="an-app-says-more-than-a-1000-words">An app says more than a 1000 words</h2>

<p><strong>If you are not a developer, you have to try Lovable.dev!</strong></p>

<p>The reason is that it will expand your view on what is possible.</p>

<p>This is what you will learn from making apps with <code class="language-plaintext highlighter-rouge">Lovable.dev</code>.</p>

<ul>
  <li>How to express an idea</li>
  <li>How to reformulate and clarify your thoughts</li>
  <li>How to communicate your idea to others</li>
  <li>Understand how your first hypotheses had flaws and can be improved</li>
</ul>

<p>To help you get started, there are some examples below.</p>

<h2 id="lovabledev">Lovable.dev</h2>

<p>Use my referral link to get free credits when you sign up.</p>

<p><a href="https://lovable.dev/invite/8e75e0e5-5de0-4a7f-860d-3f192c0714f1">https://lovable.dev/invite/8e75e0e5-5de0-4a7f-860d-3f192c0714f1</a></p>

<p><em>And don’t forget to publish your app.</em>
It will give me 10 credits to build more fun games.</p>

<h2 id="team-building">Team Building</h2>

<p><strong>I recommend vibe coding for team building.</strong></p>

<p>People who don’t know how to code often have fewer ideas for what is possible to make. No longer.</p>

<p>After a few attempts you will rapidly get the hang of it.</p>

<h2 id="the-hottest-new-programming-language-is-english">The hottest new programming language is English</h2>

<p>I will be giving a talk on Vibe Coding at the <strong>Directions EMEA 2025</strong> conference, 4-6 November 2025 in Poznan, Poland.</p>

<p>More information about the conference at <a href="https://www.directionsforpartners.com/emea2025">www.directionsforpartners.com/emea2025</a>
and my <a href="https://sessionize.com/s/finn-pedersen/vibe-coding.-the-hottest-new-programming-language-/142123">session</a>.</p>

<h2 id="snake-game">Snake Game</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Build a Snake game for my phone as a web app
</code></pre></div></div>

<p><em>Extra instructions</em></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The game must not end. When the snake reaches the edge it should just continue on the other side. Start with a length of 3. Add buttons on the right hand side. The swiping up and right did not work as expected.

Make the head of the snake blue.
</code></pre></div></div>

<p><a href="https://mobile-snake-play.lovable.app">mobile-snake-play.lovable.app</a></p>

<p><img src="/assets/images-01/LovableSnakeGame.png" alt="Snake Game" /></p>

<h2 id="converting-french-to-braille">Converting French to Braille</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Make a small web app that converts a text as I type it to braille. I want the braille to be above the text input field so I can see it as I type on my phone. The braille should not be abbreviated. I expect to write in French. ...
</code></pre></div></div>

<p><a href="https://braille-ahead-typer.lovable.app">braille-ahead-typer.lovable.app</a></p>

<p><img src="/assets/images-01/LovableConvertBraille.png" alt="Braille Converter" /></p>

<h2 id="2048-game">2048 Game</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Make a 2048 game that runs on my phone.
</code></pre></div></div>

<p><em>Extra instruction</em></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Swiping works left, right, and up. Swipe down refreshes the page.
</code></pre></div></div>

<p><a href="https://twenty-forty-eight-slide.lovable.app/">twenty-forty-eight-slide.lovable.app</a></p>

<p><img src="/assets/images-01/Lovable2048game.png" alt="2048 Game" /></p>]]></content><author><name>Finn</name></author><category term="programming" /><summary type="html"><![CDATA[Vibe Coding with Lovable.dev]]></summary></entry><entry><title type="html">Vibe Coding with Cursor (Coding with AI)</title><link href="https://www.finnpedersenfrance.com/programming/2025/07/24/vibe-coding-with-cursor.html" rel="alternate" type="text/html" title="Vibe Coding with Cursor (Coding with AI)" /><published>2025-07-24T09:00:00+02:00</published><updated>2025-10-15T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/programming/2025/07/24/vibe-coding-with-cursor</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2025/07/24/vibe-coding-with-cursor.html"><![CDATA[<h1 id="vibe-coding-with-cursor">Vibe Coding with Cursor</h1>

<p>Whether we are just coding with AI or entirely Vibe Coding, we face the same challenges.</p>

<p>The term <strong>Vibe Coding</strong> went viral in February 2025 and now everyone is talking about it or trying it out.</p>

<p>I have submitted a talk, <strong>Vibe coding. The hottest new programming language is English</strong> 
on this subject, to <a href="https://www.directionsforpartners.com/emea2025">Directions EMEA 2025</a></p>

<p><strong>There is actually a lot we can learn from vibe coding:</strong></p>

<ul>
  <li>How do we communicate our needs to the AI?</li>
  <li>Is that similar to how we communicate with a developer?</li>
  <li>What is easy and what is difficult in programming?</li>
  <li>Are some programming languages more suited for development with AI than others?</li>
  <li>Do you need to be a developer to do Vibe Coding?</li>
  <li>How much coding do you actually need to know to succeed with vibe coding?</li>
  <li>Is vibe coding just for fun or can I use it in my work?</li>
</ul>

<h2 id="what-is-vibe-coding">What is Vibe Coding?</h2>

<p>Computer scientist Andrej Karpathy, a co-founder of OpenAI and former AI leader at Tesla, introduced the term vibe coding in February 2025. The concept refers to a coding approach that relies on LLMs, allowing programmers to generate working code by providing natural language descriptions rather than manually writing it.</p>

<p>Source <a href="https://en.wikipedia.org/wiki/Vibe_coding">Wikipedia Vibe Coding</a></p>

<h2 id="the-snake-game">The snake game</h2>

<p>Here is an idea for a prompt:</p>

<blockquote>
  <p>Make the snake game in Elm as we knew it from the old nokia phones.</p>
</blockquote>

<ul>
  <li>Download and install Cursor</li>
  <li>Setup your development environment</li>
  <li>Write your prompt</li>
</ul>

<h3 id="40-minutes">40 minutes</h3>

<p>Within 40 minutes, I had a running game on my computer. I was super optimistic. Take into account that I was also playing, I mean testing, the game during that time.</p>

<p><img src="/assets/images-01/ElmSnakeGame.png" alt="Snake Game" /></p>

<p>Then …</p>

<h3 id="several-days-later">Several days later</h3>

<p>The app was only running on my laptop and I wanted it to work on my phone.
This is where the troubles started.
I refused to touch the code. I had to prompt my way to the result.
However, at one point, I had to study the code and specifically ask it to change some of the CSS.</p>

<p>I also had to rollback the code completely a couple of times.</p>

<p><img src="/assets/images-01/ElmSnakeGameMobileFirst.png" alt="Snake Game" /></p>

<p><a href="https://finnpedersenkazes.github.io/elm-snake/">Try it</a></p>

<blockquote>
  <p><strong>Warning</strong>: It is quite addictive.</p>
</blockquote>

<h3 id="project-on-github">Project on GitHub</h3>

<p><a href="https://github.com/finnpedersenkazes/elm-snake">Elm Snake Game on GitHub</a></p>

<p>The main branch is the first desktop version and the mobile-first branch is the mobile-first version. 
Feel free to star the project and clone it.</p>

<p>You can follow my dialog with the AI in the <a href="https://github.com/finnpedersenkazes/elm-snake/blob/master/cursor_create_a_hello_world_app_in_elm.md">cursor_create_a_hello_world_app_in_elm.md</a> file.</p>

<p>It is the entire dialog to get to the first version. You don’t want to see the second. It is too long.</p>

<p>The <code class="language-plaintext highlighter-rouge">readme.md</code> is also almost entirely generated with a few prompts stating the sections.</p>

<h2 id="cursorcom">Cursor.com</h2>

<p>I used <a href="https://cursor.com/">Cursor.com</a>, the AI Code Editor, to realize the project.</p>

<p>I did not write a single line of code, so I did not care about setting anything up.</p>

<h2 id="programming-languages">Programming Languages</h2>

<p>Are all programming languages born equal for AI peer programming and Vibe Coding?
They are not. Actually, this will challenge our common worldview on programming languages.</p>

<p>What if another language will challenge <code class="language-plaintext highlighter-rouge">C#</code> or <code class="language-plaintext highlighter-rouge">TypeScript</code> simply because the AI works better with it?
Could a small and rather unknown language like <code class="language-plaintext highlighter-rouge">Elm</code> suddenly spring into popularity because it works much better with AI?</p>

<p>Some really interesting articles have started to appear.</p>

<ul>
  <li><a href="https://interjectedfuture.com/elm-as-target-language-for-vibe-coding/">Elm as target language for vibe coding by Wil Chung</a></li>
  <li><a href="https://medium.com/@jonathan.demontalembert/the-ai-symbiote-why-deterministic-languages-are-the-future-of-coding-f4a5ce4916e7">The AI Symbiote: Why Deterministic Languages Are the Future of Coding by Le Jon</a></li>
</ul>

<p>This is the reason why I chose Elm for my first vibe programming project.</p>

<h3 id="elm">Elm</h3>

<p>A delightful language for reliable web applications.</p>

<p><a href="https://elm-lang.org/">elm-lang.org</a></p>

<p>Elm is a pure functional language for building interactive web applications.
The model-update-view architecture has all state management handled in a single model. 
The strong static type system guarantees <strong>no runtime errors</strong>. 
And I just love the helpful compiler messages and easy-to-read code.</p>

<blockquote>
  <p>If it compiles, it works.</p>
</blockquote>

<p>As it turns out, the AI in Cursor also likes the helpful error messages from Elm.</p>

<h4 id="try-elm">Try Elm</h4>

<p>There are a couple of ways to try out Elm. Here are a few.</p>

<ul>
  <li><a href="https://elm-editor.com/">elm-editor.com</a></li>
  <li><a href="https://ellie-app.com/new">ellie-app.com</a></li>
</ul>

<h3 id="other-languages-and-al">Other languages and AL</h3>

<p>Since the amount of open source code for a given language is also important for training the LLM, 
the most popular languages like JavaScript, TypeScript, and Python have an advantage.</p>

<p>A purpose-specific language might also have some advantages. AL is purpose-specific.</p>

<h4 id="al-and-ai">AL and AI</h4>

<p>Age and how much a language changes plays a role. AL is old and has evolved a lot. 
There is no tradition for open source projects on GitHub, best practices were not followed, and 
the language has evolved a lot. For example, we no longer write <code class="language-plaintext highlighter-rouge">Find('-')</code> we write <code class="language-plaintext highlighter-rouge">FindFirst()</code>. 
So the LLM has to discard what was good in the past and now considered bad.</p>

<p>Where <code class="language-plaintext highlighter-rouge">Elm</code> has been stable for the past five years.</p>

<h2 id="ai-assistance-helping-the-ai">AI assistance helping the AI</h2>

<p>When I had trouble with the Mobile-First UI of the Game, I found that the Inspect function in
Chrome has an AI assistance. I asked how to fix the issue and passed the advice
directly to the AI developer in Cursor. Nice!</p>

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

<p>Are you in a team? 
Do you work with developers, project managers, product owners, users, or managers?</p>

<p><strong>Here is what you should do:</strong> Make a team-building event and vibe code something together.</p>

<p>You will learn much more than you could possibly imagine.</p>

<blockquote>
  <p><strong>Warning</strong>: You will have so much fun that you will have a hard time getting back to work.</p>
</blockquote>

<p><strong>Have fun and learn</strong></p>]]></content><author><name>Finn</name></author><category term="Programming" /><summary type="html"><![CDATA[Vibe Coding with Cursor]]></summary></entry><entry><title type="html">Temporary Tables in Tests (Business Central)</title><link href="https://www.finnpedersenfrance.com/programming/2025/06/26/temporary-tables-in-tests.html" rel="alternate" type="text/html" title="Temporary Tables in Tests (Business Central)" /><published>2025-06-26T10:00:00+02:00</published><updated>2025-09-05T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/programming/2025/06/26/temporary-tables-in-tests</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2025/06/26/temporary-tables-in-tests.html"><![CDATA[<h1 id="temporary-tables-in-tests">Temporary Tables in Tests</h1>

<p>The theme for this series is <em>Super fast tests covering 100% of your code</em>.</p>

<p>This 2025 series of articles is a continuation of the 2024 series called the Clean Code Initiative.</p>

<p>Clean Code is a condition for writing good tests, but it does not guarantee code coverage by tests.</p>

<h2 id="scenario">Scenario</h2>

<p>The app we are building communicates with an external API.</p>

<p>We face several key requirements:</p>

<ul>
  <li>The app can only be in production mode when running in a production environment. If running in a sandbox, it must operate in test mode.</li>
  <li>Our tests must cover all code paths, including those only reachable in production mode.</li>
  <li>Our tests cannot call the actual API, but we still need to verify both the requests we send and how we decode the API responses.</li>
  <li>Tests must be extremely fast, with runtimes measured in milliseconds.</li>
  <li>Writing tests should be quick and cost-effective.</li>
  <li>We want to test only our own code, assuming that dependencies and external systems work as expected. We assume everyone else knows what they are doing</li>
</ul>

<blockquote>
  <p><strong>Temporary Tables</strong> will satisfy the need for speed and isolation from the database.</p>
</blockquote>

<h2 id="avoid-touching-the-database-in-tests">Avoid Touching the Database in Tests</h2>

<p>We have looked at Environment Interfaces and how they save us time and improve speed, but there is one more thing to avoid during tests: touching the database. Here, we have to think differently—we are not going to develop an interface toward the database, and we don’t have to.</p>

<p>If we want to run hundreds of tests in seconds, we have to change the way we write functions. While the Clean Code Initiative told you to stick to the standard patterns, we are now going to challenge this a little bit in some cases.</p>

<h2 id="pure-functions">Pure Functions</h2>

<p>First of all, we want to have <strong>pure functions</strong>—that is, functions with no side effects. Touching the database is a side effect.</p>

<p>This has important consequences for where we place database operations like <code class="language-plaintext highlighter-rouge">Record.Get()</code>, <code class="language-plaintext highlighter-rouge">Record.Modify()</code>, <code class="language-plaintext highlighter-rouge">Record.Insert()</code>, and <code class="language-plaintext highlighter-rouge">Record.FindSet()</code> in our code. In this project, they are now all at the top level.</p>

<h2 id="passing-records-by-reference">Passing Records by Reference</h2>

<p>To achieve fast, isolated tests, we use <strong>temporary tables</strong>, which run entirely in memory. When a function operates on a temporary record, you must pass that record <strong>by reference</strong> using the <code class="language-plaintext highlighter-rouge">var</code> keyword. This ensures that any changes made to the record inside the function are retained and that the function works with the same in-memory instance. In the example below, both <code class="language-plaintext highlighter-rouge">EncodePhoneNumber</code> and the helper method <code class="language-plaintext highlighter-rouge">GetPhoneNumber</code> take <code class="language-plaintext highlighter-rouge">Setup</code> as a <code class="language-plaintext highlighter-rouge">var</code> parameter, allowing the test to run against a temporary, in-memory version of the table instead of querying or modifying the actual database.</p>

<pre><code class="language-al">internal procedure EncodePhoneNumber(var Setup: Record Setup; ObjectKey: Text; PhoneNumber: Text; var Object: JsonObject)
var
    PhoneNumberAsText: Text;
begin
    PhoneNumberAsText := this.GetPhoneNumber(Setup, PhoneNumber);
    Object.Add(ObjectKey, PhoneNumberAsText);
end;
</code></pre>

<pre><code class="language-al">local procedure GetPhoneNumber(var Setup: Record Setup; PhoneNumber: Text): Text
begin
    if Setup."Application State" &lt;&gt; Enum::"Application State"::Production then
        exit(Setup."Phone Number Test");
    exit(PhoneNumber);
end;
</code></pre>

<p>Previously, I would simply have made a <code class="language-plaintext highlighter-rouge">Setup.Get()</code> at the bottom level where I needed the information.
This new approach forces me to have a well-organized architecture of my code.</p>

<h4 id="testing-in-the-context-of-a-production-environment">Testing in the context of a production environment</h4>

<pre><code class="language-al">[Test]
internal procedure TestEncodePhoneNumberProduction()
var
    TempSetup: Record Setup temporary;
    Decoders: Codeunit Decoders;
    Encoders: Codeunit Encoders;
    Object: JsonObject;
    CalculatedPhoneNumber: Text;
    CustomerPhoneNumber: Text;
begin
    // [SCENARIO #015] Testing encoding a phone number to a JSON object in a production environment.
    // [GIVEN] A phone number
    // [WHEN] encoded to JSON object
    // [THEN] we should get the same phone number

    this.TestHelper.InitializeSetup(TempSetup);
    TempSetup."Application State" := Enum::"Application State"::Production;
    CustomerPhoneNumber := '1234567890';
    Encoders.EncodePhoneNumber(TempSetup, 'Test', CustomerPhoneNumber, Object);
    Decoders.DecodeText(Object, 'Test', CalculatedPhoneNumber);
    this.Assert.AreEqual(CustomerPhoneNumber, CalculatedPhoneNumber, 'In the production environment we expected to encode the customer phone number directly.');
end;
</code></pre>

<h4 id="testing-in-the-context-of-a-test-environment">Testing in the context of a test environment</h4>

<pre><code class="language-al">[Test]
internal procedure TestEncodePhoneNumberTest()
var
    TempSetup: Record Setup temporary;
    Decoders: Codeunit Decoders;
    Encoders: Codeunit Encoders;
    Object: JsonObject;
    CalculatedPhoneNumber: Text;
    CustomerPhoneNumber: Text;
begin
    // [SCENARIO #016] Testing encoding a phone number to a JSON object in a test environment.
    // [GIVEN] A phone number
    // [WHEN] encoded to JSON object
    // [THEN] we should get the test phone number

    this.TestHelper.InitializeSetup(TempSetup);
    TempSetup."Application State" := Enum::"Application State"::Test;
    TempSetup."Phone Number Test" := '1234567890';
    CustomerPhoneNumber := '0987654321';
    Encoders.EncodePhoneNumber(TempSetup, 'Test', CustomerPhoneNumber, Object);
    Decoders.DecodeText(Object, 'Test', CalculatedPhoneNumber);
    this.Assert.AreEqual(TempSetup."Phone Number Test", CalculatedPhoneNumber, 'In the test environment we expected to ignore the customer phone number and encode the test phone number from the setup.');
end;
</code></pre>

<h4 id="running-the-actual-code">Running the actual code</h4>

<p>In the actual code, the <code class="language-plaintext highlighter-rouge">Setup.Get()</code> is executed once at the highest level. This also gives me a better understanding of all dependencies on the setup table in the code. That is, we do not retrieve the setup table in multiple places throughout the code. We get it once and pass it around by reference.</p>

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

<p>This new design has several advantages:</p>

<ul>
  <li><strong>Better performance:</strong> Fewer reads to the database.</li>
  <li><strong>Fast, isolated tests:</strong> Run tests without database access using just temporary tables.</li>
  <li><strong>Improved code overview:</strong> Dependencies are clear and easy to manage.</li>
</ul>

<p>Overall, this approach leads to a better design, even though it is not entirely how we used to write code. It is essential for writing modern, maintainable, and high-performance AL test code.</p>

<h2 id="clean-al-code-initiative">Clean AL Code Initiative</h2>

<ul>
  <li>Part 1: <a href="/programming/2023/10/10/BC-Rulesets.html">Rulesets in Business Central</a></li>
  <li>Part 2: <a href="/programming/2023/12/28/Namespaces-in-AL.html">Namespaces in AL</a></li>
  <li>Part 3: <a href="/programming/2023/12/29/Extensions-for-AL.html">VS Code Extensions for AL</a></li>
  <li>Part 4: <a href="/programming/2024/01/03/Automated-Tests-in-AL.html">Automated Tests in AL</a></li>
  <li>Part 5: <a href="/programming/2024/01/05/BC-Advanced-Rulesets.html">Advanced CodeCop Analyzer and Custom Rulesets</a></li>
  <li>Part 6: <a href="/programming/2024/01/11/how-to-make-a-code-review.html">How to make a code review</a></li>
  <li>Part 7: <a href="/programming/2024/01/12/TryFunctions-in-AL.html">Preconditions and TryFunctions in AL</a></li>
</ul>

<h2 id="super-fast-tests-covering-100-of-your-code">Super fast tests covering 100% of your code</h2>

<ul>
  <li>Part 1: <a href="/programming/2025/06/26/environment-interface-part-1.html">Environment Interfaces - Part 1 - System Environment</a></li>
  <li>Part 2: <a href="/programming/2025/06/26/environment-interface-part-2.html">Environment Interfaces - Part 2 - External API</a></li>
  <li>Part 3: <a href="/programming/2025/06/26/environment-interface-part-3.html">Environment Interfaces - Part 3 - Standard Application</a></li>
  <li><strong>Part 4</strong>: <a href="/programming/2025/06/26/temporary-tables-in-tests.html">Temporary Tables in Tests</a></li>
</ul>]]></content><author><name>Finn</name></author><category term="Programming" /><summary type="html"><![CDATA[Temporary Tables in Tests]]></summary></entry><entry><title type="html">Environment Interfaces - Part 3 (Business Central)</title><link href="https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-3.html" rel="alternate" type="text/html" title="Environment Interfaces - Part 3 (Business Central)" /><published>2025-06-26T09:00:00+02:00</published><updated>2025-09-05T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-3</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-3.html"><![CDATA[<h1 id="environment-interfaces---part-3">Environment Interfaces - Part 3</h1>

<p>The theme for this series is <em>Super fast tests covering 100% of your code</em>.</p>

<p>This 2025 series of articles is a continuation of the 2024 series called the Clean Code Initiative.</p>

<p>Clean Code is a condition for writing good tests, but it does not guarantee code coverage by tests.</p>

<h2 id="introduction">Introduction</h2>

<p>I will introduce three different contexts of Environment Interfaces. Discussed in parts 1, 2, and 3.</p>

<ol>
  <li><a href="/programming/2025/06/26/environment-interface-part-1.html">Interface against the system environment (Sandbox or Production)</a></li>
  <li><a href="/programming/2025/06/26/environment-interface-part-2.html">Interface against an external API</a></li>
  <li>Interface against the standard application</li>
</ol>

<blockquote>
  <p><strong>Environment interfaces</strong> usually have a single implementation within the application but can have several implementations in the test application. There is no corresponding <code class="language-plaintext highlighter-rouge">Enum</code> implementing the <code class="language-plaintext highlighter-rouge">Interface</code>.</p>
</blockquote>

<h2 id="scenario">Scenario</h2>

<p>The app we are building communicates with an external API.</p>

<p>We face several key requirements:</p>

<ul>
  <li>The app can only be in production mode when running in a production environment. If running in a sandbox, it must operate in test mode.</li>
  <li>Our tests must cover all code paths, including those only reachable in production mode.</li>
  <li>Our tests cannot call the actual API, but we still need to verify both the requests we send and how we decode the API responses.</li>
  <li>Tests must be extremely fast, with runtimes measured in milliseconds.</li>
  <li>Writing tests should be quick and cost-effective.</li>
  <li>We want to test only our own code, assuming that dependencies and external systems work as expected. We assume everyone else knows what they are doing</li>
</ul>

<blockquote>
  <p><strong>Environment Interfaces</strong> are the solution to this apparent paradox.</p>
</blockquote>

<h2 id="interface-against-standard-business-central">Interface against Standard Business Central</h2>

<p>In parts 1 and 2, we have seen how interfaces enable us to reach code we could otherwise never reach in tests.</p>

<p>However, there are other important constraints: we cannot spend too much time writing tests, and we only want to test our own code.</p>

<p>This is why we sometimes have to develop an interface against the standard application.</p>

<h3 id="finance-interface">Finance interface</h3>

<p>In this case, we are handling payments and refunds.</p>

<pre><code class="language-al">namespace MicrosoftPartner.AppName.Finance;

using Microsoft.Finance.GeneralLedger.Journal;
using Microsoft.Sales.History;

interface IFinance
{
    procedure CreatePaymentLine(
        var PaymentRequest: Record "Payment Request";
        var Setup: Record Setup;
        var GenJournalBatch: Record "Gen. Journal Batch";
        var GenJournalLine: Record "Gen. Journal Line";
        var GenJournalTemplate: Record "Gen. Journal Template";
        var PostedSalesInvoiceHeader: Record "Sales Invoice Header")

    procedure CreateRefundLine(
        var RefundRequest: Record "Refund Request";
        var Setup: Record Setup;
        var GenJournalBatch: Record "Gen. Journal Batch";
        var GenJournalLine: Record "Gen. Journal Line";
        var GenJournalTemplate: Record "Gen. Journal Template";
        var SalesCrMemoHeader: Record "Sales Cr.Memo Header")
}
</code></pre>

<h3 id="implementing-the-app-version-of-the-interface">Implementing the App version of the interface</h3>

<p>The implementation is as you would expect. It is the code you would normally have written. 
We are inserting a GenJournalLine and we are using <code class="language-plaintext highlighter-rouge">.Validate</code> to ensure that the line is created with all the logic on the fields</p>

<pre><code class="language-al">codeunit 50019 "App Finance" implements IFinance
{
    procedure CreatePaymentLine(...)
    begin
        GenJournalLine.Init();
        GenJournalLine.Validate("Journal Template Name", GenJournalTemplate.Name);
        GenJournalLine.Validate("Journal Batch Name", GenJournalBatch.Name);
        GenJournalLine.Validate("Document Type", Enum::"Gen. Journal Document Type"::Payment);
        GenJournalLine.Validate("Document No.", PaymentRequest."Request Reference");
        
        ...
        GenJournalLine.Insert(true);
    end;

    procedure CreateRefundLine(...)
    begin
        GenJournalLine.Init();
        ...
        GenJournalLine.Insert(true);
    end;
}
</code></pre>
<h3 id="implementing-the-test-app-version-of-the-interface">Implementing the Test App version of the interface</h3>

<p>This is where it becomes interesting.</p>

<h4 id="stub-finance">Stub Finance</h4>

<p>The stub function simply returns what I have asked it to return. 
This is how I control which part of my code is touched.</p>

<pre><code class="language-al">namespace MicrosoftPartner.AppName.Test.Helpers;

using Microsoft.Finance.GeneralLedger.Journal;
using Microsoft.Sales.History;

codeunit 50120 "Stub Finance" implements IFinance
{
    Description = 'Stub Finance';

    var
        SetupVar: Record Setup;
        GenJournalBatchVar: Record "Gen. Journal Batch";
        GenJournalLineVar: Record "Gen. Journal Line";
        GenJournalTemplateVar: Record "Gen. Journal Template";
        PaymentRequestVar: Record "Payment Request";
        RefundRequestVar: Record "Refund Request";
        PostedSalesCrMemoHeaderVar: Record "Sales Cr.Memo Header";
        PostedSalesInvoiceHeaderVar: Record "Sales Invoice Header";

    internal procedure CreatePaymentLine(var PaymentRequest: Record "Payment Request"; var Setup: Record Setup; var GenJournalBatch: Record "Gen. Journal Batch"; var GenJournalLine: Record "Gen. Journal Line"; var GenJournalTemplate: Record "Gen. Journal Template"; var PostedSalesInvoiceHeader: Record "Sales Invoice Header")
    begin
        PaymentRequest := this.PaymentRequestVar;
        Setup := this.SetupVar;
        GenJournalBatch := this.GenJournalBatchVar;
        GenJournalLine := this.GenJournalLineVar;
        GenJournalTemplate := this.GenJournalTemplateVar;
        PostedSalesInvoiceHeader := this.PostedSalesInvoiceHeaderVar;
    end;

    internal procedure SetupPaymentResponse(var PaymentRequest: Record "Payment Request"; var Setup: Record Setup; var GenJournalBatch: Record "Gen. Journal Batch"; var GenJournalLine: Record "Gen. Journal Line"; var GenJournalTemplate: Record "Gen. Journal Template"; var PostedSalesInvoiceHeader: Record "Sales Invoice Header")
    begin
        this.PaymentRequestVar := PaymentRequest;
        this.SetupVar := Setup;
        this.GenJournalBatchVar := GenJournalBatch;
        this.GenJournalLineVar := GenJournalLine;
        this.GenJournalTemplateVar := GenJournalTemplate;
        this.PostedSalesInvoiceHeaderVar := PostedSalesInvoiceHeader;
    end;

    internal procedure CreateRefundLine(var RefundRequest: Record "Refund Request"; var Setup: Record Setup; var GenJournalBatch: Record "Gen. Journal Batch"; var GenJournalLine: Record "Gen. Journal Line"; var GenJournalTemplate: Record "Gen. Journal Template"; var SalesCrMemoHeader: Record "Sales Cr.Memo Header")
    begin
        RefundRequest := this.RefundRequestVar;
        Setup := this.SetupVar;
        GenJournalBatch := this.GenJournalBatchVar;
        GenJournalLine := this.GenJournalLineVar;
        GenJournalTemplate := this.GenJournalTemplateVar;
        SalesCrMemoHeader := this.PostedSalesCrMemoHeaderVar;
    end;

    internal procedure SetupRefundResponse(var RefundRequest: Record "Refund Request"; var Setup: Record Setup; var GenJournalBatch: Record "Gen. Journal Batch"; var GenJournalLine: Record "Gen. Journal Line"; var GenJournalTemplate: Record "Gen. Journal Template"; var SalesCrMemoHeader: Record "Sales Cr.Memo Header")
    begin
        this.RefundRequestVar := RefundRequest;
        this.SetupVar := Setup;
        this.GenJournalBatchVar := GenJournalBatch;
        this.GenJournalLineVar := GenJournalLine;
        this.GenJournalTemplateVar := GenJournalTemplate;
        this.PostedSalesCrMemoHeaderVar := SalesCrMemoHeader;
    end;
}
</code></pre>

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

<p>Running logic in the base application introduces several challenges:</p>

<p>Setting up tests is time-consuming due to dependencies on G/L Accounts, Banks, General Journal Batch, General Journal Template, Number Series, and more — a rabbit hole that can consume significant amounts of time.</p>

<p>It creates a strong dependency on the base app, increasing maintenance overhead.</p>

<p>It is slow, as it writes to the database and rolls back changes, preventing the use of temporary variables.</p>

<p>By using Environment Interfaces, I have avoided these issues. My tests are fast, independent, and focused on my code.</p>

<p>You might say that this is cheating. That I am not really testing the code.
And you are right in the sense that I have to understand perfectly what Business Central is returning when it creates the General Journal Line. But I am not testing that Business Central is working; I am testing that my code is working.</p>

<h2 id="clean-al-code-initiative">Clean AL Code Initiative</h2>

<ul>
  <li>Part 1: <a href="/programming/2023/10/10/BC-Rulesets.html">Rulesets in Business Central</a></li>
  <li>Part 2: <a href="/programming/2023/12/28/Namespaces-in-AL.html">Namespaces in AL</a></li>
  <li>Part 3: <a href="/programming/2023/12/29/Extensions-for-AL.html">VS Code Extensions for AL</a></li>
  <li>Part 4: <a href="/programming/2024/01/03/Automated-Tests-in-AL.html">Automated Tests in AL</a></li>
  <li>Part 5: <a href="/programming/2024/01/05/BC-Advanced-Rulesets.html">Advanced CodeCop Analyzer and Custom Rulesets</a></li>
  <li>Part 6: <a href="/programming/2024/01/11/how-to-make-a-code-review.html">How to make a code review</a></li>
  <li>Part 7: <a href="/programming/2024/01/12/TryFunctions-in-AL.html">Preconditions and TryFunctions in AL</a></li>
</ul>

<h2 id="super-fast-tests-covering-100-of-your-code">Super fast tests covering 100% of your code</h2>

<ul>
  <li>Part 1: <a href="/programming/2025/06/26/environment-interface-part-1.html">Environment Interfaces - Part 1 - System Environment</a></li>
  <li>Part 2: <a href="/programming/2025/06/26/environment-interface-part-2.html">Environment Interfaces - Part 2 - External API</a></li>
  <li><strong>Part 3</strong>: <a href="/programming/2025/06/26/environment-interface-part-3.html">Environment Interfaces - Part 3 - Standard Application</a></li>
  <li>Part 4: <a href="/programming/2025/06/26/temporary-tables-in-tests.html">Temporary Tables in Tests</a></li>
</ul>

<h2 id="london-style-terminology">London Style Terminology</h2>

<p>When writing tests in Business Central, you may come across different types of test doubles—objects that replace real dependencies in your code. The “London style” (or “mockist”) terminology is widely used in the testing community to describe these patterns. Understanding these terms will help you communicate more clearly about your test design and choose the right approach for your scenario.</p>

<table>
  <thead>
    <tr>
      <th>Term</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Dummy</strong></td>
      <td>An object passed as a parameter but never used. Only exists to fill parameter lists.</td>
    </tr>
    <tr>
      <td><strong>Stub</strong></td>
      <td>A fake object that returns fixed data and <strong>does not track usage</strong>.</td>
    </tr>
    <tr>
      <td><strong>Fake</strong></td>
      <td>A working implementation with simplified behavior (e.g., in-memory database).</td>
    </tr>
    <tr>
      <td><strong>Spy</strong></td>
      <td>Like a stub, but records how it was called for later verification.</td>
    </tr>
    <tr>
      <td><strong>Mock</strong></td>
      <td>A fake that <strong>also verifies</strong> how it was used (e.g., was method X called?).</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Finn</name></author><category term="Programming" /><summary type="html"><![CDATA[Environment Interfaces - Part 3]]></summary></entry><entry><title type="html">Environment Interfaces - Part 2 (Business Central)</title><link href="https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-2.html" rel="alternate" type="text/html" title="Environment Interfaces - Part 2 (Business Central)" /><published>2025-06-26T08:00:00+02:00</published><updated>2025-09-05T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-2</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-2.html"><![CDATA[<h1 id="environment-interfaces---part-2">Environment Interfaces - Part 2</h1>

<p>The theme for this series is <em>Super fast tests covering 100% of your code</em>.</p>

<p>This 2025 series of articles is a continuation of the 2024 series called the Clean Code Initiative.</p>

<p>Clean Code is a condition for writing good tests, but it does not guarantee code coverage by tests.</p>

<h2 id="introduction">Introduction</h2>

<p>I will introduce three different contexts of Environment Interfaces. Discussed in parts 1, 2, and 3.</p>

<ol>
  <li><a href="/programming/2025/06/26/environment-interface-part-1.html">Interface against the system environment (Sandbox or Production)</a></li>
  <li>Interface against an external API</li>
  <li><a href="/programming/2025/06/26/environment-interface-part-3.html">Interface against the standard application</a></li>
</ol>

<blockquote>
  <p><strong>Environment interfaces</strong> usually have a single implementation within the application but can have several implementations in the test application. There is no corresponding <code class="language-plaintext highlighter-rouge">Enum</code> implementing the <code class="language-plaintext highlighter-rouge">Interface</code>.</p>
</blockquote>

<h2 id="scenario">Scenario</h2>

<p>The app we are building communicates with an external API.</p>

<p>We face several key requirements:</p>

<ul>
  <li>The app can only be in production mode when running in a production environment. If running in a sandbox, it must operate in test mode.</li>
  <li>Our tests must cover all code paths, including those only reachable in production mode.</li>
  <li>Our tests cannot call the actual API, but we still need to verify both the requests we send and how we decode the API responses.</li>
  <li>Tests must be extremely fast, with runtimes measured in milliseconds.</li>
  <li>Writing tests should be quick and cost-effective.</li>
  <li>We want to test only our own code, assuming that dependencies and external systems work as expected. We assume everyone else knows what they are doing</li>
</ul>

<blockquote>
  <p><strong>Environment Interfaces</strong> are the solution to this apparent paradox.</p>
</blockquote>

<h2 id="interface-against-the-external-api">Interface against the external API</h2>

<p>Before describing the interface, I need a small helper Enum.</p>

<h3 id="api-method-enum">Api Method Enum</h3>

<p>There is a standard Enum, but I just want to keep it simple and to what I need.</p>

<pre><code class="language-al">enum 50005 "Api Method"
{
    value(0; GET)
    {
        Caption = 'GET', Locked = true;
    }
    value(1; POST)
    {
        Caption = 'POST', Locked = true;
    }
}
</code></pre>

<h3 id="api-request-interface">Api Request Interface</h3>

<p>It is a very simple interface with just one function <strong>Send</strong>.
But it is all we need.</p>

<pre><code class="language-al">interface IApiRequest
{
    procedure Send(RequestMethod: Enum "Api Method"; RequestUrl: Text; Payload: Text; SecretKey: SecretText; var ResponseStatusCode: Integer; var ResponseContent: Text)
}
</code></pre>

<h3 id="implementing-the-app-version-of-the-interface">Implementing the App version of the interface</h3>

<p>The implementation of the Send function is quite generic and you can reuse this code. The API accepts a JSON payload, an encrypted key, and it returns a Status Code (200 for OK) and a response payload also in JSON.</p>

<pre><code class="language-al">codeunit 50011 "App Api Request" implements IApiRequest
{
    internal procedure Send(RequestMethod: Enum "Api Method"; RequestUrl: Text; Payload: Text; SecretKey: SecretText; var ResponseStatusCode: Integer; var ResponseContent: Text)
    var
        ApiTools: Codeunit ApiTools;
        HttpClient: HttpClient;
        HttpContent: HttpContent;
        ContentHeaders: HttpHeaders;
        RequestHeaders: HttpHeaders;
        HttpRequestMessage: HttpRequestMessage;
        HttpResponseMessage: HttpResponseMessage;
        Hash: SecretText;
    begin
        Hash := ApiTools.CreateHash(Payload, SecretKey);

        HttpRequestMessage.Method := Format(RequestMethod);
        HttpRequestMessage.SetRequestUri(RequestUrl);
        HttpRequestMessage.GetHeaders(RequestHeaders);
        RequestHeaders.Clear();
        RequestHeaders.Add('Accept', 'application/json');
        RequestHeaders.Add('Authorization', Hash);

        if RequestMethod = Enum::"Api Method"::POST then begin
            HttpContent.WriteFrom(Payload);
            HttpContent.GetHeaders(ContentHeaders);
            ContentHeaders.Clear();
            ContentHeaders.Add('Content-Type', 'application/json');
            HttpRequestMessage.Content := HttpContent;
        end;

        if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
            ResponseStatusCode := HttpResponseMessage.HttpStatusCode();
            if ResponseStatusCode = ApiTools.HttpStatusCodeOK() then
                HttpResponseMessage.Content().ReadAs(ResponseContent)
            else
                ApiTools.WebServiceCallFailedError(HttpResponseMessage.HttpStatusCode());
        end else
            ApiTools.ConnectionError();
    end;
}
</code></pre>

<h3 id="implementing-the-test-app-version-of-the-interface">Implementing the Test App version of the interface</h3>

<p>This is where it becomes interesting.</p>

<h4 id="stub-api-request">Stub Api Request</h4>

<p>The stub function simply returns what I have asked it to return. 
This is how I control which part of my code is touched.</p>

<pre><code class="language-al">codeunit 50113 "Stub Api Request" implements IApiRequest
{
    Description = 'Stub Api Request';

    var
        ResponseStatusCodeVar: Integer;
        ResponseContentVar: Text;

    internal procedure Send(RequestMethod: Enum "Api Method"; RequestUrl: Text; Payload: Text; SecretKey: SecretText; var ResponseStatusCode: Integer; var ResponseContent: Text)
    begin
        ResponseStatusCode := this.ResponseStatusCodeVar;
        ResponseContent := this.ResponseContentVar;
    end;

    internal procedure SetupResponse(ResponseStatusCode: Integer; ResponseContent: Text)
    begin
        this.ResponseStatusCodeVar := ResponseStatusCode;
        this.ResponseContentVar := ResponseContent;
    end;
}
</code></pre>

<h3 id="using-it">Using it</h3>

<p>For each endpoint the API exposes, I have written a Codeunit with these functions</p>

<pre><code class="language-al">codeunit 50007 "Api Operation &lt;Endpointname&gt;"
{
    Description = 'Implementation of the Api Operation &lt;Endpointname&gt;';

    internal procedure OperationType(): Text
    begin
        exit('&lt;Endpointname&gt;');
    end;

    internal procedure RequestMethod(): Enum "Api Method"
    begin
        exit(Enum::"Api Method"::POST);
    end;

    internal procedure RequestPayload(var EndpointTable: Record "Endpoint Table Name"): Text[2048]
    begin
        exit(&lt;the calculated payload&gt;);
    end;

    internal procedure SendRequest(var Setup: Record Setup"; var EndpointTable: Record "Endpoint Table Name"; ApiRequest: Interface IApiRequest)
    var
        Decoders: Codeunit Decoders;
        ResponseStatusCode: Integer;
        SecretKey: SecretText;
        RequestUrl: Text;
        ResponseContent: Text;
    begin
        RequestUrl := Setup.ApiUrl(this.OperationType());
        SecretKey := Setup.ApiHmac();
        ResponseStatusCode := 0;
        ApiRequest.Send(this.RequestMethod(), RequestUrl, this.RequestPayload(EndpointTable), SecretKey, ResponseStatusCode, ResponseContent);
        Decoders.DecodeEndpointResponse(Setup, ResponseStatusCode, ResponseContent, EndpointTable);
    end;
}
</code></pre>

<h4 id="running-the-actual-code">Running the actual code</h4>

<p>When I call the SendRequest function, I pass it the implementation.</p>

<pre><code class="language-al">var
  AppApiRequest: Codeunit "App Api Request";
...

ApiOperationEndpoint.SendRequest(Setup, Rec, AppApiRequest);
</code></pre>

<h4 id="testing-in-the-context-of-a-fake-api">Testing in the context of a fake API</h4>

<p>Before calling <code class="language-plaintext highlighter-rouge">SendRequest</code>, I call the <code class="language-plaintext highlighter-rouge">SetupResponse</code> and tell my fake API what to return to me. 
I know that this sounds strange at first, but trust me. I am not testing if the API works. 
I am testing how my code reacts to the response from the API. 
In this way, I can ensure that I am also testing more exotic events and responses.</p>

<p><strong>This is the setup for success:</strong></p>

<pre><code class="language-al">StubApiRequest.SetupResponse(200, '{"Result":"OK","Reference":"TEST"}');
ApiOperationEndpoint.SendRequest(TempSetup, TempRecord, StubApiRequest);
</code></pre>

<p><strong>Failure could look like this:</strong></p>

<pre><code class="language-al">StubApiRequest.SetupResponse(200, StubApiResponses.NotUniqueErrorResponse());
ApiOperationEndpoint.SendRequest(TempSetup, TempRecord, StubApiRequest);
</code></pre>

<p>I still get a valid JSON, but something is wrong. Then I am testing if my decoders are working as they should.</p>

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

<p>By using stubs and environment interfaces, I can thoroughly test all my decoding logic, failure detection, and error handling — without ever calling the real API during tests.</p>

<p>This approach ensures that every line of my code is exercised and verified, resulting in fast, reliable, and maintainable tests for my Business Central app.</p>

<h2 id="clean-al-code-initiative">Clean AL Code Initiative</h2>

<ul>
  <li>Part 1: <a href="/programming/2023/10/10/BC-Rulesets.html">Rulesets in Business Central</a></li>
  <li>Part 2: <a href="/programming/2023/12/28/Namespaces-in-AL.html">Namespaces in AL</a></li>
  <li>Part 3: <a href="/programming/2023/12/29/Extensions-for-AL.html">VS Code Extensions for AL</a></li>
  <li>Part 4: <a href="/programming/2024/01/03/Automated-Tests-in-AL.html">Automated Tests in AL</a></li>
  <li>Part 5: <a href="/programming/2024/01/05/BC-Advanced-Rulesets.html">Advanced CodeCop Analyzer and Custom Rulesets</a></li>
  <li>Part 6: <a href="/programming/2024/01/11/how-to-make-a-code-review.html">How to make a code review</a></li>
  <li>Part 7: <a href="/programming/2024/01/12/TryFunctions-in-AL.html">Preconditions and TryFunctions in AL</a></li>
</ul>

<h2 id="super-fast-tests-covering-100-of-your-code">Super fast tests covering 100% of your code</h2>

<ul>
  <li>Part 1: <a href="/programming/2025/06/26/environment-interface-part-1.html">Environment Interfaces - Part 1 - System Environment</a></li>
  <li><strong>Part 2</strong>: <a href="/programming/2025/06/26/environment-interface-part-2.html">Environment Interfaces - Part 2 - External API</a></li>
  <li>Part 3: <a href="/programming/2025/06/26/environment-interface-part-3.html">Environment Interfaces - Part 3 - Standard Application</a></li>
  <li>Part 4: <a href="/programming/2025/06/26/temporary-tables-in-tests.html">Temporary Tables in Tests</a></li>
</ul>

<h2 id="london-style-terminology">London Style Terminology</h2>

<p>When writing tests in Business Central, you may come across different types of test doubles—objects that replace real dependencies in your code. The “London style” (or “mockist”) terminology is widely used in the testing community to describe these patterns. Understanding these terms will help you communicate more clearly about your test design and choose the right approach for your scenario.</p>

<table>
  <thead>
    <tr>
      <th>Term</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Dummy</strong></td>
      <td>An object passed as a parameter but never used. Only exists to fill parameter lists.</td>
    </tr>
    <tr>
      <td><strong>Stub</strong></td>
      <td>A fake object that returns fixed data and <strong>does not track usage</strong>.</td>
    </tr>
    <tr>
      <td><strong>Fake</strong></td>
      <td>A working implementation with simplified behavior (e.g., in-memory database).</td>
    </tr>
    <tr>
      <td><strong>Spy</strong></td>
      <td>Like a stub, but records how it was called for later verification.</td>
    </tr>
    <tr>
      <td><strong>Mock</strong></td>
      <td>A fake that <strong>also verifies</strong> how it was used (e.g., was method X called?).</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Finn</name></author><category term="Programming" /><summary type="html"><![CDATA[Environment Interfaces - Part 2]]></summary></entry><entry><title type="html">Environment Interfaces - Part 1 (Business Central)</title><link href="https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-1.html" rel="alternate" type="text/html" title="Environment Interfaces - Part 1 (Business Central)" /><published>2025-06-26T07:00:00+02:00</published><updated>2025-09-05T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-1</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2025/06/26/environment-interface-part-1.html"><![CDATA[<h1 id="environment-interfaces---part-1">Environment Interfaces - Part 1</h1>

<p>The theme for this series is <em>Super fast tests covering 100% of your code</em>.</p>

<p>This 2025 series of articles is a continuation of the 2024 series called the Clean Code Initiative.</p>

<p>Clean Code is a prerequisite for writing good tests, but it does not guarantee code coverage by tests.</p>

<h2 id="introduction">Introduction</h2>

<p>I will introduce three different contexts of Environment Interfaces, discussed in parts 1, 2, and 3.</p>

<ol>
  <li>Interface against the system environment (Sandbox or Production)</li>
  <li><a href="/programming/2025/06/26/environment-interface-part-2.html">Interface against an external API</a></li>
  <li><a href="/programming/2025/06/26/environment-interface-part-3.html">Interface against the standard application</a></li>
</ol>

<blockquote>
  <p><strong>Environment interfaces</strong> usually have a single implementation within the application but can have several implementations in the test application. There is no corresponding <code class="language-plaintext highlighter-rouge">Enum</code> implementing the <code class="language-plaintext highlighter-rouge">Interface</code>.</p>
</blockquote>

<h2 id="scenario">Scenario</h2>

<p>The app we are building communicates with an external API.</p>

<p>We face several key requirements:</p>

<ul>
  <li>The app can only be in production mode when running in a production environment. If running in a sandbox, it must operate in test mode.</li>
  <li>Our tests must cover all code paths, including those only reachable in production mode.</li>
  <li>Our tests cannot call the actual API, but we still need to verify both the requests we send and how we decode the API responses.</li>
  <li>Tests must be extremely fast, with runtimes measured in milliseconds.</li>
  <li>Writing tests should be quick and cost-effective.</li>
  <li>We want to test only our own code, assuming that dependencies and external systems work as expected. We assume everyone else knows what they are doing</li>
</ul>

<blockquote>
  <p><strong>Environment Interfaces</strong> are the solution to this apparent paradox.</p>
</blockquote>

<h2 id="interface-against-the-system-environment">Interface against the system environment</h2>

<p>You can find the code for this article on GitHub.</p>

<p><a href="https://github.com/finnpedersenfrance/BC-Environment-Interfaces">github.com/finnpedersenfrance/BC-Environment-Interfaces</a></p>

<h3 id="system-environment-enum">System Environment Enum</h3>
<p>First, we need a new Enum. If you are running BC in the cloud, there are only two options. You are either in a
<strong>Sandbox</strong> or a <strong>Production</strong> environment.</p>

<pre><code class="language-al">enum 50004 "System Environment"
{
    value(0; " ")
    {
        Caption = ' ', Locked = true;
    }
    value(1; Sandbox)
    {
        Caption = 'Sandbox';
    }
    value(2; Production)
    {
        Caption = 'Production';
    }
}
</code></pre>

<h3 id="environment-interface">Environment Interface</h3>

<p>Then we need an interface.</p>

<p>The interface is the recipe for which functions need to be implemented.</p>

<pre><code class="language-al">interface IEnvironment
{
    /// &lt;summary&gt;
    /// Returns the state of the current system environment. That is either Production, Sandbox or empty.
    /// &lt;/summary&gt;
    /// &lt;param&gt;&lt;/param&gt;
    /// &lt;returns&gt;System Environment&lt;/returns&gt;
    procedure SystemEnvironment(): Enum "System Environment"

    /// &lt;summary&gt;
    /// Returns the name of the current company.
    /// &lt;/summary&gt;
    /// &lt;param&gt;&lt;/param&gt;
    /// &lt;returns&gt;Company Name&lt;/returns&gt;
    procedure ThisCompanyName(): Text[30]

    /// &lt;summary&gt;
    /// Returns the state of the current company. That is either Production or Evaluation.
    /// &lt;/summary&gt;
    /// &lt;param&gt;&lt;/param&gt;
    /// &lt;returns&gt;False if Production Company. True if Evaluation Company.&lt;/returns&gt;
    procedure IsEvaluationCompany(): Boolean
}
</code></pre>

<h3 id="implementing-the-app-version-of-the-interface">Implementing the App version of the interface</h3>

<p>The implementation is as you would expect. It is the code you would normally have written.</p>

<pre><code class="language-al">codeunit 50010 "App Environment" implements IEnvironment
{
    internal procedure SystemEnvironment(): Enum "System Environment"
    var
        EnvironmentInformation: Codeunit System.Environment."Environment Information";
    begin
        if EnvironmentInformation.IsProduction() then
            exit(Enum::"System Environment"::Production);
        if EnvironmentInformation.IsSandbox() then
            exit(Enum::"System Environment"::Sandbox);
        exit(Enum::"System Environment"::" ");
    end;

    internal procedure ThisCompanyName(): Text[30]
    begin
        exit(CopyStr(CompanyName(), 1, 30));
    end;

    internal procedure IsEvaluationCompany(): Boolean
    var
        Company: Record System.Environment.Company;
    begin
        Company.Get(CompanyName());
        exit(Company."Evaluation Company");
    end;
}
</code></pre>

<p>This also allows us to replace the booleans with an enum, which is much nicer.</p>

<blockquote>
  <p>Notice that <code class="language-plaintext highlighter-rouge">ThisCompanyName()</code> also solves the <strong>CodeCop Warning AA0139</strong> (overflow). <code class="language-plaintext highlighter-rouge">CompanyName()</code> returns a Text which causes an overflow warning when you assign it to a <code class="language-plaintext highlighter-rouge">Company Name</code> field of type <strong>Text[30]</strong>. With this interface function, I have solved this everywhere I needed to use <code class="language-plaintext highlighter-rouge">CompanyName()</code>.</p>
</blockquote>

<h3 id="implementing-the-test-app-version-of-the-interface">Implementing the Test App version of the interface</h3>

<p>This is where it becomes interesting.</p>

<h4 id="stub-production">Stub Production</h4>

<p>I have written a Stub Production implementation of the environment. 
This allows me to fool my code into believing that it is in production.</p>

<pre><code class="language-al">codeunit 50102 "Stub Production Environment" implements IEnvironment
{
    Description = 'Stub Production Environment';

    procedure SystemEnvironment(): Enum "System Environment"
    begin
        exit(Enum::"System Environment"::Production);
    end;

    procedure ThisCompanyName(): Text[30]
    begin
        exit('Production Company Name');
    end;

    procedure IsEvaluationCompany(): Boolean
    begin
        exit(false);
    end;
}
</code></pre>
<blockquote>
  <p>Take a minute to look at how elegantly this allows us to test our code as if we were live in a production environment.</p>
</blockquote>

<h4 id="stub-test">Stub Test</h4>

<p>And a Stub Test implementation of the environment.</p>

<pre><code class="language-al">codeunit 50103 "Stub Test Environment" implements IEnvironment
{
    Description = 'Stub Test Environment';

    procedure SystemEnvironment(): Enum "System Environment"
    begin
        exit(Enum::"System Environment"::Sandbox);
    end;

    procedure ThisCompanyName(): Text[30]
    begin
        exit('Test Company Name');
    end;

    procedure IsEvaluationCompany(): Boolean
    begin
        exit(true);
    end;
}
</code></pre>

<blockquote>
  <p>And now we are in an evaluation company in a sandbox.</p>
</blockquote>

<h3 id="using-it">Using it</h3>

<p>It is crucial for my application code to know if the application is <strong>Production</strong> or in a <strong>Test</strong> setup.
For this, I am using an <code class="language-plaintext highlighter-rouge">enum</code> Application State.</p>

<pre><code class="language-al">enum 50006 "Application State"
{
    Extensible = true;

    value(0; " ")
    {
        Caption = ' ', Locked = true;
    }
    value(1; Test)
    {
        Caption = 'Test';
    }
    value(2; Production)
    {
        Caption = 'Production';
    }
}
</code></pre>

<p>I have written a function to calculate the state of the application based on the environment and the setup. The functions are in my Setup table.</p>

<pre><code class="language-al">internal procedure ValidateApplicationState(Environment: Interface IEnvironment)
begin
    Rec."Application State" := this.ApplicationState(Rec, Environment);
end;

internal procedure ApplicationState(var Setup: Record Setup; Environment: Interface IEnvironment): Enum "Application State"
begin
    case true of
        (Setup.Licensee = Environment.ThisCompanyName()) and
        (Environment.SystemEnvironment() = Enum::"System Environment"::Production) and
        (not Environment.IsEvaluationCompany()):
            exit(Enum::"Application State"::Production);
        (Setup.Licensee = Environment.ThisCompanyName()):
            exit(Enum::"Application State"::Test);
        else
            exit(Enum::"Application State"::" ");
    end;
end;
</code></pre>

<p>We are in <strong>production mode</strong>, only and only if</p>

<ul>
  <li>the app is in a Production environment and</li>
  <li>it is not an evaluation company, and</li>
  <li>the Licensee matches the name of the current company</li>
</ul>

<p>We are in a <strong>test mode</strong> if</p>
<ul>
  <li>the solution is running in a sandbox or</li>
  <li>the company is an evaluation company</li>
  <li>the Licensee still has to match the name of the current company</li>
</ul>

<p><strong>An important safety feature</strong></p>

<p>If someone makes a copy of the company, the company name will differ from the Licensee name in our setup, which will automatically disable the solution and prevent anyone from using the solution by mistake.</p>

<p>Now I can call this function in both my tests and production, reaching all the code.</p>

<h4 id="testing-in-the-context-of-a-test-environment">Testing in the context of a test environment</h4>

<p>Testing that the application is in test mode in a sandbox.</p>

<pre><code class="language-al">[Test]
internal procedure TestTestMode()
var
    TempSetup: Record Setup temporary;
    StubTestEnvironment: Codeunit "Stub Test Environment";
begin
    // [SCENARIO #003] Application State in test
    // [GIVEN] a setup with a valid licensee
    // [WHEN] calling ValidateApplicationState
    // [THEN] the application state should be test

    this.TestHelper.InitializeSetup(TempSetup);
    TempSetup.Licensee := StubTestEnvironment.ThisCompanyName();
    TempSetup.ValidateApplicationState(StubTestEnvironment);

    this.Assert.AreEqual(Enum::"Application State"::Test, TempSetup."Application State", 'Expected Application State to be Test.');
end;
</code></pre>

<h4 id="testing-in-the-context-of-a-production-environment">Testing in the context of a production environment</h4>

<p>Testing in our simulated production environment.</p>

<pre><code class="language-al">[Test]
internal procedure TestProductionMode()
var
    TempSetup: Record Setup temporary;
    StubProductionEnvironment: Codeunit "Stub Production Environment";
begin
    // [SCENARIO #002] Application State in production
    // [GIVEN] a setup with a valid licensee
    // [WHEN] calling ValidateApplicationState
    // [THEN] the application state should be production

    this.TestHelper.InitializeSetup(TempSetup);
    TempSetup.Licensee := StubProductionEnvironment.ThisCompanyName();
    TempSetup.ValidateApplicationState(StubProductionEnvironment);

    this.Assert.AreEqual(Enum::"Application State"::Production, TempSetup."Application State", 'Expected Application State to be Production.');
end;
</code></pre>

<p>Testing that the solution is disabled if there is no licensee.</p>

<pre><code class="language-al">[Test]
internal procedure TestNoCompanyName()
var
    TempSetup: Record Setup temporary;
    StubProductionEnvironment: Codeunit "Stub Production Environment";
begin
    // [SCENARIO #001] No company name in disabled environment
    // [GIVEN] a setup with no licensee
    // [WHEN] calling ValidateApplicationState
    // [THEN] the application state should be empty. I.e. disabled.

    this.TestHelper.InitializeSetup(TempSetup);
    TempSetup.Licensee := '';
    TempSetup.ValidateApplicationState(StubProductionEnvironment);

    this.Assert.AreEqual(Enum::"Application State"::" ", TempSetup."Application State", 'Expected Application State to be disabled.');
end;

</code></pre>
<h4 id="the-code-in-the-app">The code in the App</h4>

<p>I am not testing the following code at the highest level. I call the function from the Setup page.</p>

<pre><code class="language-al">trigger OnModifyRecord(): Boolean
var
    AppEnvironment: Codeunit "App Environment";
begin
    Rec.ValidateApplicationState(AppEnvironment);
end;
</code></pre>

<h2 id="bonus-safety-feature">Bonus Safety feature</h2>

<p>You can implement one more safety feature. Just to be sure that you solution is disabled in the copied company. 
Subscribe to the <code class="language-plaintext highlighter-rouge">OnAfterCopyCompanyOnAction</code> event.</p>

<pre><code class="language-al">codeunit 50031 "Event Subscribers Copy Company"
{
    Description = 'Event Subscribers for Copy Company. This ensures that the copied company cannot be used by mistake.';

    [EventSubscriber(ObjectType::Page, Page::Companies, OnAfterCopyCompanyOnAction, '', false, false)]
    local procedure OnAfterCopyCompanyOnAction(CompanyName: Text[30])
    var
        Setup: Record Setup;
    begin
        Setup.ChangeCompany(CompanyName);
        if Setup.Get() then begin
            Setup.Licensee := '';
            Setup."Application State" := Enum::"Application State"::" ";
            Setup."Job Scheduler on Standby" := true;
            Setup.Modify(false);
        end;
    end;
}
</code></pre>

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

<p>Bugs are often found in code that was never executed before. Think about it. If I had a bug in the part of my code that would only be reached in production, I would otherwise never find it during tests.</p>

<p>With this design, I am quite confident that my code will act as planned when put in production and my tests cover 100% of my code.</p>

<h2 id="clean-al-code-initiative">Clean AL Code Initiative</h2>

<ul>
  <li>Part 1: <a href="/programming/2023/10/10/BC-Rulesets.html">Rulesets in Business Central</a></li>
  <li>Part 2: <a href="/programming/2023/12/28/Namespaces-in-AL.html">Namespaces in AL</a></li>
  <li>Part 3: <a href="/programming/2023/12/29/Extensions-for-AL.html">VS Code Extensions for AL</a></li>
  <li>Part 4: <a href="/programming/2024/01/03/Automated-Tests-in-AL.html">Automated Tests in AL</a></li>
  <li>Part 5: <a href="/programming/2024/01/05/BC-Advanced-Rulesets.html">Advanced CodeCop Analyzer and Custom Rulesets</a></li>
  <li>Part 6: <a href="/programming/2024/01/11/how-to-make-a-code-review.html">How to make a code review</a></li>
  <li>Part 7: <a href="/programming/2024/01/12/TryFunctions-in-AL.html">Preconditions and TryFunctions in AL</a></li>
</ul>

<h2 id="super-fast-tests-covering-100-of-your-code">Super fast tests covering 100% of your code</h2>

<ul>
  <li><strong>Part 1</strong>: <a href="/programming/2025/06/26/environment-interface-part-1.html">Environment Interfaces - Part 1 - System Environment</a></li>
  <li>Part 2: <a href="/programming/2025/06/26/environment-interface-part-2.html">Environment Interfaces - Part 2 - External API</a></li>
  <li>Part 3: <a href="/programming/2025/06/26/environment-interface-part-3.html">Environment Interfaces - Part 3 - Standard Application</a></li>
  <li>Part 4: <a href="/programming/2025/06/26/temporary-tables-in-tests.html">Temporary Tables in Tests</a></li>
</ul>

<h2 id="london-style-terminology">London Style Terminology</h2>

<p>When writing tests in Business Central, you may come across different types of test doubles—objects that replace real dependencies in your code. The “London style” (or “mockist”) terminology is widely used in the testing community to describe these patterns. Understanding these terms will help you communicate more clearly about your test design and choose the right approach for your scenario.</p>

<table>
  <thead>
    <tr>
      <th>Term</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Dummy</strong></td>
      <td>An object passed as a parameter but never used. Only exists to fill parameter lists.</td>
    </tr>
    <tr>
      <td><strong>Stub</strong></td>
      <td>A fake object that returns fixed data and <strong>does not track usage</strong>.</td>
    </tr>
    <tr>
      <td><strong>Fake</strong></td>
      <td>A working implementation with simplified behavior (e.g., in-memory database).</td>
    </tr>
    <tr>
      <td><strong>Spy</strong></td>
      <td>Like a stub, but records how it was called for later verification.</td>
    </tr>
    <tr>
      <td><strong>Mock</strong></td>
      <td>A fake that <strong>also verifies</strong> how it was used (e.g., was method X called?).</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Finn</name></author><category term="Programming" /><summary type="html"><![CDATA[Environment Interfaces - Part 1]]></summary></entry><entry><title type="html">How to Contribute to Wikipedia</title><link href="https://www.finnpedersenfrance.com/technology/2025/01/17/how-to-contribute-to-wikipedia.html" rel="alternate" type="text/html" title="How to Contribute to Wikipedia" /><published>2025-01-17T06:00:00+01:00</published><updated>2025-09-05T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/technology/2025/01/17/how-to-contribute-to-wikipedia</id><content type="html" xml:base="https://www.finnpedersenfrance.com/technology/2025/01/17/how-to-contribute-to-wikipedia.html"><![CDATA[<h1 id="how-to-contribute-to-wikipedia--my-experience">How to Contribute to Wikipedia — My Experience</h1>

<p>I recently took a small detour from my usual topics to write my first Wikipedia article.</p>

<p>I have been renting out apartments in Denmark for more than 30 years and found myself searching for some information that dated back before 2001.</p>

<p>I wanted to share the information because</p>

<ul>
  <li>It was hard to find</li>
  <li>It was of general interest for both parties when renting an apartment, a room, or a house in Denmark.</li>
  <li>It potentially concerns a lot of people. Basically, all landlords and tenants.</li>
</ul>

<p>Since it was missing from Wikipedia, I decided to fill that gap. Here is a short guide, based on my experience, on how you can do it too.</p>

<h2 id="my-article-only-in-danish">My article (only in Danish)</h2>

<ul>
  <li><a href="https://da.wikipedia.org/wiki/Standardblanket_for_lejeaftaler">Wikipedia: Standardblanket for lejeaftaler</a></li>
</ul>

<p>In short, the article helps landlords and tenants to use the right version of the rental contract.
Over the past 29 years, there have been 4 versions, and I explain the importance of using the right version and
the potential consequences when you don’t.</p>

<h2 id="preparation">Preparation</h2>

<p><strong>Check if your topic is missing or underdeveloped</strong></p>

<p>Before creating an article, do a quick search on Wikipedia to make sure it doesn’t already exist or that it’s not adequately covered.</p>

<p><strong>Read the key guidelines</strong></p>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Wikipedia:Neutral_point_of_view">Wikipedia:Neutral Point of View</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style">Wikipedia:Manual of Style</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Wikipedia:Five_pillars">Wikipedia:Five Pillars</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Wikipedia:Notability">Wikipedia:Notability</a></li>
</ul>

<p>There is a lot to read and it took me some time. 
Most is common sense, but I had extra focus on ensuring the <strong>Neutral Point of View</strong>.
I also wanted to make sure that it looked like a real Wikipedia article. That is, ensure consistency.</p>

<p><strong>Gather reliable sources</strong></p>

<p>Every fact, statement, or claim should ideally have a reference from a reputable source. Wikipedia articles thrive on verifiability.</p>

<p>Most of my sources were government websites, so that was easy.</p>

<h2 id="writing-your-draft">Writing Your Draft</h2>

<p>Once you have signed up, you go to your Sandbox and start writing.</p>

<p><strong>Use the Wikipedia Sandbox</strong></p>

<p>You draft your article in the personal sandbox to get familiar with Wikipedia’s markup and style before publishing.</p>

<p><strong>Focus on neutrality</strong></p>

<p>Wikipedia articles must avoid personal opinions. State the facts, cite sources, and keep a neutral tone.</p>

<p>I made sure that both the landlord and the tenant would benefit equally from the article.</p>

<p><strong>Wikipedia markup</strong></p>

<p>Wikipedia has a special markup language, which is different from, for example, Markdown.
But you don’t really have to know anything about it. Simply use the editor.</p>

<p>Here are a few examples.</p>

<ul>
  <li>Use double square brackets for internal links, e.g., <code class="language-plaintext highlighter-rouge">[[Article Title]]</code>.</li>
  <li>Bold the article title the first time it appears, e.g., <code class="language-plaintext highlighter-rouge">'''Article Title'''</code>.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">== Heading ==</code> syntax for section titles.</li>
</ul>

<h2 id="publishing-and-follow-up">Publishing and Follow-Up</h2>

<p>Once you have moved your article from the Sandbox to its real place, it is no longer your baby.
It lives its own life, and other people can contribute to it.</p>

<p><strong>Review your draft</strong></p>

<p>Double-check grammar, style, and references. Ensure your draft aligns with 
Wikipedia’s <a href="https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style">Manual of Style</a>.</p>

<p><strong>Move from sandbox to main space</strong></p>

<p>Once you’re confident, you can publish your draft. Be prepared for feedback and possible edits from the Wikipedia community.</p>

<p><strong>Monitor and respond</strong></p>

<p>Other editors may add, remove, or refine content. Engage politely on the article’s talk page if you need to discuss changes.</p>

<h2 id="why-contribute">Why Contribute?</h2>

<p><strong>Share knowledge</strong></p>

<p>If you notice a topic of general interest that isn’t on Wikipedia or is incomplete, adding your expertise benefits everyone.</p>

<p><strong>Strengthen Wikipedia</strong></p>

<p>By contributing high-quality articles, you help maintain and improve the world’s largest encyclopedia.</p>

<p><strong>Learn along the way</strong></p>

<p>Writing in Wikipedia’s style forces you to be concise, neutral, and well-referenced — excellent skills for any research-based writing.</p>

<h2 id="closing-thoughts">Closing Thoughts</h2>

<p>Contributing to Wikipedia was hard work, fun, rewarding, and informative.</p>

<p>In <em>The Divine Comedy</em> by Dante Alighieri, in part <em>Purgatorio</em>, Canto XV (15), Virgil explains to Dante:</p>

<blockquote>
  <p><em>That the more of any material thing one man has, the less of it there is for others; 
whereas <strong>the more peace or knowledge or love one man has, the more there is for all the others</strong>.</em></p>
</blockquote>

<p>Happy editing!</p>]]></content><author><name>Finn</name></author><category term="Technology" /><summary type="html"><![CDATA[How to Contribute to Wikipedia — My Experience]]></summary></entry><entry><title type="html">Record Links and Anti-patterns (Business Central)</title><link href="https://www.finnpedersenfrance.com/programming/2024/09/08/BC-RecordLinks.html" rel="alternate" type="text/html" title="Record Links and Anti-patterns (Business Central)" /><published>2024-09-08T07:00:00+02:00</published><updated>2025-09-05T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/programming/2024/09/08/BC-RecordLinks</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2024/09/08/BC-RecordLinks.html"><![CDATA[<h1 id="record-links-and-anti-patterns">Record Links and Anti-patterns</h1>

<blockquote>
  <p>The notes and links are disappearing. What is going on?</p>
</blockquote>

<p>Recently, I had to solve the above mystery for a customer. It made me discover some of the inner workings of Record Links and I thought I better share this knowledge before someone gets hurt.</p>

<p>The scenario is quite simple. Imagine a Sales Document. It has a primary key with two fields: Document Type and Number. <em>Classic.</em></p>

<p>When the document changes type from Quote to Order, in this case, it keeps its number and it is just the type that changes.</p>

<p>However, each time the notes and links of the document vanished.</p>

<blockquote>
  <p>Where did they go? Are they lost forever or can we get them back?</p>
</blockquote>

<h2 id="record-links">Record Links</h2>

<p>Notes and links are records in the same table: <code class="language-plaintext highlighter-rouge">Record Links</code>.</p>

<h3 id="anti-pattern">Anti-pattern</h3>
<p>Record Links is an anti-pattern because of this:</p>

<ul>
  <li>The table is shared across all companies.</li>
  <li>Instead, it has a field <code class="language-plaintext highlighter-rouge">Company</code> specifying which company the record belongs to.</li>
</ul>

<p>I ignore the origins of this design decision, but I suppose it could have been like any other table with DataPerCompany and consequently there would be no need for the Company field.</p>

<h3 id="fields">Fields</h3>

<p>The primary key is an integer and it is automatically incremented by the system.</p>

<p>The field <code class="language-plaintext highlighter-rouge">Record ID</code> is of the special type <code class="language-plaintext highlighter-rouge">RecordID</code>. This is a unique pointer to any record in the database independently of the table and company. I will show you how to find the original record further down.</p>

<h3 id="table-design">Table Design</h3>

<p>The following is a reduced version of the table, included so you don’t have to look for it.</p>

<p>You can find the table in the Microsoft System app.</p>

<pre><code class="language-al">table 2000000068 "Record Link"
{
    Caption = 'Record Link';
    DataPerCompany = false;
    ReplicateData = false;
    Scope = Cloud;
    InherentPermissions = rX;

    fields
    {
        field(1; "Link ID"; Integer)
        {
            AutoIncrement = true;
            Caption = 'Link ID';
        }
        field(2; "Record ID"; RecordID)
        {
            Caption = 'Record ID';
        }
        field(3; URL1; Text[2048])
        {
            Caption = 'URL1';
        }
        field(7; Description; Text[250])
        {
            Caption = 'Description';
        }
        field(8; Type; Option)
        {
            Caption = 'Type';
            OptionCaption = 'Link,Note';
            OptionMembers = Link,Note;
        }
        field(9; Note; BLOB)
        {
            Caption = 'Note';
            SubType = Memo;
        }
        field(12; Company; Text[30])
        {
            Caption = 'Company';
            TableRelation = System.Environment.Company.Name;
        }
    }

    keys
    {
        key(Key1; "Link ID")
        {
            Clustered = true;
        }
        key(Key2; "Record ID")
        {
        }
        key(Key3; Company, "Record ID")
        {
        }
    }
}
</code></pre>

<h3 id="page-design">Page Design</h3>

<p>If you want to make a page showing all the record links, you need the following code on the OnOpenPage trigger. Because you are only interested in seeing the record links for the current company.</p>

<pre><code class="language-al">trigger OnOpenPage()
begin
    Rec.SetRange(Company, Database.CompanyName());
end;
</code></pre>

<h3 id="function-design">Function Design</h3>

<h4 id="test-if-record-link-is-orphaned">Test if Record Link is Orphaned</h4>

<p>To test if a record link is orphaned, you need a function like this:</p>

<pre><code class="language-al">local procedure RecordLinkIsOrphaned(RecordLink: Record "Record Link"): Boolean
var
    RecRef: RecordRef;
begin
    if RecordLink.Company &lt;&gt; Database.CompanyName() then
        RecRef.ChangeCompany(RecordLink.Company);
    exit(not RecRef.Get(RecordLink."Record ID"));
end;
</code></pre>

<p>If you do not change the company, the <code class="language-plaintext highlighter-rouge">Get</code> will fail when the record is not in the current company.</p>

<h4 id="move-a-record-link-to-another-record">Move a Record Link to another record</h4>

<p>Assume that we have some kind of Sales Document as mentioned in the beginning.</p>

<p>In our scenario, the record links did not get moved to the new record.</p>

<p>Luckily, there is a standard codeunit to help us with this: <code class="language-plaintext highlighter-rouge">Record Link Management</code>.</p>

<pre><code class="language-al">procedure MoveRecordLink(FromSalesDocument: Record SalesDocument; ToSalesDocument: Record SalesDocument)
var
    LinkManagement: Codeunit "Record Link Management";
begin
    LinkManagement.CopyLinks(FromSalesDocument, ToSalesDocument);
    FromSalesDocument.DeleteLinks();
end;
</code></pre>

<p><strong>Warning: Do not try this in production</strong></p>

<p>My first version of the above function looked like this:</p>

<pre><code class="language-al">procedure MoveRecordLink(FromSalesDocument: Record SalesDocument; ToSalesDocument: Record SalesDocument)
var
    LinkManagement: Codeunit "Record Link Management";
begin
    LinkManagement.CopyLinks(FromSalesDocument, ToSalesDocument);
    LinkManagement.RemoveLinks(FromSalesDocument);
end;
</code></pre>

<p>Though this looks right, it is wrong. The following line will remove the record links on <strong>all</strong> records in the entire table, not just on the selected record.</p>

<pre><code class="language-al">    LinkManagement.RemoveLinks(FromSalesDocument); // DANGER
</code></pre>

<p>So, please, note that you have to use the following code to remove the links on a specific record.</p>

<pre><code class="language-al">    FromSalesDocument.DeleteLinks();
</code></pre>

<h2 id="deleting-a-record-link---behind-the-scenes">Deleting a Record Link - Behind the Scenes</h2>

<p>Let’s say, you have added a note to a document and then you delete the document. What happens to the note?</p>

<p>It depends on whether the OnDelete trigger is run or not.</p>

<p>If you delete the document from the UI, that is a Page, then the note is deleted automatically. 
If you delete the document from code, you have to explicitly tell the code to run the OnDelete trigger.</p>

<p>This will delete the note:</p>

<pre><code class="language-al">    SalesDocument.Delete(true);
</code></pre>

<p>This will not delete the note and the note will become an orphan.</p>

<pre><code class="language-al">    SalesDocument.Delete(false);
</code></pre>

<h3 id="anti-pattern-1">Anti-pattern</h3>

<p>Normally, at least in the past, we only ran the trigger, if there was code on it. <code class="language-plaintext highlighter-rouge">Rec.Delete(false);</code></p>

<p>Now, if you are deleting a record that has record links, but no code in the OnDelete trigger, you have to run it like this <code class="language-plaintext highlighter-rouge">Rec.Delete(true);</code>.</p>

<h3 id="removing-orphaned-links">Removing Orphaned Links</h3>

<p>There is a function for that and you can use it like this:</p>

<pre><code class="language-al">local procedure RemoveOrphanedLinks()
var
    LinkManagement: Codeunit "Record Link Management";
begin
    LinkManagement.RemoveOrphanedLinks();
end;
</code></pre>

<p>This will remove all orphaned links in the entire database in all companies.</p>

<h3 id="job-queue-entry">Job Queue Entry</h3>

<p>I have not quite understood the relation ship between error messages in Job Queue Entry and Record Links.</p>

<p>If you make a page displaying all Record Links in the company, you will see notes belonging to Job Queue Entries. This a bit strange. If you open the corresponding Job Queue Entry Card page and click <strong>Show Error</strong>, you will see the latest error. But in the Record Links there will be the entire history of errors.</p>

<p>If you know anything about this, let me know.</p>

<h2 id="back-to-our-mystery">Back to Our Mystery</h2>

<p>The origin of the disappearing links and notes were this code.</p>

<pre><code class="language-al">SalesDocument.Find();
SalesDocument.Delete();
SalesDocument.Copy(NewSalesDocument);
</code></pre>

<p>The good part of the story is that the programmer wrote <code class="language-plaintext highlighter-rouge">SalesDocument.Delete();</code> thereby making all the Record Links orphans and since only the document type changed, I was able to find and pair all Record Links with its parent again.</p>

<p>I fixed the code like this.</p>

<pre><code class="language-al">SalesDocument.Find();
MoveRecordLink(SalesDocument, NewSalesDocument);
SalesDocument.Delete();
SalesDocument.Copy(NewSalesDocument);
</code></pre>

<h2 id="when-anti-patterns-meet">When Anti-patterns Meet</h2>

<p>This was the story of what can happen when two anti-patters meet.</p>

<p>The story ended happily. The users got their links and notes back.</p>

<p>But it could have gone terribly wrong.</p>

<p>Anti-patterns are the opposite of best practice and they are bad for our brains. 
When we see something we first presume, it works as usual and we use our intuition to move forward quickly. 
When it doesn’t we have to analyze it and it will slow us down.</p>

<p>Message to all BC developers: <strong>Stick to best practices whenever you can and avoid anti-patterns.</strong></p>]]></content><author><name>Finn</name></author><category term="Programming" /><summary type="html"><![CDATA[Record Links and Anti-patterns]]></summary></entry><entry><title type="html">Breaking Changes in Version 26 (Business Central)</title><link href="https://www.finnpedersenfrance.com/programming/2024/03/21/breaking-changes-bc26.html" rel="alternate" type="text/html" title="Breaking Changes in Version 26 (Business Central)" /><published>2024-03-21T06:00:00+01:00</published><updated>2025-09-05T07:00:00+02:00</updated><id>https://www.finnpedersenfrance.com/programming/2024/03/21/breaking-changes-bc26</id><content type="html" xml:base="https://www.finnpedersenfrance.com/programming/2024/03/21/breaking-changes-bc26.html"><![CDATA[<h1 id="breaking-changes-are-coming-in-business-central-version-260">Breaking Changes are Coming in Business Central version 26.0</h1>

<p>Microsoft has announced their new upgrade strategy, where they explain that they need to clean up obsoleted fields and tables since version 12.</p>

<p>Version 25 is going to be a <strong>Jump Version</strong>.</p>

<p><img src="/assets/images-01/New-upgrade-strategy.png" alt="New Upgrade Strategy" /></p>

<p>Source: <a href="https://www.youtube.com/watch?v=Gwgpj1U1wxI&amp;list=WL&amp;index=10">Microsoft on YouTube: Introduction to Business Central cloud migration (2023)</a></p>

<h3 id="what-is-a-jump-version">What is a Jump Version?</h3>

<p>The following versions are Jump Versions.</p>

<ul>
  <li>Version 14.0 aka BC 2019 Spring</li>
  <li>Version 25.0 aka BC 2024 Wave 2 (arrives in October 2024)</li>
  <li><em>Version 30.0 aka BC 2027 Wave 1 (every 5 versions will be a jump version in the future.)</em></li>
</ul>

<p>You can upgrade directly to a Jump Version if you are on a previous version. 
But you have to stop on the Jump Version to get to the next one or higher.</p>

<p>This means that to upgrade to version 26.0 aka BC 2025 release wave 1, you will first have to upgrade to version 25, if you are on an earlier version.</p>

<h2 id="now-is-a-good-time-to-clean-up">Now is a good time to clean up</h2>

<p><strong>What does it all mean?</strong> It means that it is in your and your customers’ best interest to clean up, implement robustness and automated tests.</p>

<h3 id="automated-tests">Automated Tests</h3>

<p>We used to test when a new version was installed.
Now we need to automatically test business-critical functions and APIs frequently.</p>

<p>There has never been a better argument or a better time for getting started with automated tests.</p>

<p>In my <strong>Clean AL Code Initiative</strong> series, you can read more about
<a href="/programming/2024/01/03/Automated-Tests-in-AL.html">Automated Tests in AL</a></p>

<h3 id="robustness">Robustness</h3>

<p>We need to <strong>have built-in robustness</strong> in our customizations.
We need to <strong>require</strong> that our suppliers also have this <strong>robustness built-in</strong>.</p>

<p>In my <strong>Clean AL Code Initiative</strong> series, you can read more about 
<a href="/programming/2024/01/11/how-to-make-a-code-review.html">How to make a code review</a>
and how to make robust code in <a href="/programming/2024/01/12/TryFunctions-in-AL.html">Preconditions and TryFunctions in AL</a></p>

<h2 id="explain-it-and-sell-it-to-your-customers">Explain it and Sell it to your customers</h2>

<p>We told customers that they never have to go through upgrade hell again, but did we tell them that they now are <a href="/programming/2024/03/21/maintenance-mode.html">in maintenance mode</a>?</p>

<p><strong>We need to explain and sell this to our customers.</strong></p>]]></content><author><name>Finn</name></author><category term="Programming" /><summary type="html"><![CDATA[Breaking Changes are Coming in Business Central version 26.0]]></summary></entry></feed>