<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Simanta Sarma</title>
    <description>The latest articles on Forem by Simanta Sarma (@ximanta).</description>
    <link>https://forem.com/ximanta</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2729284%2Ff79207b8-05c4-4bda-a726-98fa26d598e8.png</url>
      <title>Forem: Simanta Sarma</title>
      <link>https://forem.com/ximanta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ximanta"/>
    <language>en</language>
    <item>
      <title>Stop Lying to Your CI Pipeline</title>
      <dc:creator>Simanta Sarma</dc:creator>
      <pubDate>Fri, 13 Mar 2026 19:16:43 +0000</pubDate>
      <link>https://forem.com/ximanta/stop-lying-to-your-ci-pipeline-478l</link>
      <guid>https://forem.com/ximanta/stop-lying-to-your-ci-pipeline-478l</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of the "Stop Lying to Your Stack" series&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the age of AI-assisted development, a new service can be scaffolded in minutes. Spring Boot project structure, security configuration, database migrations, REST endpoints, everything generated, reviewed, and committed at blazing speed.&lt;/p&gt;

&lt;p&gt;But here is what I see teams consistently treat as an afterthought: &lt;strong&gt;The CI pipeline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It gets added last, often copied from another service in the same organisation, and rarely analysed. And almost never treated as the architectural artefact it actually is.&lt;/p&gt;

&lt;p&gt;The result is a pipeline that appears to work. Tests go green, PRs merge, deployments happen. But underneath, the pipeline is carrying assumptions that were never validated, permissions that were never justified, and coverage gaps that nobody noticed.&lt;/p&gt;

&lt;p&gt;This article is about what I have learned from setting up and reviewing PR verification pipelines for Spring Boot services, and the patterns that mature teams follow after they learn these lessons the hard way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Copying Pipelines
&lt;/h2&gt;

&lt;p&gt;When a new service spins up inside an existing organisation, the obvious move is to find the nearest sibling service and copy its workflow files. Same stack, same team conventions, same cloud provider.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most of the time, it mostly works. But "mostly works" is not the same as "correct."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What copying preserves is not just the decisions that were made deliberately. It also preserves the assumptions that were never questioned, the permissions that were granted lazily, the placeholders that were never updated, and the coverage gaps that nobody noticed because the tests still went green.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You should treat a CI workflow review the same way you treat a dependency audit. Read it line by line. Understand what each piece does. Ask whether it belongs for this specific service.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Test Before You Trust: Running CI Locally With &lt;code&gt;act&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before pushing a new workflow to a real repository and waiting for GitHub to run it, there is a better path. Run it locally.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nektosact.com/" rel="noopener noreferrer"&gt;act &lt;/a&gt;is an open-source tool by nektos that executes GitHub Actions workflows on your machine using Docker. The GitHub Actions runner environment is simulated inside a container. Steps execute in order, logs appear in your terminal, and you find the problems before the PR does.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The critical decision when using &lt;code&gt;act&lt;/code&gt; is which Docker image to use for the simulated &lt;code&gt;ubuntu-latest&lt;/code&gt; runner. There are three options, and the tradeoffs are real.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Micro (~200MB)&lt;/strong&gt;: Barely anything. Shell tools and not much else. Useful only for workflows with zero dependencies on pre-installed tooling. Not suitable for Maven builds or anything that touches Node.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Medium (~500MB to 1GB)&lt;/strong&gt;: A reasonable development environment: git, curl, Java, common build tools. Fast to pull. Low disk footprint. Suitable for many CI jobs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: What it does not have: Node.js and this catches teams off guard.&lt;/p&gt;

&lt;p&gt;A Spring Boot service with a React frontend built via &lt;code&gt;frontend-maven-plugin&lt;/code&gt; might seem like it handles Node internally, so Node being absent in the runner should not matter.&lt;/p&gt;

&lt;p&gt;In practice, the Maven plugin downloads its own Node runtime, but the download process depends on the network environment, SSL certificates, and system-level dependencies that may differ between the medium image and what GitHub's real runner provides.&lt;/p&gt;

&lt;p&gt;Hiccups happen. The build that works perfectly on the real runner can fail in the medium image in ways that are difficult to diagnose. So this sets up the justification for the third option.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Large (~17GB+)&lt;/strong&gt;: This mirrors GitHub's actual &lt;code&gt;ubuntu-latest&lt;/code&gt; runner almost exactly, Node is present, Docker is present. Every tool that GitHub pre-installs on its runners is there. What works locally with the large image will work in CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost is the size. Pulling 17GB once is acceptable, but storing it and keeping it is a commitment to disk space. On a developer workstation with limited storage, this is a real constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The practical guidance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are validating a new pipeline for the first time and want confidence that what you see locally reflects what CI will do, use the large image. The disk cost is a one-time investment.&lt;/p&gt;

&lt;p&gt;If you are making small iterative changes and have already validated the pipeline end-to-end, the medium image is fast enough for quick feedback on configuration changes, as long as you know its gaps.&lt;/p&gt;

&lt;p&gt;I advise to never use the micro image for a full Maven build with a frontend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Over-Permissioning: Not Just Bad Practice
&lt;/h2&gt;

