<?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: Paul Brooks</title>
    <description>The latest articles on DEV Community by Paul Brooks (@paul-brooks).</description>
    <link>https://dev.to/paul-brooks</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121908%2Feb989ad1-a8d3-457f-910e-0fe068a8f142.png</url>
      <title>DEV Community: Paul Brooks</title>
      <link>https://dev.to/paul-brooks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paul-brooks"/>
    <language>en</language>
    <item>
      <title>A Cucumber Alternative for Kotlin and Java</title>
      <dc:creator>Paul Brooks</dc:creator>
      <pubDate>Thu, 01 Oct 2026 19:53:56 +0000</pubDate>
      <link>https://dev.to/paul-brooks/a-cucumber-alternative-for-kotlin-and-java-2j02</link>
      <guid>https://dev.to/paul-brooks/a-cucumber-alternative-for-kotlin-and-java-2j02</guid>
      <description>&lt;p&gt;Cucumber earns its place on one kind of test: an acceptance test whose output someone outside the development team reads. A tester, an analyst, a product owner. That reader is the only reason to put Given-When-Then on a test at all. Unit tests are code, and developers can read code.&lt;/p&gt;

&lt;p&gt;I wrote &lt;a href="https://kensa.dev" rel="noopener noreferrer"&gt;Kensa&lt;/a&gt; for that reader, and it works the same in Kotlin and Java. This post takes a Cucumber scenario, moves it to Kensa, and maps the rest of Cucumber's concepts across, so you can see what a move involves before you start one. If you want the argument for dropping feature files first, that's in &lt;a href="https://kensa.dev/blog/bdd-without-gherkin" rel="noopener noreferrer"&gt;BDD Without Gherkin&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One scenario, before and after
&lt;/h2&gt;

&lt;p&gt;The scenario comes from Clearwave, an example order service that provisions voice and broadband through two suppliers. One supplier rejects the order. In Cucumber:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Order Service

  &lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Order is rejected by FibreVision
    &lt;span class="nf"&gt;Given &lt;/span&gt;OpenNetwork will complete the order
    &lt;span class="nf"&gt;And &lt;/span&gt;FibreVision will reject the order
    &lt;span class="nf"&gt;When &lt;/span&gt;a voice and broadband order is placed
    &lt;span class="nf"&gt;Then &lt;/span&gt;the order confirmation should be pending
    &lt;span class="nf"&gt;And &lt;/span&gt;eventually FibreVision should report the order rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With step definitions along these lines:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt;&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderSteps&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;world&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OrderWorld&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{word} will reject the order"&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;supplierWillRejectTheOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;stubFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trackingId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderScenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rejected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a voice and broadband order is placed"&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;aVoiceAndBroadbandOrderIsPlaced&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confirmation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;anOrder&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// and three more&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderSteps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderWorld&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderSteps&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderWorld&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;world&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Given&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{word} will reject the order"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;supplierWillRejectTheOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stubFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;primeOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTrackingId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;OrderScenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Rejected&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INSTANCE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@When&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a voice and broadband order is placed"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;aVoiceAndBroadbandOrderIsPlaced&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setConfirmation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;place&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anOrder&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// and three more&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same test in Kensa, as it stands in the example project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt;&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="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`order&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;fibre&lt;/span&gt; &lt;span class="nf"&gt;vision`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;openNetworkWillCompleteTheOrder&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fibreVisionWillRejectTheOrder&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="nf"&gt;whenever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;aVoiceAndBroadbandOrderIsPlaced&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;theOrderConfirmation&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;shouldBePending&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;thenEventuallyFibreVisionNotifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shouldBeRejected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;supplier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fixtures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;broadbandSupplier&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;&lt;strong&gt;Java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;orderIsRejectedByFibreVision&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;openNetworkWillCompleteTheOrder&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fibreVisionWillRejectTheOrder&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="n"&gt;whenever&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aVoiceAndBroadbandOrderIsPlaced&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="n"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theOrderConfirmation&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;shouldBePending&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;thenEventually&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;fibreVisionNotifications&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;shouldBeRejected&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;fixtures&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BROADBAND_SUPPLIER&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kensa parses that method at runtime and renders it as the scenario. Each call becomes a sentence, and the broadband supplier fixture renders as the value it held on this run, so the report says FibreVision, not the name of a variable.&lt;/p&gt;

