DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Enterprise AI Trend Report: Gain insights on ethical AI, MLOps, generative AI, large language models, and much more.

2024 Cloud survey: Share your insights on microservices, containers, K8s, CI/CD, and DevOps (+ enter a $750 raffle!) for our Trend Reports.

PostgreSQL: Learn about the open-source RDBMS' advanced capabilities, core components, common commands and functions, and general DBA tasks.

AI Automation Essentials. Check out the latest Refcard on all things AI automation, including model training, data security, and more.

Related

  • What BDD Is and Why You Need It: Java and Behavior Driven Development
  • Building a Performant Application Using Netty Framework in Java
  • Behavior-Driven Development (BDD) Framework for Terraform
  • Leveraging Java's Fork/Join Framework for Efficient Parallel Programming: Part 1

Trending

  • Advanced-Data Processing With AWS Glue
  • Navigating the Digital Frontier: A Journey Through Information Technology Progress
  • RRR Retro and IPL for Rewards and Recognition
  • Harnessing the Power of SIMD With Java Vector API
  1. DZone
  2. Coding
  3. Frameworks
  4. Smart BDD vs. Cucumber Using Java and JUnit5

Smart BDD vs. Cucumber Using Java and JUnit5

Smart BDD vs. Cucumber using Java and JUnit5. Smart BDD promotes best practices with less code, complexity, higher-quality tests, and increased productivity.

By 
James Bayliss user avatar
James Bayliss
·
Aug. 11, 23 · Analysis
Like (6)
Save
Tweet
Share
8.6K Views

Join the DZone community and get the full member experience.

Join For Free

Cucumber is the leading Behavior-driven development (BDD) framework. It is language-agnostic and integrates with other frameworks. You write the specification/feature, then write the glue code, then write the test code.

With Smart BDD, you write the code first using best practices, and this generates the following:

  • Interactive feature files that serve as documentation
  • Diagrams to better document the product

The barrier to entry is super low. You start with one annotation or add a file to resources/META-INF!

That's it. You're generating specification/documentation. Please note I will interchange specifications, features, and documentation throughout.

If you haven't seen Smart BDD before, here's an example:

Smart BDD get book example

The difference in approach leads to Smart BDD:

  • To have less code and higher quality code
  • Therefore, less complexity
  • Therefore, lowering the cost of maintaining and adding testing
  • Therefore, increasing productivity
  • Oh, and you get sequence diagrams (see picture above), plus many new features are in the pipeline

Both goals are the same, in a nutshell — specifications that can be read by anyone and tests that are exercised.

Implementing BDD with Cucumber will give you benefits. However, there is a technical cost to adding and maintaining feature files. This means extra work has to be done.

There are three main layers: feature file, glue code, and test code:

  • You write the feature file
  • Then the glue code
  • Then the test code

This approach, with extra layers and workarounds for limitations and quirks, leads Cucumber (we'll explore in more with code detail below):

  • To have more code and lower quality. You have to work around limitations and quirks.
  • Therefore, more complexity
  • Therefore, increasing the cost of maintaining and adding testing
  • Therefore, decreasing productivity
  • Therefore, decreased coverage

The quality of code can be measured in its ability to change! Hence, best practices and less code fulfill this brief.

It's time to try and back these claims up. Let's check out the latest examples from Cucumber.

For example, below, I created a repo for one small example — calculator-java-junit5. Then, I copied and pasted it into a new project.

First, Let’s Implement the Cucumber Solution

Feature file:

Gherkin
 
Feature: Shopping

  Scenario: Give correct change
    Given the following groceries:
      | name  | price |
      | milk  | 9     |
      | bread | 7     |
      | soap  | 5     |
    When I pay 25
    Then my change should be 4


Java source code:

Java
 
public class ShoppingSteps {

    private final RpnCalculator calc = new RpnCalculator();

    @Given("the following groceries:")
    public void the_following_groceries(List<Grocery> groceries) {
        for (Grocery grocery : groceries) {
            calc.push(grocery.price.value);
            calc.push("+");
        }
    }

    @When("I pay {}")
    public void i_pay(int amount) {
        calc.push(amount);
        calc.push("-");
    }

    @Then("my change should be {}")
    public void my_change_should_be_(int change) {
        assertEquals(-calc.value().intValue(), change);
    }
    // omitted Grocery and Price class
}


Mapping for test input:

Java
 
public class ParameterTypes {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @DefaultParameterTransformer
    @DefaultDataTableEntryTransformer
    @DefaultDataTableCellTransformer
    public Object transformer(Object fromValue, Type toValueType) {
        return objectMapper.convertValue(fromValue, objectMapper.constructType(toValueType));
    }
}


Test runner:

Java
 
/**
 * Work around. Surefire does not use JUnits Test Engine discovery
 * functionality. Alternatively execute the
 * org.junit.platform.console.ConsoleLauncher with the maven-antrun-plugin.
 */
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("io/cucumber/examples/calculator")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "io.cucumber.examples.calculator")
public class RunCucumberTest {
}