&lt;p&gt;The most common issue in copied workflows is excessive permissions on the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;. This is easy to overlook because it does not break anything.&lt;/p&gt;

&lt;p&gt;The workflow passes. No error is thrown. The permissions are simply broader than necessary, and nobody notices until something goes wrong.&lt;/p&gt;

&lt;p&gt;What "goes wrong" in this context is worth understanding in concrete terms.&lt;/p&gt;

&lt;p&gt;A typical PR verification job in a copied workflow often looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permissions:
  actions: write
  packages: write
  contents: write
  checks: write
  pull-requests: write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these permissions is a real capability granted to a short-lived token that executes inside your build. Consider what each one enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;contents: write&lt;/code&gt; allows pushing commits and creating releases. A workflow step with this permission can commit code to your repository without a pull request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;packages: write&lt;/code&gt; allows pushing images to GitHub Container Registry under your organisation's namespace.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;actions: write&lt;/code&gt; allows cancelling workflow runs, re-running failed jobs, and modifying workflow dispatch inputs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;packages: write&lt;/code&gt; and &lt;code&gt;contents: write&lt;/code&gt; together on a PR build means that any compromised dependency in your Maven build, any malicious action that slips into your transitive chain, or any step that is manipulated via a malicious pull request from a fork has the credentials to write to your repository and push images to your registry.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the supply chain attack surface. It is not hypothetical. GitHub Actions supply chain attacks have occurred against real organisations. The mitigation is not complex detection tooling. It is the leat priviledge principle: simply not granting permissions that are not needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The correct permissions for a job that runs Maven tests and posts a PR comment are three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permissions:
  contents: read       # checkout requires this
  checks: write        # publish-report creates check run results
  pull-requests: write # publish-report posts PR comments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing more. The job functions identically and the attack surface is reduced to what is actually required.&lt;/p&gt;

&lt;p&gt;There is a deeper point here about how teams think about CI security. Most security attention in a repository goes toward the application code: dependency audits, SAST scanning, secret detection. The CI configuration itself is treated as infrastructure, not as code that runs with credentials. That asymmetry is exactly what attackers exploit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pin Actions to a Commit SHA, Not a Tag
&lt;/h2&gt;

&lt;p&gt;Related to over-permissioning is a practice that takes one extra minute to implement and significantly improves your pipeline's security posture.&lt;/p&gt;

&lt;p&gt;When a workflow references an action by a floating tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uses: actions/checkout@v6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the tag &lt;code&gt;v6&lt;/code&gt; is a mutable pointer. The action maintainer can move it to any commit at any time.&lt;/p&gt;

&lt;p&gt;If an attacker compromises the action repository and moves the tag to a commit containing malicious code, your workflow pulls and executes that code on the next CI run. You have not changed anything and your workflow file looks the same. But what executes is not what you reviewed.&lt;/p&gt;

&lt;p&gt;This is tag hijacking. It is a real attack vector.&lt;/p&gt;

&lt;p&gt;The defence is to pin to a specific commit SHA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reference is immutable. Moving the &lt;code&gt;v6&lt;/code&gt; tag has no effect on your workflow. The comment documents which version the SHA corresponds to, so the intent remains readable.&lt;/p&gt;

&lt;p&gt;Pinning also solves a stability problem. Maintainers occasionally move tags intentionally: a patch release gets tagged under the same major version, and &lt;code&gt;v6&lt;/code&gt; now points to a commit with different behaviour than it did yesterday.&lt;/p&gt;

&lt;p&gt;Teams discover this during post-incident reviews when CI behaviour changed without anyone modifying the workflow file. With a pinned SHA, your CI is stable. Changes to the action are opt-in, not automatic.&lt;/p&gt;

&lt;p&gt;The operational concern is keeping SHAs up to date. The answer is Dependabot. &lt;/p&gt;

&lt;p&gt;When configured for GitHub Actions, Dependabot automatically submits PRs to update pinned SHAs when new versions of actions are released. You get security reviews, a documented changelog, and a deliberate merge decision instead of silent auto-updates via mutable tags.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pinning to SHAs is a small change. It takes seconds per action. Teams that add it after their first pipeline setup rarely remove it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Missing Integration Test Report
&lt;/h2&gt;

&lt;p&gt;This is a simple oversight. Easy to miss. Easy to fix.&lt;/p&gt;

&lt;p&gt;Spring Boot services under Maven run two test categories: Surefire for unit tests, Failsafe for integration tests. A workflow that only reports Surefire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;report-path: '**/target/surefire-reports/TEST*.xml'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;silently discards integration test results. The tests run. They pass or fail. Nobody sees the outcome in the PR.&lt;/p&gt;

&lt;p&gt;The fix is a second report step pointing at Failsafe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;report-path: '**/target/failsafe-reports/TEST*.xml'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have seen this missed repeatedly because the integration tests rarely fail locally and the pipeline "works."&lt;/p&gt;