&lt;p&gt;Each step definition becomes a function:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt;&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="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fibreVisionWillRejectTheOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GivensContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixtures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;fibreVisionStub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;primeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixtures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;trackingId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nc"&gt;OrderScenario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rejected&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;&lt;strong&gt;Java&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GivensContext&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fibreVisionWillRejectTheOrder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fibreVisionStub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primeOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFixtures&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TRACKING_ID&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;OrderScenario&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Rejected&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INSTANCE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The body is the step definition's body. What's gone is the string that bound it. The method name is the sentence, so there's no regular expression to match and nothing for a rename to break.&lt;/p&gt;

&lt;p&gt;The function can live in the test class, as it does here, or anywhere the tests can call it. A step used across several test classes goes in a base class or a shared helper, the same way you'd share any other code, and it reads the same in every report that uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything else Cucumber gives you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cucumber&lt;/th&gt;
&lt;th&gt;Kensa&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feature file and step definitions&lt;/td&gt;
&lt;td&gt;The test method, rendered as sentences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Step definition&lt;/td&gt;
&lt;td&gt;A function returning an &lt;code&gt;Action&lt;/code&gt; or a &lt;code&gt;StateCollector&lt;/code&gt;, in the test class or shared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature description&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Notes&lt;/code&gt; on the test class, with markdown and links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Background&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;SetupStep&lt;/code&gt;, written once and passed to &lt;code&gt;given&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scenario Outline and Examples&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ParameterizedTest&lt;/code&gt;, with &lt;code&gt;@ParameterizedTestDescription&lt;/code&gt; naming each row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data table&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@ExpandableRenderedValue(renderAs = Tabular)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;World or scenario context&lt;/td&gt;
&lt;td&gt;Fixtures for inputs, captured outputs for results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags&lt;/td&gt;
&lt;td&gt;Your framework's tags, plus &lt;code&gt;@Issue&lt;/code&gt; and &lt;code&gt;@Epic&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hooks&lt;/td&gt;
&lt;td&gt;Your framework's lifecycle, unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few of those are worth a sentence more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixtures replace the World.&lt;/strong&gt; A Cucumber World is a bag of mutable state that the steps write to and read from. Kensa fixtures are declared once, typed, created lazily and fresh for each test, and they can depend on each other. A tracking ID, a customer, an address built from both. Because they're named, the report shows each one by name wherever the test uses it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fresh fixtures make parallel runs safe.&lt;/strong&gt; Nothing is shared between tests, so they can run at the same time against the same deployed services. Clearwave gives every test its own &lt;code&gt;trackingId&lt;/code&gt; fixture and sends it as a header on each request. The supplier stubs use it to route primed responses and captured messages back to the test that caused them, so each report shows only its own traffic while the whole suite runs in parallel. The report's overview shows what that saved: wall clock against total elapsed, and the speed-up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario Outlines become parameterised tests.&lt;/strong&gt; Each row of the examples table is an invocation, and &lt;code&gt;@ParameterizedTestDescription&lt;/code&gt; gives each one a readable label in the report instead of a list of arguments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags link to the ticket.&lt;/strong&gt; &lt;code&gt;@Issue("PROJ-42")&lt;/code&gt; puts a badge on the test that opens the ticket, and the report can be filtered by issue or epic. If the acceptance criteria were written in Jira, the person who wrote them can get from there to what ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the report adds
&lt;/h2&gt;

