Custom Test Suites¶
The qed-demos project serves as the reference implementation for creating your own SUT repositories. Follow these steps to set up a new test project.
Step 1: Create the Directory Structure¶
Create a new directory alongside the framework:
C:\QEDFramework\qed-sut-myapp\
├── settings.gradle.kts
├── build.gradle.kts
├── resources\
│ └── fonts\
│ └── montserrat-v31-latin-regular.woff2 ← copy from framework
└── src\
├── main\
│ └── kotlin\
│ └── (page objects, data classes, URL paths, etc.)
└── test\
└── kotlin\
└── myapp\
├── myapp.xml ← TestNG suite definition
├── myapp-config.json ← test run configuration
└── testcases\
└── (test classes)
The test subdirectory name, XML filename, and config JSON filename should all match the value passed to
-Ptestsuite.
Step 2: Create settings.gradle.kts¶
rootProject.name = "qed-sut-myapp"
// ── Composite build ──────────────────────────────────────────────────
// Adjust the path to match your local directory layout.
//
includeBuild("../QEDFramework")
// Include QED-Shared if your project uses shared data classes
// includeBuild("../QEDFramework/QED-Shared")
Composite builds do not chain transitively. Every SUT repo that depends on the framework must also explicitly include
QED-Sharedif it uses shared types.
Step 3: Create build.gradle.kts¶
plugins {
kotlin("jvm") version "2.0.20"
}
group = "com.qed.sut"
version = "1.0.0"
repositories {
mavenCentral()
}
kotlin {
jvmToolchain(22)
}
dependencies {
// QED Framework (resolved via composite build)
implementation("com.qed:qed-framework:1.0.0")
testImplementation("org.jetbrains.kotlin:kotlin-test")
// Add any project-specific dependencies here
}
// ── Test suite selection ─────────────────────────────────────────────
// Usage: ./gradlew clean test -Ptestsuite=myapp -Penvironment=dev
//
val env: String = project.findProperty("environment") as? String ?: "dev"
tasks.withType<Test> {
if (project.hasProperty("testsuite")) {
val testSuite = project.property("testsuite") as String
useTestNG {
useDefaultListeners = false
suites("src/test/kotlin/$testSuite/$testSuite.xml")
}
}
systemProperty("env.name", env)
testLogging {
events("passed", "skipped", "failed")
}
}
tasks.register<Copy>("copyExtentFonts") {
from("resources/fonts")
into(layout.buildDirectory.dir("test-output/ExtentReport/fonts"))
}
tasks.named("test") {
finalizedBy("copyExtentFonts")
}
Don't forget to register this Gradle project in IntelliJ as described in Installation, Step 4.
Step 4: Create the TestNG Suite XML¶
Create src/test/kotlin/myapp/myapp.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="My App" verbose="10" parallel="methods" thread-count="5">
<parameter name="configfile"
value="src/test/kotlin/myapp/myapp-config.json"/>
<test name="My Test Suite">
<classes>
<class name="testcases.MyFirstTest"/>
</classes>
</test>
</suite>
Step 5: Create the Config JSON¶
Create src/test/kotlin/myapp/myapp-config.json:
{
"testrunmetadata": {
"projectName": "My App",
"environment": "dev",
"baseURL": "https://your-app-url.com"
}
}
Step 6: Create a .gitignore¶
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
.kotlin/
.idea/
*.iml
out/
hs_err_pid*.log
replay_pid*.log
.DS_Store
Thumbs.db
test-output/
Step 7: Write Your First Test¶
Create a page object in src/main/kotlin/pages/:
package pages
import qed.basepage.BasePage
import com.microsoft.playwright.Page
class MyLoginPage(page: Page) : BasePage(page) {
// Define your page elements and actions
}
Create a test in src/test/kotlin/myapp/testcases/:
package testcases
import org.testng.annotations.Test
import qed.testbaseclass.BaseTest
class MyFirstTest : BaseTest() {
@Test
fun verifyLoginPage() {
// Your test logic here
}
}
Step 8: Open in IntelliJ and Run¶
- Open the SUT directory as a separate IntelliJ project (
File → Open → New Window) - Wait for Gradle sync to complete
- Set up a run configuration:
- Run:
clean test -Ptestsuite=myapp -Penvironment=dev - Gradle project: point to your SUT directory
- Run:
- Run the suite
Step 9: Push to GitHub¶
- Create a new private repository on GitHub (empty, no README)
- Initialise and push:
cd C:\QEDFramework\qed-sut-myapp
git init
git add .
git commit -m "Initial commit"
git remote add origin git@github.com:YourUsername/qed-sut-myapp.git
git branch -M main
git push -u origin main
Project Structure Summary¶
| Component | Purpose |
|---|---|
settings.gradle.kts |
Names the project and wires in the framework via includeBuild |
build.gradle.kts |
Declares dependencies and configures test suite selection |
src/main/kotlin/ |
Page objects, data classes, URL paths |
src/test/kotlin/<suite>/ |
TestNG XML, config JSON, and test classes |
resources/fonts/ |
Font file for Extent Reports |
.gitignore |
Excludes build artefacts and IDE files |
The qed-demos project inside the framework repository is the reference implementation of this pattern. When in doubt, compare your setup against it.
Sharing Data Classes with a Ktor Application¶
When your system under test is a Kotlin-based Ktor application, you can share data classes, route definitions, and interfaces between the application and its test suite. This eliminates double maintenance — if a field changes in the app, the test suite fails at compile time rather than silently at runtime.
Architecture¶
QED-Shared ← Generic interfaces (e.g. RequestType)
↑ ↑ Part of the framework repo (public)
framework QED-Shared-MyApp ← App-specific data classes, routes, DTOs
↑ ↑ Separate private repo
SUT repo Ktor app
QED-Shared lives inside the framework repository and contains only generic types such as RequestType.
QED-Shared-MyApp is a separate private repository containing your application-specific data classes — request and response models, route definitions, DTOs, and any other types that both the Ktor app and the test suite need to share.
Why This Pattern?¶
If both the Ktor app and the test suite define a Farm data class independently, they can drift apart — a renamed field in the app silently breaks the test at runtime. With a shared library, both sides use the same class:
@Serializable
data class Farm(
val id: Int,
val name: String,
val region: String,
val herdSize: Int
)
If a field is renamed or removed, both the app and the tests fail to compile immediately.
Setting Up QED-Shared-MyApp¶
Create a new directory alongside the framework:
C:\QEDFramework\QED-Shared-MyApp\
├── settings.gradle.kts
├── build.gradle.kts
└── src\
└── main\
└── kotlin\
└── (shared data classes)
settings.gradle.kts:
rootProject.name = "QED-Shared-MyApp"
includeBuild("../QED-Shared")
build.gradle.kts:
plugins {
kotlin("jvm") version "2.0.20"
kotlin("plugin.serialization") version "2.0.20"
}
group = "com.qed"
version = "1.0.0"
repositories {
mavenCentral()
}
kotlin {
// Use Java 17 for broad compatibility with Ktor apps.
// The QED framework (Java 22) can consume Java 17 libraries without issues.
jvmToolchain(17)
}
dependencies {
implementation(kotlin("stdlib"))
implementation("com.qed:QED-Shared:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
implementation("com.squareup.moshi:moshi:1.15.2")
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
}
What Belongs in the Shared Library¶
- Request and response data classes used in Ktor route handlers
- Route definitions (paths, HTTP methods, permissions)
- Enums and interfaces referenced by both the app and the tests
- DTOs that cross the API boundary
What does not belong here: app-specific business logic, database entities, or test-specific utilities.
Configuring the Ktor App¶
settings.gradle.kts in your Ktor project:
rootProject.name = "MyKtorApp"
includeBuild("../../QEDFramework/QED-Shared")
includeBuild("../../QEDFramework/QED-Shared-MyApp")
build.gradle.kts:
dependencies {
implementation("com.qed:QED-Shared:1.0.0")
implementation("com.qed:QED-Shared-MyApp:1.0.0")
// ... other Ktor dependencies
}
Configuring the SUT Repo¶
settings.gradle.kts:
rootProject.name = "qed-sut-myapp"
includeBuild("../QEDFramework")
includeBuild("../QEDFramework/QED-Shared")
includeBuild("../QEDFramework/QED-Shared-MyApp")
Important: Gradle composite builds do not resolve transitively. Every consumer must explicitly declare all its
includeBuilddependencies — even if another included build already depends on them. This is the most common pitfall when setting up the shared library pattern.
Keeping QED-Shared-MyApp Private¶
Since QED-Shared-MyApp contains application-specific data classes, keep it in a private GitHub repository. QED-Shared itself is part of the public framework repo and contains only generic types.