&lt;p&gt;When integration tests do fail in CI, engineers look at the job log, not the missing PR comment, and eventually find the failure. The gap becomes visible only when someone asks why there is no integration test summary on the PR.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Two report steps, two report paths. It takes two minutes to add and closes a coverage gap that has caused confusion on more than one team I have worked with.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Placeholders as Technical Debt Markers
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;turing85/publish-report&lt;/code&gt; action has a &lt;code&gt;comment-header&lt;/code&gt; field. Its purpose is to identify the comment uniquely across CI runs so the action can update the same comment rather than creating a new one each time.&lt;/p&gt;

&lt;p&gt;A value like &lt;code&gt;my-comment-header&lt;/code&gt; is the default placeholder from the documentation. It was never meant to stay.&lt;/p&gt;

&lt;p&gt;In a mature codebase, descriptive values are a must:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;comment-header: pr-unit-test-results
comment-header: pr-integration-test-results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not cosmetic. When two separate test report steps both use &lt;code&gt;my-comment-header&lt;/code&gt;, the second step overwrites the first. One report disappears. The PR shows a single comment that alternates between unit test results and integration test results depending on execution order.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Placeholders left in production configuration are a reliable signal. They tell you how the pipeline was built and whether it was reviewed after the initial setup. In a mature repository, there are none.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Patterns Mature Teams Add After the First Pipeline
&lt;/h2&gt;

&lt;p&gt;The patterns below are not prominently documented. They are things teams add after running a pipeline for a few weeks and observing what actually happens in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sticky PR Comments
&lt;/h3&gt;

&lt;p&gt;Without this, every CI run on a PR creates a new comment. Push five commits while fixing a failing test, and the PR fills with five separate test report comments. The PR timeline becomes noise.&lt;/p&gt;

&lt;p&gt;Add one line to each report step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- name: Publish Unit Test Report
  uses: turing85/publish-report@v2.3.1
  if: ${{ always() }}
  with:
    sticky-comment: true
    comment-header: pr-unit-test-results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first CI run creates the comment. Every subsequent run on the same PR updates it in place. The PR always shows one current test report per category, not a history of every run.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;comment-header&lt;/code&gt; value is what makes this work. It is the key the action uses to find and update the existing comment. This is another reason the placeholder value &lt;code&gt;my-comment-header&lt;/code&gt; is a problem: if multiple steps share the same header, the sticky logic collapses them into one comment instead of maintaining separate unit and integration test reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancel Redundant Runs
&lt;/h3&gt;

&lt;p&gt;When a developer pushes a commit, realises there is a typo, and pushes a second commit immediately, two CI runs start. The first run will never matter. It will complete, consume minutes from your CI quota, and potentially block the PR check until it finishes.&lt;/p&gt;

&lt;p&gt;Concurrency groups cancel the older run as soon as the newer one starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;concurrency:
  group: pr-${{ github.ref }}
  cancel-in-progress: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The group key is the branch reference. Two pushes to the same branch are in the same group. The second push cancels the first run automatically. On an active PR with multiple review cycles, this can save meaningful CI minutes and keeps the PR checks reflecting the current commit rather than a stale one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout Guards
&lt;/h3&gt;

&lt;p&gt;A test that hangs, like a database connection that never times out, a TestContainers startup that stalls will cause a CI job to run until GitHub's default job timeout of six hours. Six hours of consumed runner minutes, and a PR that appears to be running indefinitely.&lt;/p&gt;

&lt;p&gt;Add an explicit timeout at the job level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thirty minutes is generous for a Spring Boot build with integration tests. If the job exceeds it, it fails fast with a clear timeout message rather than hanging indefinitely. The failure is immediately diagnosable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual Trigger for Debugging
&lt;/h3&gt;

&lt;p&gt;Add &lt;code&gt;workflow_dispatch&lt;/code&gt; to the trigger block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;on:
  pull_request:
    branches:
      - main
  workflow_dispatch:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows any team member to trigger the workflow manually from the GitHub Actions UI without pushing a commit. Useful for debugging CI failures that do not reproduce locally, re-running after fixing a flaky external dependency, or validating a workflow change on a branch before raising a PR.&lt;/p&gt;

&lt;p&gt;It costs nothing to add and has saved debugging time more than once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Emoji Shortcodes Over Unicode
&lt;/h2&gt;

&lt;p&gt;A small decision that matters for rendering reliability.&lt;/p&gt;

&lt;p&gt;Using Unicode emoji directly in workflow YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;comment-message-success: "🥳 {0} passed | ✅ {1} | ❌ {2} | ⚠️ {3}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works most of the time. But Unicode emoji render inconsistently across markdown processors, email clients that deliver GitHub notifications, and Slack integrations that mirror PR comments. The rendering depends on the system font, the platform's emoji version, and the markdown parser in use.&lt;/p&gt;

&lt;p&gt;GitHub emoji shortcodes resolve through GitHub's own rendering engine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;comment-message-success: ":partying_face: {0} passed | :white_check_mark: {1} | :x: {2} | :warning: {3}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wherever GitHub Markdown is rendered, be it PR comments, check run summaries, email notifications, the shortcodes produce consistent output because GitHub controls the rendering. This is not a style preference. It is a rendering contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Know What Your Service Does Not Need
&lt;/h2&gt;