&lt;p&gt;This is where a move stops being a translation.&lt;/p&gt;

&lt;p&gt;A Cucumber report shows the scenario, green or red. An acceptance test against a running system does more than that. It sends requests, the system talks to other services, and responses come back. Kensa records those interactions while the test runs. The order test above captures the order request, the confirmation, and each notification the two supplier stubs send back. The report shows them as a sequence diagram drawn from that traffic, and each message opens to its full payload.&lt;/p&gt;

&lt;p&gt;For the reader this post started with, that's the difference. They can see what the system did, in order, with the data, without opening the code. The &lt;a href="https://clearwave.kensa.dev" rel="noopener noreferrer"&gt;Clearwave example report&lt;/a&gt; has this test in it, as &lt;code&gt;OrderServiceTest&lt;/code&gt; and &lt;code&gt;OrderServiceJavaTest&lt;/code&gt;, along with the rest of the suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving a suite across
&lt;/h2&gt;

&lt;p&gt;You don't have to do it in one go. Cucumber JVM and Kensa both run on the JUnit Platform, so one build can run both suites while you move scenarios over one at a time. Most of the work is moving step definition bodies into functions and deleting the strings.&lt;/p&gt;

&lt;p&gt;There's a cost. Kensa is opinionated about how a test reads, since the test body is what the reader sees. It has more machinery than a test library, and it doesn't have Cucumber's twenty years of Stack Overflow answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Kensa isn't the answer
&lt;/h2&gt;

&lt;p&gt;If nobody outside the team reads the output, you don't need Cucumber or Kensa. Write plain tests. &lt;a href="https://kensa.dev/blog/bdd-in-kotlin" rel="noopener noreferrer"&gt;BDD in Kotlin&lt;/a&gt; covers the wider field.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kensa is open source. The &lt;a href="https://kensa.dev/docs/quickstart/kotlin-quickstart" rel="noopener noreferrer"&gt;Kotlin&lt;/a&gt; and &lt;a href="https://kensa.dev/docs/quickstart/java-quickstart" rel="noopener noreferrer"&gt;Java&lt;/a&gt; quickstarts take about five minutes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>java</category>
      <category>bdd</category>
      <category>testing</category>
    </item>
    <item>
      <title>BDD in Kotlin: The Options, and How to Choose</title>
      <dc:creator>Paul Brooks</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:27:34 +0000</pubDate>
      <link>https://dev.to/paul-brooks/bdd-in-kotlin-the-options-and-how-to-choose-3mha</link>
      <guid>https://dev.to/paul-brooks/bdd-in-kotlin-the-options-and-how-to-choose-3mha</guid>
      <description>&lt;p&gt;I've been writing acceptance tests on the JVM since 2009. Concordion first, then Cucumber on the teams around me, then Yatspec, and eventually I wrote my own.&lt;/p&gt;

&lt;p&gt;Yatspec's trick was that it read the test method itself. You wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orchestration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sends&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anOrder&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the report showed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Given&lt;/strong&gt; orchestration sends an order&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No feature file, no step definitions, no glue. The test &lt;em&gt;was&lt;/em&gt; the sentence.&lt;/p&gt;

&lt;p&gt;That's the idea I've been chasing ever since, and it's why I wrote &lt;a href="https://kensa.dev" rel="noopener noreferrer"&gt;Kensa&lt;/a&gt;. So this is a biased guide. It's also a short one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that decides it
&lt;/h2&gt;

&lt;p&gt;Everyone starts by comparing syntax. That's the wrong place to start.&lt;/p&gt;

&lt;p&gt;The question is: &lt;strong&gt;who reads the output, and do they have to?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "the developers", don't add a framework. Kotest or JUnit with well-named tests does everything you want, and anything layered on top is costing you time for nothing.&lt;/p&gt;

