<?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>DEV Community: ane@agrivisser.com</title>
    <description>The latest articles on DEV Community by ane@agrivisser.com (@anevisser).</description>
    <link>https://dev.to/anevisser</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%2F3922326%2Fe20d7ad3-0de6-45ba-8c2c-cef2b410fdc4.png</url>
      <title>DEV Community: ane@agrivisser.com</title>
      <link>https://dev.to/anevisser</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anevisser"/>
    <language>en</language>
    <item>
      <title>I built a Kotlin DSL test automation framework because existing ones kept failing their own standards</title>
      <dc:creator>ane@agrivisser.com</dc:creator>
      <pubDate>Fri, 15 May 2026 23:23:55 +0000</pubDate>
      <link>https://dev.to/anevisser/i-built-a-kotlin-dsl-test-automation-framework-because-existing-ones-kept-failing-their-own-184n</link>
      <guid>https://dev.to/anevisser/i-built-a-kotlin-dsl-test-automation-framework-because-existing-ones-kept-failing-their-own-184n</guid>
      <description>&lt;p&gt;&lt;em&gt;Tags: #testing #kotlin #opensource #cicd&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Many test automation frameworks don't meet the engineering standards they're supposed to uphold.&lt;/p&gt;

&lt;p&gt;I say that having used several professionally. Slow execution, brittle locators, silent failures, opaque logic, dead code, documentation that contradicts itself, CI pipelines held together with workarounds, and — my personal favourite — a framework from a reputable company whose generated selectors were so long they wouldn't fit on a widescreen. Its tooling produced 95% noise. Core functions silently failed or behaved unpredictably.&lt;/p&gt;

&lt;p&gt;The cruel irony: frameworks built to enforce quality, failing their own quality bar.&lt;/p&gt;

&lt;p&gt;So I built my own. This is QED.&lt;/p&gt;




&lt;h2&gt;
  
  
  What QED is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;QED&lt;/strong&gt; is a Kotlin DSL test automation framework for UI and API testing — open source, MIT licensed, and built from scratch on SOLID principles. The name comes from &lt;em&gt;Quod Erat Demonstrandum&lt;/em&gt; — "that which was to be proven." Every test is a proof. Every run is evidence.&lt;/p&gt;

&lt;p&gt;The framework takes that idea into the reporting layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Passed — &lt;em&gt;Quod erat demonstrandum.&lt;/em&gt; Proven.&lt;/li&gt;
&lt;li&gt;⚠️ Skipped — &lt;em&gt;Quaestio manet.&lt;/em&gt; The question remains.&lt;/li&gt;
&lt;li&gt;❌ Failed — &lt;em&gt;Investigandum est.&lt;/em&gt; Further investigation is required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's built on top of tools you probably already know — Playwright, REST-assured, TestNG, and ExtentReports — but wraps them in expressive Kotlin DSLs that make tests read like intent rather than implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problems it solves
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Brittle page objects
&lt;/h3&gt;

&lt;p&gt;Most frameworks treat page objects as monoliths — tightly coupled, hard to reuse, duplicated across the suite. QED uses &lt;strong&gt;screen area composition&lt;/strong&gt;: pages are assembled from smaller, independently testable components (header, sidebar, modal, etc.). If a shared component changes, you fix it once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glue code hell
&lt;/h3&gt;

&lt;p&gt;Tools like Cucumber promise readable tests but often deliver fragmented logic split across step definitions, regex matchers, and external feature files. QED's DSL keeps the test logic in one place, expressed in the language of the domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opaque CI pipelines
&lt;/h3&gt;

&lt;p&gt;GitHub Actions configs have a way of growing into spaghetti. QED ships with a clean, documented pipeline structure — separate frontend and backend jobs, proper caching, and no redundant steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the DSL looks like
&lt;/h2&gt;

&lt;p&gt;Here's a mixed UI + API test — both in the same test context, sharing setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;baseTest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BaseTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;hasRest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HasRest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"apichallenges"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;hasBrowser&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HasBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"uitestingurl"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UI_RESTTest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TestContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasBrowser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasRest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mixed UI/API test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"All"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;testUI_Rest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;payroll&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Payroll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"create todo process payroll"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"todo description"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QEDJson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payroll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;landingPage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenericPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UITestingPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;hasBrowser&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;navigateToApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;startFromPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;landingPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RequestType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;APIChalURLPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SIM_ENTITIES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"check response body"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;asText&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bob"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;asInt&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test reads top to bottom. There's no ceremony. The &lt;code&gt;verify&lt;/code&gt; block is expressive enough that a non-engineer could follow the intent, but precise enough that an engineer can debug a failure without spelunking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-world use: DairyMax
&lt;/h2&gt;

&lt;p&gt;QED isn't a demo project. I use it to test &lt;strong&gt;DairyMax&lt;/strong&gt;, a production web application I'm building. The framework runs in a GitHub Actions pipeline on every push — UI tests in one job, API tests in another, with proper dependency management between them.&lt;/p&gt;

&lt;p&gt;That's meant one very practical constraint shaped the entire design: the framework has to work cleanly in CI, not just locally. Playwright browser setup, environment-specific SUT profiles, and test tagging (&lt;code&gt;groups = ["All"]&lt;/code&gt;, &lt;code&gt;groups = ["Smoke"]&lt;/code&gt;) all came out of that real-world friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qed-framework/
├── src/                   # Core framework
│   ├── base/              # BaseTest, TestContext
│   ├── browser/           # HasBrowser, Playwright wrappers
│   ├── rest/              # HasRest, REST-assured wrappers
│   ├── dsl/               # Verify, expect, page DSL
│   └── reporting/         # ExtentReports integration
├── QED-Shared/            # Shared utilities (submodule)
├── qed-demos/             # Example tests
└── .github/workflows/     # CI/CD pipelines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;HasBrowser&lt;/code&gt; and &lt;code&gt;HasRest&lt;/code&gt; capabilities are composed into a &lt;code&gt;TestContext&lt;/code&gt; — you only include what a given test class needs. A pure API test never touches Playwright. A UI test that doesn't need REST doesn't carry that weight.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The pipeline is split into two jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run API tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew test -Dgroups=API&lt;/span&gt;

  &lt;span class="na"&gt;ui-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-tests&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Playwright browsers&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew installPlaywright&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run UI tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew test -Dgroups=UI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UI tests run after API tests pass. Playwright browsers are installed as a dedicated step. Reports are archived as artifacts. Nothing clever, nothing fragile.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;QED is intentionally lean. I want to keep it that way — the modular architecture makes it straightforward to extend without bloating the core. On the roadmap: deeper SUT profile management, richer assertion matchers, and longer term, some interesting ideas around AI-assisted test generation I'm exploring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;strong&gt;GitHub (MIT):&lt;/strong&gt; &lt;a href="https://github.com/AneVisser/qed-framework" rel="noopener noreferrer"&gt;https://github.com/AneVisser/qed-framework&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Full documentation:&lt;/strong&gt; &lt;a href="https://anevisser.github.io/qed-framework/" rel="noopener noreferrer"&gt;https://anevisser.github.io/qed-framework/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The docs cover prerequisites, DSL usage, page object composition, REST setup, CI/CD configuration, and the design philosophy in detail. Feedback, issues, and stars all welcome.&lt;/p&gt;

&lt;p&gt;If you've built something similar or hit the same walls with existing frameworks — I'd genuinely like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>kotlin</category>
      <category>opensource</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