&lt;p&gt;Copying a workflow from a sibling service also copies its context. Environment variables referencing external APIs the new service does not call.&lt;/p&gt;

&lt;p&gt;Secrets that need to be provisioned even though they are never read. Setup steps that configure toolchains the service manages internally.&lt;/p&gt;

&lt;p&gt;The discipline here is subtraction. Before asking what to add, ask what in the copied workflow does not belong to this service.&lt;/p&gt;

&lt;p&gt;This is harder in an AI-assisted context because AI tooling is additive. It optimises for completeness and includes patterns from similar services because they are plausible.&lt;/p&gt;

&lt;p&gt;The engineer's role is to evaluate each inclusion critically. What does this env var point to? Does this service call that API? Is this setup step actually needed, or does the build tool handle it internally?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;None of the unnecessary inclusions break the build. They add weight: configuration that misleads future readers, secrets that need rotation, steps that consume time. In a payment processing service, clarity about what runs in the build environment is not optional.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Pattern&lt;/p&gt;&lt;/th&gt;
&lt;th colspan="1" rowspan="1"&gt;&lt;p&gt;Why It Matters&lt;/p&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Least-privilege &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permissions&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Reduces supply chain attack surface; each permission is a real credential capability&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Pin actions to commit SHA&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Prevents tag hijacking and silent behaviour changes from tag moves&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Separate surefire and failsafe reports&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Unit and integration failures signal different root causes; surface them separately&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Meaningful &lt;code&gt;comment-header&lt;/code&gt; values&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Required for sticky comments to work correctly; placeholders cause reports to overwrite each other&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;code&gt;sticky-comment: true&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Keeps PR timeline clean; one updated comment instead of one per CI run&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Concurrency group with cancel-in-progress&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Stops redundant runs when new commits arrive; saves CI minutes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;code&gt;timeout-minutes&lt;/code&gt; on jobs&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Prevents hanging tests from consuming hours of runner time&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt; trigger&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Enables manual re-runs for debugging without requiring a commit push&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;GitHub emoji shortcodes over Unicode&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Consistent rendering across PR comments, email, and Slack integrations&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Remove irrelevant env vars and setup steps&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Keep the pipeline aligned with what the service actually does&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The conversation in software engineering has shifted. Velocity is no longer the constraint. AI handles the implementation baseline quickly. What remains scarce is judgment. Knowing what to build, what to include, and what each decision does to the system at runtime and in production.&lt;/p&gt;

&lt;p&gt;A CI pipeline is a small system. It holds credentials. It runs code. It produces the signals your team acts on after every pull request. Treating it with the same care you give to a service's security configuration or schema design is not over-engineering.&lt;/p&gt;

&lt;p&gt;It is engineering.&lt;/p&gt;

&lt;p&gt;Set it up correctly once. Every pull request after that benefits from it.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;If this resonated, share it with a teammate who has a &lt;code&gt;my-comment-header&lt;/code&gt; somewhere in production. The conversation is worth having before the supply chain review, not after.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Coming up more in the "Stop Lying to Your Stack" series.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>githubactions</category>
      <category>ci</category>
    </item>
    <item>
      <title>Stop Lying to Your Tests: Real Infrastructure Testing with Testcontainers (Spring Boot 4)</title>
      <dc:creator>Simanta Sarma</dc:creator>
      <pubDate>Sat, 07 Mar 2026 15:29:49 +0000</pubDate>
      <link>https://forem.com/ximanta/stop-lying-to-your-tests-real-infrastructure-testing-with-testcontainers-spring-boot-4-pl9</link>
      <guid>https://forem.com/ximanta/stop-lying-to-your-tests-real-infrastructure-testing-with-testcontainers-spring-boot-4-pl9</guid>
      <description>&lt;p&gt;In the age of AI-assisted development, writing code has never been faster.&lt;br&gt;
But here is the uncomfortable truth that most teams discover only after production incidents:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI generates implementations. It cannot decide what your tests should verify.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That responsibility of knowing what to test, with what fidelity, and why belongs to engineers with platform knowledge and architectural judgment. Nowhere is this more visible than in integration testing.&lt;/p&gt;

&lt;p&gt;This article is about a shift I see as non-negotiable for teams building reliable backend services in 2026.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Deceptive Comfort of H2
&lt;/h2&gt;

&lt;p&gt;For years, the default Spring Boot integration testing pattern looked like this.&lt;/p&gt;

&lt;p&gt;Add H2 to the classpath, point your tests at an in-memory database, and tests run fast. No Docker. No external dependencies. Everything green.&lt;/p&gt;

&lt;p&gt;The problem is that H2 is not your production database.&lt;/p&gt;

&lt;p&gt;It does not enforce the same constraint semantics. It does not support all SQL constructs your migrations use. It does not run your schema through the same type coercion rules. And when your Flyway migrations grow in complexity, partial indexes, custom check constraints, advisory locks, partitioned tables, H2 quietly silences the errors that PostgreSQL would immediately surface.&lt;/p&gt;

&lt;p&gt;You are not testing your system. You are testing a shadow of it.&lt;/p&gt;