build.gradle.kts showing the cucumber config:

Kotlin
 
dependencies {
    testImplementation("io.cucumber:cucumber-java")
    testImplementation("io.cucumber:cucumber-junit-platform-engine")
}

tasks.test {
    // Work around. Gradle does not include enough information to disambiguate
    // between different examples and scenarios.
    systemProperty("cucumber.junit-platform.naming-strategy", "long")
}


Secondly, We Will Implement the Smart BDD Solution

Java source code:

Java
 
@ExtendWith(SmartReport.class)
public class ShoppingTest {
    private final RpnCalculator calculator = new RpnCalculator();

    @Test
    void giveCorrectChange() {
        givenTheFollowingGroceries(
            item("milk", 9),
            item("bread", 7),
            item("soap", 5));
        whenIPay(25);
        myChangeShouldBe(4);
    }

    public void whenIPay(int amount) {
        calculator.push(amount);
        calculator.push("-");
    }

    public void myChangeShouldBe(int change) {
        assertThat(-calculator.value().intValue()).isEqualTo(change);
    }

    public void givenTheFollowingGroceries(Grocery... groceries) {
        for (Grocery grocery : groceries) {
            calculator.push(grocery.getPrice());
            calculator.push("+");
        }
    }
    // omitted Grocery class 
}


build.gradle.kts showing the Smart BDD config:

Kotlin
 
dependencies {
    testImplementation("io.bit-smart.bdd:report:0.1-SNAPSHOT")
}


This generates:

Turtle
 
Scenario: Give correct change (PASSED)
Given the following groceries
  "milk" 9
  "bread" 7
  "soap" 5
When I pay 25
My change should be 4


Notice how simple Smart BDD is, with much fewer moving parts — 1 test class vs 4 files.
We removed the Cucumber feature file. The feature file has a few main drawbacks:

  • It adds the complexity of mapping between itself and the source code
  • As an abstraction, it will leak into the bottom layers
  • It is very hard to keep feature files consistent
  • When developing an IDE, it will need to support the feature file. Frequently you'll be left with no support

You don't have these drawbacks in Smart BDD. In fact, it promotes best practices and productivity.

The counterargument for feature files is normally, well, it allows non-devs to create user stories and or acceptance criteria. The reality is when a product owner writes a user story and or acceptance criteria, it will almost certainly be modified by the developer. Using Smart BDD, you can still write user stories and or acceptance criteria in your backlog. It's a good starting point to help you write the code. In time you'll end up with more consistency.

In the Next Section, I’ll Try To Demonstrate the Complexity of Cucumber

Let's dive into something more advanced:

  • A dollar is 2 of the currency below
  • Visa payments take 1 currency processing fee
Gherkin
 
When I pay 25 "Dollars"
Then my change should be 29


It is reasonable to think that we can add this method:

Java
 
@When("I pay {int} {string}")
public void i_pay(int amount,String currency){
    calc.push(amount*exchangeRate(currency));
    calc.push("-");
}


However, this is the output:

Plain Text
 