&lt;p&gt;If the answer is "a tester, an analyst or a product owner, and they'll make a decision from it", you need an artefact for someone who can't read Kotlin. That's a different problem, and it's the one every tool below is trying to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field, briefly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cucumber and JBehave.&lt;/strong&gt; Scenarios in Gherkin, step definitions matched to the text. The feature file is readable by anyone and can exist before the code, which is real value when scenarios are genuinely written together up front. The cost is three things to maintain instead of one, bound by text your IDE can't refactor. Rename a method and the &lt;code&gt;.feature&lt;/code&gt; file doesn't follow. And the report shows you the Gherkin, not what ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kotest &lt;code&gt;BehaviorSpec&lt;/code&gt;.&lt;/strong&gt; Given-When-Then with no extra dependency, no separate files, and refactoring works because it's all just code. For tests developers read it's the right answer and I use it myself. The output is a test result, which is exactly no use to a product owner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spek.&lt;/strong&gt; Kotlin-native, Gherkin-shaped DSL. Last release was 2.0.19 in August 2022. Fine if you're on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JGiven.&lt;/strong&gt; Readable text derived from method names, so refactoring keeps them in sync. That instinct is correct, and it's the same one behind Yatspec and Kensa. The cost is stage classes: every scenario needs Given, When and Then defined as separate types, and a method name can't carry a runtime value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serenity BDD.&lt;/strong&gt; The closest to what Kensa is for, and the one I haven't used in anger. It generates a substantial report aimed at the same reader, with results by requirement and screenshots through the run. The cost is how much framework you take on, most of it WebDriver and Screenplay, and a report that's static once written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concordion.&lt;/strong&gt; Where I started. The specification is a document written for the reader, which is a genuinely different artefact and the thing the rest of the field misses.&lt;/p&gt;

&lt;p&gt;Every one of those produces something a person can read. None of them produces something a person can &lt;em&gt;ask a question of&lt;/em&gt;, and that gap is most of why I kept going.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But our analysts write the specifications"
&lt;/h2&gt;

&lt;p&gt;This is the argument I hear most often for Cucumber, and for years I thought it settled the matter.&lt;/p&gt;

&lt;p&gt;The assumption underneath it is that if non-developers write Given-When-Then, they have to write it somewhere the test tooling can read. That isn't true. Think about where those sentences actually get written on your project. It's the ticket. Someone writes acceptance criteria in Jira as Given/When/Then, because that's where the work is described and that's the tool the business already lives in. Nobody opens the repository to write a &lt;code&gt;.feature&lt;/code&gt; file. If they do, it's a developer typing up what the ticket already said.&lt;/p&gt;

&lt;p&gt;So the specification exists, and it's already somewhere shared and readable. What Gherkin adds is a second copy of it in your source tree, the step definitions to bind it, and the job of keeping all three in agreement.&lt;/p&gt;

&lt;p&gt;Close the loop instead. The ticket's Given/When/Then gets implemented as the test, in the same words, carrying the reference:&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="nd"&gt;@Issue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PROJ-42"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`refund&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="nf"&gt;hours`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;anOrderPaidBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="nf"&gt;whenever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;aRefundIsRequested&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;theRefund&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;isProcessedWithin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hours&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;Set &lt;code&gt;issueTrackerUrl&lt;/code&gt; once and the report badge links back to the ticket. The ticket links forward to the report. An analyst opens what they wrote, clicks through, and sees what the system actually did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outside in
&lt;/h2&gt;

&lt;p&gt;Kensa is built for acceptance tests. Not unit tests with better names. The system under test is a deployed application or a set of services, and the test sits outside it. You push a message in, you wait, and you verify what came back and what the system did on the way.&lt;/p&gt;

&lt;p&gt;That means no special build. No test profile exposing a generated key so an assertion can find it, no reaching into a repository to check a row landed. If the only way to observe something is from inside the application, the test doesn't get to see it either, which is also the position you're in when it misbehaves in production.&lt;/p&gt;