&lt;p&gt;I have seen services pass hundreds of H2-backed integration tests and then fail their first production deployment because a Flyway migration had a PostgreSQL-specific syntax that H2 accepted without complaint.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Architectural Shift: Real Infrastructure, Every Run
&lt;/h2&gt;

&lt;p&gt;The shift I advocate is straightforward in principle but meaningful in execution.&lt;/p&gt;

&lt;p&gt;Instead of this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration Test → H2 In-Memory → Schema (simplified)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every integration test run connects to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration Test → PostgreSQL Container (Testcontainers) → Real Flyway Migrations → Real Schema&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Testcontainers starts a real PostgreSQL instance inside Docker for each test suite. Your Flyway migrations run exactly as they would in production. Hibernate validates the resulting schema. If any migration has a flaw, the test suite fails before a single test method executes.&lt;/p&gt;

&lt;p&gt;This is not just better testing. It is a feedback loop that catches infrastructure regressions within seconds of committing code.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Abstract Base Class Pattern
&lt;/h2&gt;

&lt;p&gt;The architecture decision I consistently make in Spring Boot services is to centralize the Testcontainers setup in a single abstract base class that all integration tests extend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AbstractContainerIT  (base)
       │
       ├── OrderContextIT        @Order(1)  — smoke: context loads, health endpoint
       ├── MerchantApiIT         @Order(2)  — merchant CRUD, pagination
       ├── TransactionFlowIT     @Order(3)  — auth + capture + settlement
       └── SettlementBatchIT     @Order(4)  — batch close, ordering-dependent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One container. One Spring context. Shared across all test classes in the JVM.&lt;/p&gt;

&lt;p&gt;The base class declares the container as &lt;code&gt;public static final&lt;/code&gt;, meaning the PostgreSQL container starts once and is reused. This is the key performance decision. Without it, you pay the container startup cost for every test class — which on a large service can add minutes to your test suite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PostgreSQLContainer&lt;/span&gt; &lt;span class="n"&gt;postgresContainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PostgreSQLContainer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres:17"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDatabaseName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"service_test"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"testuser"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"testpass"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withReuse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.withReuse(true)&lt;/code&gt; goes one step further — on a developer's local machine, Testcontainers will reuse the already-running container from a previous test run. The first run takes 3–5 seconds to start the container. Every subsequent run is instant. This matters enormously for developer experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;@DynamicPropertySource&lt;/code&gt;: The Right Hook for Dynamic Configuration
&lt;/h2&gt;

&lt;p&gt;A critical implementation decision is where you start the container and how you wire its dynamic properties, like JDBC URL, username, password into the Spring &lt;code&gt;Environment&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The correct approach is &lt;code&gt;@DynamicPropertySource&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@DynamicPropertySource&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;configureProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DynamicPropertyRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;postgresContainer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spring.datasource.url"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;postgresContainer:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getJdbcUrl&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spring.datasource.username"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;postgresContainer:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUsername&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spring.datasource.password"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;postgresContainer:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getPassword&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting the container here, inside the &lt;code&gt;@DynamicPropertySource&lt;/code&gt; method, guarantees the container is running before Spring assembles its &lt;code&gt;Environment&lt;/code&gt;. The properties Spring reads to configure the &lt;code&gt;DataSource&lt;/code&gt; bean come directly from the live container.&lt;/p&gt;

&lt;p&gt;Some implementations use a &lt;code&gt;static {}&lt;/code&gt; block to start the container, which also works mechanically, but &lt;code&gt;@DynamicPropertySource&lt;/code&gt; is a Spring Test first-class hook, integrates cleanly with context caching, and makes the intent explicit the container lifecycle and property contribution are co-located.&lt;/p&gt;

&lt;p&gt;This is a small decision with architectural implications: when the codebase has 50 integration test classes, clarity about lifecycle order prevents subtle startup failures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test Profile Isolation
&lt;/h2&gt;

&lt;p&gt;A pattern I enforce consistently: integration tests must not depend on the main application configuration to be runnable.&lt;/p&gt;

&lt;p&gt;In practice, this means creating a dedicated test profile that overrides environment-specific settings. For services that are OAuth2 resource servers, this typically means overriding the JWT issuer configuration:&lt;/p&gt;

&lt;p&gt;The main application configuration points to a live authorization server for OIDC discovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spring.security.oauth2.resourceserver.jwt.issuer-uri=http://auth-server:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the Spring Boot test context starts, it attempts to contact this URL to fetch &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;. In CI and in local development when the auth server is not running this fails immediately and the entire test suite crashes before any test runs.&lt;/p&gt;

&lt;p&gt;The fix is a test profile that switches from discovery to a static JWK set URI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# application-test-postgres.yaml&lt;/span&gt;
&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;oauth2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resourceserver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;issuer-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~&lt;/span&gt;                        &lt;span class="c1"&gt;# disables OIDC discovery&lt;/span&gt;
          &lt;span class="na"&gt;jwk-set-uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth-server:8080/oauth2/jwks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test context still configures OAuth2 correctly. JWT validation logic still works. But the context no longer depends on a live external service at startup time.&lt;/p&gt;

