BDD vs BDD-Style
Just because of describing the expected behavior of a system in Cucumber Feature Files doesn't make it proper Behavior-Driven Development. Proper BDD is abstract and agnostic of implementation details or interfaces.
- Author:
- Christian Hujer, CEO / CTO at Nelkinda Software Craft Pvt Ltd
- First Published:
- by
Nelkinda Software Craft Private Limited
- Last Modified:
- by Christian Hujer
- Approximate reading time:
1 BDD-Style
I often see people claiming to perform BDD because they write feature files and use a framework like Cucumber to execute them. Their feature files look like this:
Feature: Login Background: Given a user "testuser" with password "testpassword" exists Scenario: Successful Login Given I have all cookies cleared And I go to the start page When I enter "testuser" in the field "username" And I enter "testpassword" in the field "password" And I hit the "login" button Then the HTTP response code MUST be 200 "Ok" And the response MUST set a cookie with the JWT
LoginUi.feature
While this looks like BDD at first glance, it's not real BDD. The test does not describe the behavior of the system, but its user interface. Such tests could possibly still be useful tests. But this is not proper BDD, and thus it shouldn't be called like that. I like to call such tests BDD-style.
2 "Proper" BDD
This is how the same feature looks like when expressed as a real BDD feature file:
Feature: Login Background: Given a user "testuser" with password "testpassword" exists Scenario: Given I am currently not logged in When logging in with username "testuser" and password "testpassword" Then login MUST be successful
Login.feature
Observe that the feature file does not have any "solutionized" elements. It does not mention any interface. It is agnostic of the technology.
Just like the other feature file, this can still be run as an end-to-end user interface test. But it can also be run as a REST-API test. Or as a unit test.
2.1 End-to-End User Interface Glue
We can run this feature file as an end-to-end user interface test. For that, we could, for example, use Selenium WebDriver.
public class UiStepDefs { @LocalPort private int port; @Autowired private UserService userService; private WebDriver webDriver; @Before public void setupWebDriver() { WebDriverManager.chromedriver().setup(); final ChromeOptions options = new ChromeOptions(); options.addArguments("headless"); webDriver = new ChromeDriver(options); } @After public void closeDriver() { webDriver.quit(); } @Given("a user {string} with password {string} exists") public void createUser(final String username, final String password) { userService.addUser(username, password); } @Given("I am currently not logged in") public void notLoggedIn() { webDriver.manage().deleteAllCookies(); webDriver.get("http://localhost:" + port); } @When("logging in with username {string} and password {string}") public void login(final String username, final String password) { webDriver.findElement(By.id("username")).sendKeys(username); webDriver.findElement(By.id("password")).sendKeys(password); webDriver.findElement(By.id("login")).click(); } @Then("login MUST be successful") public void assertLoginSuccess() { assertNotNull(webDriver.manage().getCookieNamed("JWT")); } }
UiStepDefs.java
2.2 Integration Test Glue
We can run the very same feature file as an integration test. For that, we could, for example, use an HTTP client.
public class ApiStepDefs { @Autowired private TestRestTemplate testRestTemplate; private Response response; @Given("a user {string} with password {string} exists") public void createUser(final String username, final String password) { userService.addUser(username, password); } @Given("I am currently not logged in") public void notLoggedIn() { // Nothing to do } @When("logging in with username {string} and password {string}") public void login(final String username, final String password) { response = testRestTemplate.postForEntity("/session", null, Object.class); } @Then("login MUST be successful") public void assertLoginSuccess() { assertEquals(HttpStatus.OK, response.getStatusCode()); } }
ApiStepDefs.java
2.3 Unit Test Glue
And we can run the very same feature file as a unit test (sic!). For that, we directly create the object under test, mocking external dependencies, and directly call its methods.
public class UnitStepDefs { private UserRepository userRepository = Mockito.mock(UserRepository.class); private SessionRepository sessionRepository = Mockito.mock(SessionRepository.class); private SessionService sessionService = new SessionService(userRepository); private Optional<Session> session; @Given("a user {string} with password {string} exists") public void createUser(final String username, final String password) { Mockito.when(userRepository.findByUsername(username)).thenReturn(new User(username, Bcrypt.encrypt(password))); } @Given("I am currently not logged in") public void notLoggedIn() { // Nothing to do } @When("logging in with username {string} and password {string}") public void login(final String username, final String password) { session = sessionService.login(username, password); } @Then("login MUST be successful") public void assertLoginSuccess() { assert(session.isPresent()); } }
UnitStepDefs.java
2.4 Explanation
A key thing is to describe the behavior without bringing any details about the solution like architectural interface into the test. That knowledge is reserved for the step definition. You know you have achieved that if you can run the same feature file with different step definitions to verify the behavior on different layers of the test pyramid.
3 P.S.: Caveat about Login
The example above uses Login as the feature to be verified. Most of us know login situations and will immediately understand the context. That makes login a good example to explain this aspect of BDD. However, let me point out this: For most systems, "Login" should be even more abstract, and definitely not be a user story. But that's a story for another blog article.
4 P.P.S.: "BDD-Style"?
I believe that the phrase "BDD-Style" doesn't fully communicate that this is usually not the way to go. I'm therefore looking for phrases that communicate this intent better. Candidates are "Faux-BDD" and "Pseudo-BDD", and I'm open for suggestions.