DEV Community

ane@agrivisser.com
ane@agrivisser.com

Posted on

I built a Kotlin DSL test automation framework because existing ones kept failing their own standards

Tags: #testing #kotlin #opensource #cicd


Many test automation frameworks don't meet the engineering standards they're supposed to uphold.

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.

The cruel irony: frameworks built to enforce quality, failing their own quality bar.

So I built my own. This is QED.


What QED is

QED 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 Quod Erat Demonstrandum — "that which was to be proven." Every test is a proof. Every run is evidence.

The framework takes that idea into the reporting layer:

  • ✅ Passed — Quod erat demonstrandum. Proven.
  • ⚠️ Skipped — Quaestio manet. The question remains.
  • ❌ Failed — Investigandum est. Further investigation is required.

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.


The problems it solves

Brittle page objects

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

Glue code hell

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.

Opaque CI pipelines

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.


What the DSL looks like

Here's a mixed UI + API test — both in the same test context, sharing setup:

private val baseTest = BaseTest()
private val hasRest = HasRest(baseTest, urlKey = "apichallenges")
private val hasBrowser = HasBrowser(baseTest, urlKey = "uitestingurl")

class UI_RESTTest : TestContext(baseTest, hasBrowser, hasRest) {

    @Test(priority = 0, description = "mixed UI/API test", groups = ["All"])
    fun testUI_Rest() {
        val payroll = Payroll("create todo process payroll", true, "todo description")
        val json = QEDJson.toJson(payroll)
        val landingPage = GenericPage(UITestingPage(this))

        hasBrowser?.apply {
            navigateToApp()
            startFromPage(landingPage) {
                val result = rest.send(RequestType.POST, APIChalURLPath.SIM_ENTITIES, json, 201)
                verify("check response body") {
                    expect(result.get("name").asText()).to.equal("bob")
                    expect(result.get("id").asInt()).to.equal(11)
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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


Real-world use: DairyMax

QED isn't a demo project. I use it to test DairyMax, 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.

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 (groups = ["All"], groups = ["Smoke"]) all came out of that real-world friction.


Architecture overview

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
Enter fullscreen mode Exit fullscreen mode

The HasBrowser and HasRest capabilities are composed into a TestContext — 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.


CI/CD with GitHub Actions

The pipeline is split into two jobs:

jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
      - name: Run API tests
        run: ./gradlew test -Dgroups=API

  ui-tests:
    runs-on: ubuntu-latest
    needs: api-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
      - name: Install Playwright browsers
        run: ./gradlew installPlaywright
      - name: Run UI tests
        run: ./gradlew test -Dgroups=UI
Enter fullscreen mode Exit fullscreen mode

UI tests run after API tests pass. Playwright browsers are installed as a dedicated step. Reports are archived as artifacts. Nothing clever, nothing fragile.


What's next

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.


Try it

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.

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.

Top comments (0)