&lt;p&gt;This is a non-obvious but essential step. A test suite that cannot start in an isolated environment is not a reliable test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dependency Hygiene: What You Include Shapes Your Schema
&lt;/h2&gt;

&lt;p&gt;This is the insight that trips most teams, and it has become more important in the AI era because AI tooling tends to include dependencies liberally.&lt;/p&gt;

&lt;p&gt;In Spring Boot 4 (and Spring Modulith), some dependencies register JPA entities automatically the moment they appear on the classpath. No configuration required. No opt-in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spring-modulith-starter-jpa&lt;/code&gt;, for example, registers &lt;code&gt;event_publication&lt;/code&gt; and &lt;code&gt;event_publication_archive&lt;/code&gt; as JPA entities. If you run with &lt;code&gt;spring.jpa.hibernate.ddl-auto=validate&lt;/code&gt; — which you should in integration tests — Hibernate will fail at startup with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Schema validation: missing table [event_publication]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is entirely correct behavior. Hibernate is doing its job. But if your service has not yet implemented any async cross-module events (the reason you would include that dependency), including it prematurely causes every test run to fail until you either add the DDL or remove the dependency.&lt;/p&gt;

&lt;p&gt;The architectural principle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Include a dependency when you need its capability. Not before.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a stateless JWT-authenticated API gateway, &lt;code&gt;spring-session-jdbc&lt;/code&gt; has no place. It is designed for server-side session persistence. The exact opposite of stateless token authentication. Including it because a similar project had it is a maintenance liability.&lt;/p&gt;

&lt;p&gt;These are not "pom.xml hygiene" decisions. They are architectural decisions about what your service does and does not need, with direct consequences for your schema, your startup time, and your test reliability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Spring Boot 4: Test Starters Are Now Decomposed
&lt;/h2&gt;

&lt;p&gt;This is a concrete migration point worth noting as teams move to Spring Boot 4.&lt;/p&gt;

&lt;p&gt;In Spring Boot 3, &lt;code&gt;spring-boot-starter-test&lt;/code&gt; included testing support for the full web stack. In Spring Boot 4, web MVC testing was extracted into a separate starter.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@AutoConfigureMockMvc&lt;/code&gt; moved to a new package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spring Boot 3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Spring Boot 4&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And requires an additional dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-webmvc-test&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;test&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you upgrade to Spring Boot 4 and see &lt;code&gt;@AutoConfigureMockMvc&lt;/code&gt; unresolved in your IDE, this is the reason. It is not a bug, it is intentional decomposition that gives teams finer-grained control over test dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;@SpringBootTest&lt;/code&gt; vs &lt;code&gt;RANDOM_PORT&lt;/code&gt; — The Context Cache Question
&lt;/h2&gt;

&lt;p&gt;A common question when setting up integration tests: should you use &lt;code&gt;@SpringBootTest&lt;/code&gt; (default MOCK environment) or &lt;code&gt;@SpringBootTest(webEnvironment = RANDOM_PORT)&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;For REST API integration tests, the MOCK environment with &lt;code&gt;MockMvc&lt;/code&gt; is almost always the better choice. Here is why.&lt;/p&gt;

&lt;p&gt;Spring caches application contexts across test classes. If two test classes share the same context configuration — same properties, same beans, same profiles — they reuse the same context. This means Flyway runs once, the connection pool initializes once, and all subsequent test classes start in milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RANDOM_PORT&lt;/code&gt; allocates a real HTTP port per test run. The variation in port introduces subtle context cache mismatches. You can end up paying the full context startup cost more often than necessary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MockMvc&lt;/code&gt; performs full request dispatch through the DispatcherServlet, invoking all filters, interceptors, and exception handlers. For the vast majority of REST API tests, it provides identical verification coverage at lower cost.&lt;/p&gt;

&lt;p&gt;Reserve &lt;code&gt;RANDOM_PORT&lt;/code&gt; for tests that specifically require real HTTP semantics — for example, testing WebSocket upgrades, chunked transfer encoding, or HTTP/2 server push.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Integration Test: Smoke and Actuator
&lt;/h2&gt;

&lt;p&gt;The first integration test I write for every service is a smoke test with exactly two assertions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. The Spring context loads successfully.
2. GET /actuator/health returns HTTP 200.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is deliberately minimal. Its purpose is not to test business logic. Its purpose is to verify that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All Spring beans wire up without circular dependencies or missing configurations&lt;/li&gt;
&lt;li&gt;Flyway migrations execute cleanly against the real PostgreSQL schema&lt;/li&gt;
&lt;li&gt;Hibernate validates the schema successfully&lt;/li&gt;
&lt;li&gt;The Actuator health endpoint responds — confirming the full HTTP pipeline is operational&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this test passes, the infrastructure is solid. Every subsequent integration test builds on that foundation.&lt;/p&gt;

&lt;p&gt;When this test fails, the failure message immediately tells you which layer broke — bean creation, migration, schema validation, or HTTP. That specificity is what makes it valuable in CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  What AI Changes — and What It Does Not
&lt;/h2&gt;

