Applying Behavior-Driven Development (BDD) in Full-Stack Web Application Development
In recent years, behavior-driven development (BDD) has become increasingly popular as a means of effectively transforming business requirements into high quality software. I have been involved in various projects where we applied BDD, for both backend and frontend development. In this post, I would like to demonstrate how you can benefit from using BDD in web application development.
BDD basics
Behavior-driven development is a test-driven agile development process, that focuses on clear requirements, test automation, and improved communication between technical teams and businesspeople. To achieve this, BDD uses behavioral specification of requirements. This is a specification by example approach, i.e. it encourages teams to specify their requirements using concrete examples. Those specific examples are called scenarios in a BDD context. Scenarios are specified using a simple domain specific language (DSL), which can easily be translated into executable tests and is human-readable at the same time.
What does a BDD Scenario look like
While BDD is not specific about the DSL that should be used to specify the scenarios, most commonly the Gherkin DSL is used. A simple scenario in Gherkin might look like this:
Scenario: Website hit counter is increased when a User visits a website
Given the hit counter of Fred's website has 5000 hits
When John visits Fred's website
Then the hit counter of Fred's website is increased to 5001
Some benefits of using BDD
In the projects where we have applied BDD, I found it helpful to improve the efficiency of the development and the quality of the resulting software in several ways:
- The specification by example approach helps in creating clear, unambiguous requirements, which can be understood by both technical and non-technical people. This helps to reduce the chance of misunderstandings and to simplify communication between the different roles that are involved in the development.
- Executable requirement specifications help developers to implement meaningful tests which focus on testing what really matters. The chance that critical problems are not detected by the automated tests gets minimized.
- BDD encourages high test automation and therefore improves the agility of the development. High quality automated tests allow to create releases at a high frequency and with high confidence. This is critical to achieve continuous delivery or even continuous deployment, which allows the developers to get feedback from the audience quickly.
What about the downsides
While BDD has quite a number of advantages, it comes with some downsides, too. In my experience, the main issues are the following:
- Applying BDD comes with a learning curve. Getting used to a new process and writing requirements in the form of scenarios can be time consuming for teams in the beginning. And integrating cucumber into a project also takes some effort of course. In most cases, it is worth the effort though.
- BDD is only as good as the scenarios you write. Scenarios should focus on the domain concepts and the behavior that is important for the user. But if you write scenarios that mostly verify technical details or UI details, you may end up implementing a lot of meaningless tests. And this will in turn slow down your development process, without adding any significant value.
A detailed introduction to BDD and the Gherkin language can be found at cucumber.io.
Creating automated tests for a Web Application using BDD and Cucumber
So let’s look at how to turn BDD scenarios into automated tests that can be integrated into a CI/CD pipeline.
The most common tool for creating automated tests based on BDD scenarios is Cucumber. It allows executing Gherkin scenarios on various platforms. I will focus here on the Cucumber-JVM and Cucumber.js implementations, but basically the same principles apply to any platform.
Using BDD at different levels of testing
In my experience, many teams use BDD for testing at end-to-end (E2E) or UI level only. But essentially BDD is not restricted in this respect. It can be applied at any level of testing, from unit tests up to system-integration and E2E tests.
If you use BDD on multiple levels of testing, it is not always easy to decide at which level a specific test should be implemented. But as a rule of thumb, I try to implement it at the lowest possible level. That is the lowest level at which the behavior can be meaningfully tested. For example, checking the correctness of a calculation like updating the balance of a bank account, can usually be tested confidently on the service level. But functionalities that involve UI interaction may have to be tested on UI level. If e.g. we want to test a functionality that allows the user to filter entries in the UI, then that will normally have to be done on UI level, in order to get a test that reliably detects problems.
Of course, you will have to use different stacks of testing tools, depending on the desired level of testing. Luckily, Cucumber can be integrated quite simply with various testing tools and frameworks.
For full stack testing of a Java / Spring Boot web app, you could consider the following tool stacks:
- UI and E2E tests can be implemented by combining Cucumber.js with a testing framework like Cypress or Playwright. Or alternatively you could use a tool like Eggplant Functional which offers some built-in support for Gherkin.
- At the service level, you can apply BDD by using Cucumber-JVM combined with Spring Boot tests. This is quite easy to achieve, as Cucumber-JVM offers support for integrating with Spring.
- The use of BDD for unit testing less common. But it can make sense for certain units, if they contain complex behavior. This is also easy to achieve when using Cucumber-JVM in combination with a mocking framework like Mockito.
A word on test shapes
You may have noticed that I used a test diamond instead of the traditional test pyramid in the diagram above. I chose the diamond because I believe that in most cases it makes sense to invest more in the integration tests than in the unit tests, whereas the test pyramid suggests focusing mostly on unit tests. But in my experience, integration tests contribute much more to the overall test confidence level that is achieved in a project than the unit tests. I will not go into detail on test shapes here, as that would be a separate topic. Just so much: in recent years, a number of different test shapes have been proposed, e.g. the testing trophy, the test automation diamond or the testing honeycomb. And I believe it’s certainly worth considering them as alternatives to the test pyramid.
Practical Examples
Now for the practical part. Here I will present some examples of UI- and service-level testing with BDD. The examples are based on a full-stack web app that we built here at ELCA, using Java/Spring Boot in the backend and JavaScript/Angular in the frontend.
I also created a small sample project which includes all the examples listed here. The full source of the sample project can be found on GitHub. This contains a minimalistic functionality for managing contacts, implemented by a REST controller in the backend and a simple frontend implemented in plain HTML/JS.
So let’s consider how the following simplistic scenario could be implemented at service-level and at UI-level:
Scenario: Keeping track of contacts
Given a user has 2 registered contacts so far
When the user adds a new contact named "Josh"
Then the contact list contains a contact named "Josh"
And the contact list contains 3 entries
Service Level Integration Testing using Cucumber-JVM, JUnit and Spring Boot Tests
In this case, we implement the scenarios to run against the REST-API of the service. The tests are executed using JUnit, and Cucumber-JVM is used to integrate the scenario implementations with Spring Boot Tests and JUnit.
Implementation
The main components of the implementation are the following:
ContactCucumberSpringConfig
The ContactCucumberSpringConfig class defines a SpringBootTest for the ContactsApplication and integrates it with Cucumber using the @CucumberContextConfiguration annotation.
contact_management.feature
The contact_management.feature file specifies the scenarios that belong to the example feature in Gherkin syntax.
ContactManagementStepDef
The ContactManagementStepDef class defines the actual implementation for each step in the feature file. In this snippet you can see the definition of the first two steps in our example scenario. This is also called “glue code”, as it connects the DSL with the actual code of the application.
ContactIntegrationTests
Finally, the ContactIntegrationTests class brings it all together: it defines a JUnit 5 test suite that incorporates the scenarios defined in the feature file. When executed, the suite will automatically be populated with the scenarios of the feature file. The key parts here are the glue and class path configuration. They have the following significance:
- the @SelectClasspathResource annotation specifies where cucumber will look for the feature files that are associated to this test class.
- the GLUE_PROPERTY_NAME configuration parameter specifies where the step definition files are located, so that cucumber can find the implementations of the single steps in the associated feature files.
For more details on implementing tests with Cucumber-JVM you may also consult the official tutorial.
Running the Scenarios against the REST API
Using this configuration, the ContactIntegrationTests can run be like any other JUnit test, and Cucumber-JVM will automatically detect the feature files and the step definitions implementing them. The results will be reported for each scenario.
UI Level Testing using Cucumber.js and Cypress
Running Gherkin Scenarios on UI level requires a bit more work. This includes the following steps:
- Create an integration test setup that starts the packaged application including the web interface before the tests and stops it again after the tests have finished. In the sample application I used specific Gradle tasks for this purpose. When using maven, you could just use the spring-boot-maven-plugin to achieve the same.
- Use Cypress to run the UI tests, and integrate it with Cucumber.js, so that it can detect and execute the Gherkin scenarios. The Cypress — Cucumber integration is achieved using the npm cypress-cucumber-preprocessor, details on the configuration are found here.
Implementation
The structure of the actual implementation of the scenarios is then very similar to the implementation in Java. The main components in this case are these:
cypress.json
In cypress.json we have to configure Cypress to look for test definitions in .feature files. In contrast to the Cucumber-JVM setup, there is no glue configuration needed here in order to link the feature files to step definitions. Instead, the cypress-cucumber-preprocessor by default just expects the step definitions to reside in a folder that has the same name as the corresponding feature file.
contact_management.feature
Again, the feature file contains the scenario in Gherkin format. Just the same as in the Cucumber-JVM setup above.
contact_management.steps.js
Finally, we have the contact_management.steps.js file, which contains the step definitions implemented in JS, and uses Cypress to access the UI. As you can see, the structure is essentially the same as in the Java implementation of the step definitions. However, a page model object is used here to abstract access to the UI elements.
Running the Scenarios against the UI
The scenarios can now be executed simply by running cypress (i.e. npm run cypress:run).
In the sample application, you can execute the scenario by running gradle :contacts-ui-tests:check. This will take care of starting the application before the test, invoking Cypress and stopping the application again after that.
Conclusion
In my experience, BDD is indeed a useful tool for improving the efficiency and quality in a development project. And I would like to point out, that it is more than just the end-to-end testing tool, which it is sometimes mistaken for. Actually, it can be used at all stages of development, and it can be sensibly applied on all levels of testing and on all components of an application.
I found the main the benefits of using BDD to be:
- Requirement specifications written using the BDD approach are clearer and more understandable than traditional specifications. This makes it a lot easier to discuss and if necessary, challenge requirements among the different technical and non-technical project members.
- BDD encourages and facilitates test automation. This lays the basis for releasing at a high frequency and with high confidence, suitable for continuous delivery or even continuous deployment.
- Executable requirement specifications help developers to implement meaningful tests which focus on testing what really matters. The chance that critical problems are not detected by the automated tests gets minimized. And this also helps to avoid testing anti-patterns, such as focusing on implementation details instead of the actual business logic, assertion-free testing, etc.
So overall the development efficiency and the testing confidence level in a project can be significantly improved by using behavior-driven development.