&lt;p&gt;Testing that way makes the report harder to build and far more useful. The evidence worth showing isn't the test's own variables, it's the traffic: the request that went in, the messages that crossed between services, their payloads, headers and queue names, and the response that came back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvn3wzj8fit7q0c94jpc4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvn3wzj8fit7q0c94jpc4.png" alt="Kensa report showing Given-When-Then sentences alongside a sequence diagram built from the messages captured between services" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nothing in that diagram was drawn by hand or declared anywhere. It's the six interactions the test observed crossing between the customer, the service under test and its two suppliers.&lt;/p&gt;

&lt;p&gt;Outside in includes a browser, since a click is as external as a queue message. Kensa puts Playwright or Selenium behind a driver-agnostic user object, so tests read as &lt;code&gt;theUser.submitsTheApplication()&lt;/code&gt; rather than as selectors, and labelled screenshots collect into their own tab. And if the thing outside your test is a Spring service, the Spring Boot starter takes the wiring: one annotation for the boilerplate, &lt;code&gt;kensa.*&lt;/code&gt; properties from &lt;code&gt;application.yml&lt;/code&gt;, and automatic capture of MockMvc, WebTestClient, &lt;code&gt;RestTemplate&lt;/code&gt; and &lt;code&gt;WebClient&lt;/code&gt; traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a test looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutTest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;KensaTest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;WithKotest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@RenderedValue&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;itemCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;checkout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CheckoutService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Basket&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`customer&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="nf"&gt;basket`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;aBasketOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itemCount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="nf"&gt;whenever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;theCustomerChecksOut&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;theOrder&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;shouldBe&lt;/span&gt; &lt;span class="nc"&gt;Created&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;aBasketOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GivensContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;basket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Basket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;theCustomerChecksOut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActionContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;theOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateCollector&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;order&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;That's the whole file. Kensa parses it at runtime and renders those three lines as sentences with the real values substituted in, so the report shows what the fixtures actually produced on this run rather than the names of the variables that held them.&lt;/p&gt;

&lt;p&gt;That example is deliberately minimal. Constructing &lt;code&gt;CheckoutService()&lt;/code&gt; in the test keeps it to one readable file, and it is not how you would run this for real. In a working acceptance suite the service is deployed, its collaborators are stubs running as singletons in the environment, and the test reaches all of them over the wire.&lt;/p&gt;

&lt;p&gt;Because the report is generated rather than written, you can interrogate it. Open a captured message and read the payload:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fboqrpruxdymtae6p7v0l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fboqrpruxdymtae6p7v0l.png" alt="A captured supplier response opened in the Kensa report, showing the full JSON payload with search and formatting controls" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Collapse the assertions you don't care about. Follow a rendered value back to the fixture that produced it. Filter to the tests touching the thing you're chasing. There's one artefact, it can't drift from the code, and refactoring just works.&lt;/p&gt;

&lt;p&gt;Zoom out and the same run reads as a whole: what passed, where the time went, and which services did the talking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fec9mesqklry8u7i9bx8y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fec9mesqklry8u7i9bx8y.png" alt="Kensa overview page showing run timing, results, results by package, tests by duration, slowest tests, interactions by participant and specification density" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The participants panel is the one no other tool has an equivalent for. Five services, 260 messages captured, and the shape of who talks to whom, none of it declared anywhere. It is just the traffic the tests observed, counted.&lt;/p&gt;

&lt;p&gt;Every screenshot here is from the &lt;a href="https://clearwave.kensa.dev" rel="noopener noreferrer"&gt;Clearwave example report&lt;/a&gt;, which is published and live. Go and click around it rather than taking my word for any of this.&lt;/p&gt;

&lt;p&gt;There's more machinery here than in a test library, and it's opinionated about structure. It also doesn't have Cucumber's twenty years of Stack Overflow answers.&lt;/p&gt;