Step failed
io.cucumber.core.runner.AmbiguousStepDefinitionsException: "I pay 25 "Dollars"" matches more than one step definition:
"I pay {int} {string}" in io.cucumber.examples.calculator.ShoppingSteps.i_pay(int,java.lang.String)


Here is where the tail starts to wag the dog. You embark on investing time and more code to work around the framework. We should always strive for simplicity and additional code, and in a boarder sense, additional features will always make code harder to maintain.

We have three options:

1. Mutate i_pay method to handle a currency. If we had 10's or 100's, occurrences of When I pay .. this would be risky and time-consuming. If we add a "Visa" payment method, we are starting to add complexity to an existing method.
2. Create a new method that doesn't start with I pay. It could be With currency I pay 25 "Dollars". Not ideal, as this isn't really what I wanted. It loses discoverability. How would we add a "Visa" payment method?
3. Use multiple steps I pay and with currency. This is the most maintainable solution. For discoverability, you'd need a consistent naming convention. With a large codebase, good luck with discoverability, as they are loosely coupled in the feature file but coupled in code.

Option 1 is the one I have seen the most — God glues methods with very complicated regular expressions. With Cucumber Expressions, it's the cleanest code I have seen. According to the Cucumber documentation, conjunction steps are an anti-pattern. If I added a payment method I pay 25 "Dollars" with "Visa" I don't know if this constitutes the conjunction step anti-pattern. If we get another requirement, "Visa" payments doubled on a "Friday," setting the day surely constitutes another step.

