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:
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:
- Kotlin
- Java
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()
}
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-assertj' // AssertJ
testImplementation 'dev.kensa:kensa-assertions-hamcrest' // Hamcrest
}
test {
useTestNG()
}
Find the latest Kensa version on GitHub releases.
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:
- Kotlin
- Java
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 }
}
import dev.kensa.Action;
import dev.kensa.ActionContext;
import dev.kensa.GivensContext;
import dev.kensa.RenderedValue;
import dev.kensa.StateCollector;
import dev.kensa.assertj.WithAssertJ;
import dev.kensa.testng.KensaTest;
import org.testng.annotations.Test;
import static org.assertj.core.api.Assertions.assertThat;
class LoanDecisionTest implements KensaTest, WithAssertJ {
@RenderedValue
private final String applicantName = "Alice";
@RenderedValue
private final int requestedAmount = 10_000;
private final LoanService service = new LoanService();
private LoanResult result;
@Test
void canApproveLoanForApplicantWithGoodCredit() {
given(anApplicantWithGoodCredit());
whenever(theLoanServiceProcessesTheApplication());
then(theLoanResult(), r -> assertThat(r.getStatus()).isEqualTo(LoanStatus.Approved));
}
@Test
void canDeclineLoanForApplicantWithPoorCredit() {
given(anApplicantWithPoorCredit());
whenever(theLoanServiceProcessesTheApplication());
then(theLoanResult(), r -> assertThat(r.getStatus()).isEqualTo(LoanStatus.Declined));
}
// --- Givens ---
private Action<GivensContext> anApplicantWithGoodCredit() {
return ctx -> ctx.getFixtures().add(new Applicant(applicantName, 750, requestedAmount));
}
private Action<GivensContext> anApplicantWithPoorCredit() {
return ctx -> ctx.getFixtures().add(new Applicant(applicantName, 300, requestedAmount));
}
// --- Action ---
private Action<ActionContext> theLoanServiceProcessesTheApplication() {
return ctx -> {
Applicant applicant = ctx.getFixtures().get(Applicant.class);
result = service.process(applicant);
};
}
// --- State ---
private StateCollector<LoanResult> theLoanResult() {
return () -> result;
}
}
What's happening here
| Element | Purpose |
|---|---|
dev.kensa.testng.KensaTest | Provides 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 / WithAssertJ | Adds then() / and() overloads that accept Kotest matchers or AssertJ assertions |
@RenderedValue | Field 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:
- Kotlin
- Java
@Test
fun canApproveLoanWithUnderwritingApproval() {
given(anApplicantWithGoodCredit())
and(anApprovalFromUnderwriting())
whenever(theLoanServiceProcessesTheApplication())
then(theLoanResult()) { status shouldBe LoanStatus.Approved }
and(theLoanReference()) { shouldStartWith("LN-") }
}
@Test
void canApproveLoanWithUnderwritingApproval() {
given(anApplicantWithGoodCredit());
and(anApprovalFromUnderwriting());
whenever(theLoanServiceProcessesTheApplication());
then(theLoanResult(), r -> assertThat(r.getStatus()).isEqualTo(LoanStatus.Approved));
and(theLoanReference(), ref -> assertThat(ref).startsWith("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:
- Kotlin
- Java
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
import dev.kensa.testng.KensaTest;
import dev.kensa.assertj.WithAssertJ;
import org.testng.ISuite;
import org.testng.ISuiteListener;
import org.testng.annotations.Listeners;
public class MyAppListener implements ISuiteListener {
@Override
public void onStart(ISuite suite) {
// start stubs, register fixtures, call Kensa.configure()...
}
@Override
public void onFinish(ISuite suite) {
// close stubs and other resources
}
}
@Listeners(MyAppListener.class)
abstract class MyAppTest implements KensaTest, WithAssertJ {}
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:
- Kotlin
- Java
Kensa.konfigure {
outputDir = Path("build/kensa")
}
Kensa.configure()
.withOutputDir("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-testngis built against TestNG7.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.