&lt;p&gt;The compiler plugin sounds like more of that than it is. It's Kotlin only, Java capture goes through the runtime instead, the Gradle plugin applies it for you, and you only need it for the richer capture: &lt;code&gt;@RenderedValue&lt;/code&gt; substitution and expandable sentences, parameterised ones included.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which one
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Only developers read it.&lt;/strong&gt; Kotest &lt;code&gt;BehaviorSpec&lt;/code&gt; or plain JUnit. Don't add a framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're testing a deployed system from outside it.&lt;/strong&gt; Kensa. Services exchanging messages, no test hooks in the application, the report assembled from the traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-developers write the scenarios.&lt;/strong&gt; Kensa, and this is the case people get wrong most often. Implement the ticket's Given/When/Then as the test, link the two with &lt;code&gt;@Issue&lt;/code&gt;, skip the feature file. The only version that genuinely needs Gherkin is one where non-developers execute the features themselves, which is rarer than the marketing suggests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're already deep in Cucumber and want a better report.&lt;/strong&gt; Serenity is the shortest path from where you're standing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want a hand-authored document.&lt;/strong&gt; Concordion, if static is genuinely what you want.&lt;/p&gt;

&lt;p&gt;The thing I'd push back on hardest is the assumption that BDD means Gherkin. It doesn't, and it never did. Gherkin is one way of getting a readable specification, and a good one if a non-developer is holding the pen. If nobody outside your team ever writes a &lt;code&gt;.feature&lt;/code&gt; file, and on most teams I've seen nobody does, you're paying the whole cost of the format for none of its benefit.&lt;/p&gt;

&lt;p&gt;Write the test once. Make the report readable. That's the whole idea.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kensa is open source. The &lt;a href="https://kensa.dev/docs/quickstart/kotlin-quickstart" rel="noopener noreferrer"&gt;Kotlin quickstart&lt;/a&gt; takes about five minutes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>java</category>
      <category>bdd</category>
      <category>testing</category>
    </item>
    <item>
      <title>BDD Without Gherkin</title>
      <dc:creator>Paul Brooks</dc:creator>
      <pubDate>Sat, 12 Sep 2026 09:23:52 +0000</pubDate>
      <link>https://dev.to/paul-brooks/bdd-without-gherkin-558b</link>
      <guid>https://dev.to/paul-brooks/bdd-without-gherkin-558b</guid>
      <description>&lt;p&gt;I've been writing acceptance tests in Given-When-Then since 2009 and I've never written a feature file by choice. So when someone asks whether you can do BDD without Gherkin, my answer is that I've never done it any other way.&lt;/p&gt;

&lt;p&gt;Cucumber has been the default for long enough that the two have merged in people's heads. They shouldn't have. BDD is a way of specifying behaviour. Gherkin is a file format one family of tools uses to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Gherkin is for
&lt;/h2&gt;

&lt;p&gt;It does one job. A non-developer can write a scenario in plain text and run it without touching code.&lt;/p&gt;

&lt;p&gt;If that is what happens on your project, keep Cucumber. A tester or an analyst who opens the repository, writes &lt;code&gt;.feature&lt;/code&gt; files and runs them is getting exactly what the format was built for.&lt;/p&gt;

&lt;p&gt;I've never seen it. What I've seen, on every team, is that the scenario gets written in the ticket. Someone puts acceptance criteria in Jira as Given/When/Then because that's where the work is described and that's the tool they already use. Then a developer types the same words into a feature file and writes step definitions to bind them. The "analysts write the specs" argument turns out to describe a copy step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Three things to keep in agreement instead of one: the feature file, the step definitions and the code. The IDE can refactor two of them. The third is text, and it's the one people read.&lt;/p&gt;

&lt;p&gt;Binding by regular expression. Rename a method and the &lt;code&gt;.feature&lt;/code&gt; file doesn't follow. Reword a step and the match silently changes.&lt;/p&gt;

