Skip to main content

Quickstart — TestNG

This guide walks through setting up Kensa with TestNG and writing your first test. The Given–When–Then DSL is identical to the JUnit variants — only the framework artifact and the lifecycle hook differ.

A full reference project is published at kensa-dev/clearwave-testng-example — the TestNG counterpart of clearwave-example.

1. Apply the Kensa Gradle Plugin

The Kensa Gradle plugin wires the Kotlin compiler plugin (so @RenderedValue and @ExpandableSentence capture values for Kotlin tests) and aggregates results into a multi-sourceset site. Apply it alongside Kotlin if you're writing Kotlin tests; pure-Java projects can still apply it to get site-mode reporting:

build.gradle.kts
plugins {
kotlin("jvm") version "2.3.21" // Kotlin tests only — minimum enforced by the plugin
id("dev.kensa.gradle-plugin") version "<plugin-version>"
}

repositories { mavenCentral() }

The plugin and kensa-core version independently — see the compatibility matrix for the supported pairings.

2. Add Test Dependencies

Pull in kensa-framework-testng plus testng itself — the framework artifact declares TestNG as compileOnly, so the consuming project picks the version. Pair it with one or more assertions bridges:

build.gradle.kts
dependencies {
testImplementation(platform("dev.kensa:kensa-bom:<kensa-core-version>"))
testImplementation("dev.kensa:kensa-framework-testng")
testImplementation("org.testng:testng:7.10.2") // kensa-framework-testng has TestNG as compileOnly

// Pick one assertions bridge (or use multiple)
testImplementation("dev.kensa:kensa-assertions-kotest") // Kotest matchers
testImplementation("dev.kensa:kensa-assertions-hamkrest") // HamKrest
}

tasks.test {
useTestNG()
}

Find the latest Kensa version on GitHub releases.

ServiceLoader auto-discovery

The Kensa lifecycle listener dev.kensa.testng.KensaTestNgListener is auto-discovered by TestNG via META-INF/services/org.testng.ITestNGListener in the published jar. Do not add it to @Listeners(...) yourself — that would register it twice and every captured interaction would appear in the report duplicated.

3. Write a Test

Implement KensaTest (from dev.kensa.testng, not dev.kensa.junit) and mix in an assertions bridge. Test methods follow the Given–When–Then structure using the given(), whenever(), and then() DSL:

import dev.kensa.Action
import dev.kensa.ActionContext
import dev.kensa.GivensContext
import dev.kensa.RenderedValue
import dev.kensa.StateCollector
import dev.kensa.kotest.WithKotest
import dev.kensa.testng.KensaTest
import io.kotest.matchers.shouldBe
import org.testng.annotations.Test

class LoanDecisionTest : KensaTest, WithKotest {

@RenderedValue
private val applicantName = "Alice"

@RenderedValue
private val requestedAmount = 10_000

private val service = LoanService()
private lateinit var result: LoanResult

@Test
fun canApproveLoanForApplicantWithGoodCredit() {
given(anApplicantWithGoodCredit())
whenever(theLoanServiceProcessesTheApplication())
then(theLoanResult()) { status shouldBe LoanStatus.Approved }
}

@Test
fun canDeclineLoanForApplicantWithPoorCredit() {
given(anApplicantWithPoorCredit())
whenever(theLoanServiceProcessesTheApplication())
then(theLoanResult()) { status shouldBe LoanStatus.Declined }
}

// --- Givens ---

private fun anApplicantWithGoodCredit() = Action<GivensContext> {
it.fixtures.add(Applicant(applicantName, creditScore = 750, amount = requestedAmount))
}

private fun anApplicantWithPoorCredit() = Action<GivensContext> {
it.fixtures.add(Applicant(applicantName, creditScore = 300, amount = requestedAmount))
}

// --- Action ---

private fun theLoanServiceProcessesTheApplication() = Action<ActionContext> { ctx ->
val applicant = ctx.fixtures.get<Applicant>()
result = service.process(applicant)
}

// --- State ---

private fun theLoanResult() = StateCollector { result }
}

What's happening here

ElementPurpose
dev.kensa.testng.KensaTestProvides the Given–When–Then DSL. Different package from the JUnit interface — make sure you don't pick up dev.kensa.junit.KensaTest by mistake
WithKotest / WithAssertJAdds then() / and() overloads that accept Kotest matchers or AssertJ assertions
@RenderedValueField value is captured and shown in the HTML report
Action<GivensContext>Lambda that runs during given() — sets up fixtures
Action<ActionContext>Lambda that runs during whenever() — exercises the system
StateCollector<T>Returns a value for then() to assert against

4. Chain Multiple Steps

Use and() to chain additional setup or assertions:

@Test
fun canApproveLoanWithUnderwritingApproval() {
given(anApplicantWithGoodCredit())
and(anApprovalFromUnderwriting())

whenever(theLoanServiceProcessesTheApplication())

then(theLoanResult()) { status shouldBe LoanStatus.Approved }
and(theLoanReference()) { shouldStartWith("LN-") }
}

5. Adding Your Own TestNG Listeners

If you need to start/stop fixtures around the whole suite — for example a stub HTTP server, a database container, or any other long-lived resource — implement a TestNG ISuiteListener and register it via @Listeners on an abstract base class:

import dev.kensa.testng.KensaTest
import dev.kensa.kotest.WithKotest
import org.testng.ISuite
import org.testng.ISuiteListener
import org.testng.annotations.Listeners

class MyAppListener : ISuiteListener {
override fun onStart(suite: ISuite) {
// start stubs, register fixtures, call Kensa.konfigure { ... }
}

override fun onFinish(suite: ISuite) {
// close stubs and other resources
}
}

@Listeners(MyAppListener::class)
abstract class MyAppTest : KensaTest, WithKotest

KensaTestNgListener itself is auto-discovered (see the note in step 2), so listing it alongside your own listener is unnecessary — and will produce duplicated captures if you do.

6. Run & View the Report

Run your tests normally with Gradle:

./gradlew test

By default, reports are written to a kensa-output directory in the system temp folder. Configure a fixed location in your test setup:

Kensa.konfigure {
outputDir = Path("build/kensa")
}

Then open index.html in a browser, or use the Kensa CLI to serve them:

kensa --dir build/kensa

TestNG-specific notes

  • Class instance reuse. TestNG creates one instance per class by default. State stored in test-class fields therefore leaks between methods. Either keep test-class fields immutable and put per-test state into a Kensa fixture (which is invocation-scoped), or annotate the class with @Test(singleThreaded = true) and re-initialise fields in @BeforeMethod.
  • @DataProvider. TestNG's parametrised-test mechanism (@Test(dataProvider = "name")) works as usual — the Kensa listener fires per parameter row.
  • TestNG version. kensa-framework-testng is built against TestNG 7.10.x; newer 7.x releases are expected to work.

Other Frameworks

If you're using JUnit instead of TestNG, see the Kotlin Quickstart or Java Quickstart — the DSL and assertions are the same.