&lt;p&gt;AI tools in 2026 can generate a complete integration test class, wire up Testcontainers, and scaffold the Spring Boot configuration in seconds. This is genuinely useful.&lt;/p&gt;

&lt;p&gt;What AI cannot do is make the architectural decisions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should this service use a real database or H2 for tests? (Real database.)&lt;/li&gt;
&lt;li&gt;Should the container start in a static block or &lt;code&gt;@DynamicPropertySource&lt;/code&gt;? (The Spring-idiomatic hook.)&lt;/li&gt;
&lt;li&gt;Should &lt;code&gt;spring-session-jdbc&lt;/code&gt; be included in a stateless API gateway? (No.)&lt;/li&gt;
&lt;li&gt;Will &lt;code&gt;spring-modulith-starter-jpa&lt;/code&gt; fail your Hibernate validation before you have the DDL? (Yes.)&lt;/li&gt;
&lt;li&gt;Does &lt;code&gt;RANDOM_PORT&lt;/code&gt; break context caching in your specific test setup? (Depends on your configuration.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These decisions require knowledge of the platform, the architecture, and the specific service's contract. They are not implementation details. They are engineering judgment.&lt;/p&gt;

&lt;p&gt;In the AI era, the engineers who deliver reliably are not the ones who write the most code. They are the ones who ask the right questions about what to build, what to include, and what each piece does to the system.&lt;/p&gt;

&lt;p&gt;Testing infrastructure is not ceremony. It is the foundation that tells you, every time you push code, whether what you built actually works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database for integration tests&lt;/td&gt;
&lt;td&gt;Real PostgreSQL via Testcontainers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container lifecycle&lt;/td&gt;
&lt;td&gt;Start inside &lt;code&gt;@DynamicPropertySource&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container reuse&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;public static final&lt;/code&gt; + &lt;code&gt;.withReuse(true)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test profile&lt;/td&gt;
&lt;td&gt;Dedicated profile; override all external service URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web environment&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@SpringBootTest&lt;/code&gt; (MOCK) + &lt;code&gt;MockMvc&lt;/code&gt; for REST APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency hygiene&lt;/td&gt;
&lt;td&gt;Include only what the service currently needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First test&lt;/td&gt;
&lt;td&gt;Smoke: context loads + &lt;code&gt;/actuator/health&lt;/code&gt; returns 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot 4&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;spring-boot-starter-webmvc-test&lt;/code&gt; for &lt;code&gt;@AutoConfigureMockMvc&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The conversation in software engineering has shifted. Velocity is no longer the constraint. AI handles the implementation baseline quickly. What remains scarce is architectural clarity: knowing which tools belong in your system, what they do to your runtime behavior, and how to structure test infrastructure that provides real confidence rather than false reassurance.&lt;/p&gt;

&lt;p&gt;Getting integration testing right is not a detail. It is the foundation that allows teams to move fast with high confidence, catch infrastructure regressions before production, and build systems that behave predictably under change.&lt;/p&gt;

&lt;p&gt;Build the foundation correctly once. Everything built on top of it benefits permanently.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this was useful, share it with a teammate who is wrestling with flaky integration tests or an H2-versus-real-database debate. The conversation is worth having early.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>integrationtest</category>
    </item>
    <item>
      <title>Org Analyzer: Your Company Research Buddy</title>
      <dc:creator>Simanta Sarma</dc:creator>
      <pubDate>Fri, 24 Jan 2025 10:53:24 +0000</pubDate>
      <link>https://forem.com/ximanta/org-analyzer-your-company-research-buddy-2441</link>
      <guid>https://forem.com/ximanta/org-analyzer-your-company-research-buddy-2441</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://srv.buysellads.com/ads/long/x/T6EK3TDFTTTTTT6WWB6C5TTTTTTGBRAPKATTTTTTWTFVT7YTTTTTTKPPKJFH4LJNPYYNNSZL2QLCE2DPPQVCEI45GHBT" rel="noopener noreferrer"&gt;Agent.ai&lt;/a&gt; Challenge: Productivity-Pro Agent (&lt;a href="https://dev.to/challenges/agentai"&gt;See Details&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 What is Org Analyzer?
&lt;/h2&gt;

&lt;p&gt;Org Analyzer is your ultimate digital research companion for decision-making, transforming complex company research into instant, actionable insights. With a single input—just the &lt;strong&gt;company name&lt;/strong&gt;—, unlock an intelligence report for any organization. &lt;/p&gt;

&lt;p&gt;From uncovering its services and offerings, locations, and finances to understanding its competitive landscape, Org Analyzer empowers you to strategize effectively with precision insights.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Built Org Analyzer 🤔
&lt;/h3&gt;

&lt;p&gt;In today’s fast-paced world, accessing accurate, detailed, and actionable company information quickly is critical for any professionals, be it decision makers, managers, marketers, or job seekers**. &lt;/p&gt;

&lt;p&gt;I built Org Analyzer to solve these challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eliminate time-consuming manual research&lt;/li&gt;
&lt;li&gt;Leverage Gen AI to aggregate and provide insights from multiple sources &lt;/li&gt;
&lt;li&gt;Provide cost-effective, accurate intelligence&lt;/li&gt;
&lt;li&gt;Deliver comprehensive company profiles instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Vision:&lt;/strong&gt; A &lt;strong&gt;one-click solution&lt;/strong&gt; to access holistic organizational intelligence.&lt;/p&gt;

&lt;h3&gt;
  
  
  😊 What Insights Do I Get?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;📋 Company Overview:&lt;/strong&gt; Profile of the organization, including its mission, history, and key achievements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📍 Locations:&lt;/strong&gt; Discover where the company operates globally, from headquarters to regional offices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🛠️ Products &amp;amp; Services:&lt;/strong&gt; The organization's offerings and what makes them unique.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🤝 Clients &amp;amp; Customers:&lt;/strong&gt; The company's customer base and key industries served.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💰 Financial Health:&lt;/strong&gt; Key financial data, including revenue, profits, and growth metrics (if publicly available).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🏆 Competitor Analysis:&lt;/strong&gt; Who the company is up against in its market and industry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;👩‍💼 Leadership Insights:&lt;/strong&gt; Dive into the profiles of the management team and executive board members.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🌟 Use Cases for Org Analyzer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📈 Market Research:&lt;/strong&gt; Analyze competitors and identify industry trends for strategic planning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💼 Job Seekers:&lt;/strong&gt; Prepare for interviews with detailed knowledge about potential employers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🏢 Business Development:&lt;/strong&gt; Understand potential clients or partners for targeted outreach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💡 Investors:&lt;/strong&gt; Evaluate company financials and competitive positioning before making investment decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📊 Sales Teams:&lt;/strong&gt; Personalize pitches with insights into client profiles and pain points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🧠 Strategic Planners:&lt;/strong&gt; Assess organizational landscapes to uncover growth opportunities.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Org Analyzer&lt;/strong&gt;: &lt;a href="https://agent.ai/agent/organalyzer" rel="noopener noreferrer"&gt;https://agent.ai/agent/organalyzer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6j3exgelhz5214a964a9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6j3exgelhz5214a964a9.png" alt="Org Analyzer Input" width="800" height="345"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 01: Org Analyzer: User Input&lt;/em&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Agent.ai Experience
&lt;/h2&gt;

&lt;p&gt;As a tech professional, I found Agent.ai to be a game-changing platform in the Agentic AI ecosystem. The ease of experimentation and rich feature set made agent creation remarkably straightforward.&lt;/p&gt;

&lt;p&gt;Here’s why I enjoyed working with this platform and how it made the development process seamless:&lt;/p&gt;

&lt;h4&gt;
  
  
  🛠️ &lt;strong&gt;Powerful LLM Support&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;The platform offers an impressive array of &lt;strong&gt;Language Model (LLM) support&lt;/strong&gt;, making it easy to experiment with different models for the best possible results. This flexibility was key to fine-tuning the performance of Org Analyzer.&lt;/p&gt;

&lt;h4&gt;
  
  
  🎨 &lt;strong&gt;Intuitive User Interface&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Agent.ai's &lt;strong&gt;intuitive UI&lt;/strong&gt; and &lt;strong&gt;user-friendly experience&lt;/strong&gt; made it a breeze to design and prototype the agent. I was able to go from concept to a working prototype faster than I imagined.&lt;/p&gt;

&lt;h4&gt;
  
  
  🤖 &lt;strong&gt;Multi-Agent Capability&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;One of the standout features of the platform is its &lt;strong&gt;multi-agent capability&lt;/strong&gt;. This enabled me to explore complex workflows and interactions effortlessly. It’s exciting to see the potential for creating more interconnected, intelligent agents.&lt;/p&gt;

&lt;h4&gt;
  
  
  🔗 &lt;strong&gt;Webhook &amp;amp; API Calling&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;The built-in &lt;strong&gt;webhook and API integration&lt;/strong&gt; features are incredibly powerful. They open up endless possibilities for building agents that can seamlessly interact with external systems and data sources.&lt;/p&gt;

&lt;h4&gt;
  
  
  📚 &lt;strong&gt;Room for Growth&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;While I found the platform easy to navigate as a tech-savvy user, I noticed that more comprehensive &lt;strong&gt;documentation for non-technical users&lt;/strong&gt; could make it accessible to a broader audience. That said, the platform's design and features are intuitive enough to explore and learn quickly.&lt;/p&gt;

&lt;h4&gt;
  
  
  🔮 &lt;strong&gt;Excited About the Future&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Agent.ai embodies the essence of the &lt;strong&gt;Agentic AI era&lt;/strong&gt;, providing tools that allow developers like me to create agents that are not only functional but also innovative. I’m thrilled to continue leveraging this platform for future projects and experiments.&lt;/p&gt;

&lt;p&gt;Kudos to the team behind this fantastic platform!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎯 Pro Tip:&lt;/strong&gt; Share your feedback and suggestions to make Org Analyzer even better. Your insights are the fuel for innovation!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>agentaichallenge</category>
      <category>productivity</category>
      <category>promptengineering</category>
    </item>
  </channel>
</rss>