&lt;p&gt;Glue. Step definitions are where the logic actually lives, and they're neither specification nor test. They accumulate.&lt;/p&gt;

&lt;p&gt;And the report shows you the Gherkin, coloured green or red. Not what the system did. For an acceptance test, what the system did is the whole point: the request that went in, the messages between services, the response that came back. The feature file can't carry any of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it looked like a good idea
&lt;/h2&gt;

&lt;p&gt;Cucumber arrived around 2008. Refactoring support in IDEs was patchy and parsing test source at runtime wasn't practical, so a plain-text file bound to code by matchers was a reasonable answer to the constraints of the time. I don't think it was ever a great answer. It was the available one. The constraints are gone and the costs stayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's left when you drop it
&lt;/h2&gt;

&lt;p&gt;Everything that mattered.&lt;/p&gt;

&lt;p&gt;You still write one concrete example per test, with a given, a when and a then. You still use the words from the ticket, so "refund is processed within 24 hours" is the acceptance criterion, the test name and the line in the report. The test still runs in CI and fails when the behaviour changes.&lt;/p&gt;

&lt;p&gt;The thing you have to work for is the readable output. Gherkin promised living documentation and delivered a feature file with colours. If a product owner is going to read the result, you need something generated from what actually ran, in language they can follow. That's where the choice of tool matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to do it on the JVM
&lt;/h2&gt;

&lt;p&gt;If only developers read the output, don't add anything. Kotest's &lt;code&gt;BehaviorSpec&lt;/code&gt; or JUnit with good method names gives you Given-When-Then in code, refactoring works, and the result is a test result. I use it myself for that.&lt;/p&gt;

&lt;p&gt;If someone who can't read Kotlin needs the output, there are two approaches.&lt;/p&gt;

&lt;p&gt;JGiven derives the sentences from method names. &lt;code&gt;given().a_paid_order()&lt;/code&gt; becomes "Given a paid order" and refactoring keeps them in sync. The cost is stage classes for every scenario, and a method name can't carry a runtime value.&lt;/p&gt;

&lt;p&gt;Yatspec's approach, and the one &lt;a href="https://kensa.dev" rel="noopener noreferrer"&gt;Kensa&lt;/a&gt; takes, is to read the test method itself. The test is ordinary Kotlin:&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="nd"&gt;@Issue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PROJ-42"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`refund&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="nf"&gt;hours`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;anOrderPaidBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="nf"&gt;whenever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;aRefundIsRequested&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;theRefund&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;isProcessedWithin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hours&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;Kensa parses that at runtime and renders the three lines as sentences with this run's values in them. &lt;code&gt;@Issue&lt;/code&gt; links the report back to the ticket, so the analyst who wrote the Given/When/Then in Jira can open it and see what the system did. One artefact. It can't drift from the code because it is the code.&lt;/p&gt;

&lt;p&gt;Because the report is generated, it can also carry what the feature file couldn't: the messages the test saw between services, their payloads, and a sequence diagram drawn from that traffic. The &lt;a href="https://clearwave.kensa.dev" rel="noopener noreferrer"&gt;Clearwave example report&lt;/a&gt; is live if you want to see one.&lt;/p&gt;

&lt;h2&gt;
  
  
  So
&lt;/h2&gt;

&lt;p&gt;Gherkin is one way to write examples down, and a good one if someone outside the team is holding the pen. If nobody is, you're paying for the format and getting nothing from it. Keep the Given-When-Then. Use the words from the ticket. Generate the report from the test that ran.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kensa is open source. The &lt;a href="https://kensa.dev/docs/quickstart/kotlin-quickstart" rel="noopener noreferrer"&gt;Kotlin quickstart&lt;/a&gt; takes about five minutes. &lt;a href="https://kensa.dev/blog/bdd-in-kotlin" rel="noopener noreferrer"&gt;BDD in Kotlin: the options, and how to choose&lt;/a&gt; covers the wider field.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>java</category>
      <category>bdd</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