Option 3 is really a thin layer on a builder. Below is one possible implementation of a builder. With this approach, adding the day of the week would be trivial (as we've chosen to use the builder pattern).

Gherkin
 
When I pay 25
And with currency "Dollars"
Java
 
public class ShoppingSteps {

    private final ShoppingService shoppingService = new ShoppingService();
    private final PayBuilder payBuilder = new PayBuilder();

    @Given("the following groceries:")
    public void the_following_groceries(List<Grocery> groceries) {
        for (Grocery grocery : groceries) {
            shoppingService.calculatorPush(grocery.getPrice().getValue());
            shoppingService.calculatorPush("+");
        }
    }

    @When("I pay {int}")
    public void i_pay(int amount) {
        payBuilder.withAmount(amount);
    }

    @When("with currency {string}")
    public void i_pay_with_currency(String currency) {
        payBuilder.withCurrency(currency);
    }

    @Then("my change should be {}")
    public void my_change_should_be_(int change) {
        pay();
        assertThat(-shoppingService.calculatorValue().intValue()).isEqualTo(change);
    }

    private void pay() {
        final Pay pay = payBuilder.build();
        shoppingService.calculatorPushWithCurrency(pay.getAmount(), pay.getCurrency());
        shoppingService.calculatorPush("-");
    }
    // builders and classes omitted
}


Let’s Implement This in Smart BDD

Java
 
@ExtendWith(SmartReport.class)
public class ShoppingTest {
    private final ShoppingService shoppingService = new ShoppingService();
    private PayBuilder payBuilder = new PayBuilder();

    @Test
    void giveCorrectChange() {
        givenTheFollowingGroceries(
            item("milk", 9),
            item("bread", 7),
            item("soap", 5));
        whenIPay(25);
        myChangeShouldBe(4);
    }

    @Test
    void giveCorrectChangeWhenCurrencyIsDollars() {
        givenTheFollowingGroceries(
            item("milk", 9),
            item("bread", 7),
            item("soap", 5));
        whenIPay(25).withCurrency("Dollars");
        myChangeShouldBe(29);
    }

    public PayBuilder whenIPay(int amount) {
        return payBuilder.withAmount(amount);
    }

    public void myChangeShouldBe(int change) {
        pay();
        assertEquals(-shoppingService.calculatorValue().intValue(), change);
    }

    public void givenTheFollowingGroceries(Grocery... groceries) {
        for (Grocery grocery : groceries) {
            shoppingService.calculatorPush(grocery.getPrice());
            shoppingService.calculatorPush("+");
        }
    }

    private void pay() {
        final Pay pay = payBuilder.build();
        shoppingService.calculatorPushWithCurrency(pay.getAmount(), pay.getCurrency());
        shoppingService.calculatorPush("-");
    }
    // builders and classes omitted
}


Let's count the number of lines for the solution of optionally paying with dollars:

  • Cucumber: 
    • ShoppingSteps 123
    • ParameterTypes 21
    • RunCucumberTest 16 
    • shopping.feature 20 
    • Total: 180 lines
  • Smart BDD: 
    • ShoppingTest 114 lines
    • Total: 114 lines

Hopefully, I have demonstrated the simplicity and productivity of Smart BDD.

Example of Using Diagrams With Smart BDD

Smart BDD get book example

This is the source code:

Java
 
@ExtendWith(SmartReport.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BookControllerIT {
    // skipped setup...

   @Override
   public void doc() {
     featureNotes("Working progress for example of usage Smart BDD");
   }
   
   @BeforeEach
   void setupUml() {
       sequenceDiagram()
         .addActor("User")
         .addParticipant("BookStore")
         .addParticipant("ISBNdb");
   }
   
   @Order(0)
   @Test
   public void getBookBy13DigitIsbn_returnsTheCorrectBook() {
     whenGetBookByIsbnIsCalledWith(VALID_13_DIGIT_ISBN_FOR_BOOK_1);
     thenTheResponseIsEqualTo(BOOK_1);
   }

   private void whenGetBookByIsbnIsCalledWith(String isbn) {
      HttpHeaders headers = new HttpHeaders();
      headers.setAccept(singletonList(MediaType.APPLICATION_JSON));
      response = template.getForEntity("/book/" + isbn, String.class, headers);
      generateSequenceDiagram(isbn, response, headers);
   }

   private void generateSequenceDiagram(String isbn, ResponseEntity<String> response, HttpHeaders headers) {
      sequenceDiagram().add(aMessage().from("User").to("BookStore").text("/book/" + isbn));
      
      List<ServeEvent> allServeEvents = getAllServeEvents();
      allServeEvents.forEach(event -> {
         sequenceDiagram().add(aMessage().from("BookStore").to("ISBNdb").text(event.getRequest().getUrl()));
         sequenceDiagram().add(aMessage().from("ISBNdb").to("BookStore").text(
           event.getResponse().getBodyAsString() +  " [" + event.getResponse().getStatus() + "]"));
      });

      sequenceDiagram().add(aMessage().from("BookStore").to("User").text(response.getBody() + " [" + response.getStatusCode().value() + "]"));
   }

    // skipped helper classes...
}


In my opinion, the above does a very good job of documenting the Book Store.

Smart BDD is being actively developed. I'll try to reduce the code required for diagrams, and potentially use annotations. Strike a balance between magic and declarative code.

I use the method whenGetBookByIsbnIsCalledWith in the example above, as this is the most appropriate abstraction. If we had more requirements, then the code could look more like the one below. This is at the other end of the spectrum. Work has gone into a test API to make testing super easy. With its approach, notice how consistent the generated documentation will be. It will make referring to the documentation much easier.

Java
 
public class GetBookTest extends BaseBookStoreTest {

    @Override
    public void doc() {
        featureNotes("Book Store example of usage Smart BDD");
    }

    @Test
    public void getBookWithTwoAuthors() {
        given(theIsbnDbContains(aBook().withAuthors("author", "another-author")));
        when(aUserRequestsABook());
        then(theResponseContains(aBook().withAuthors("author", "another-author")));
    }
}


SmartBDD allows me to choose the abstraction/solution that I feel is right without a framework getting in the way or adding to my workload.

Anything you do and don't like, please comment below. I encourage anybody to contact me if you want to know more — contact details on GitHub.

All source code can be found here.

Please check out GitHub and Smart BDD website

Cucumber (software) Java (programming language) Behavior-driven development Framework

Published at DZone with permission of James Bayliss. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • What BDD Is and Why You Need It: Java and Behavior Driven Development
  • Building a Performant Application Using Netty Framework in Java
  • Behavior-Driven Development (BDD) Framework for Terraform
  • Leveraging Java's Fork/Join Framework for Efficient Parallel Programming: Part 1

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: