
Writing Good Gherkin Enables Good Test Automation
Behavior Driven Development, also known as BDD, is an example-based methodology for developing software. The key to BDD is collaborative exercises between Business, Development, and Testers. These parties are collectively known as the Three Amigos.
When beginning the work for a new feature, the three amigos get together and write examples of how this feature will be used. By discussing these examples, all parties come to a shared understanding of how the feature should behave in various scenarios.
Let’s take a peak into one of these meetings. [If you’d prefer to watch me do this on video, I have it recorded.]
Three Amigos Meeting
Parabank team is building a new feature which will allow users to withdraw money from their account. The three amigos begin to envision this scenario and how the application will behave.
They use a Gherkin syntax which allows them to use domain-specific language in a Given, When, Then format.
The Given-When-Then syntax forces them to think about the current state of the system (that’s the Given), the action that is taken on the system (that’s the When), and the resulting behavior of the system (that’s the Then).
The three amigos decide to draft their first scenario, which is a happy path where the user withdraws from his bank account.
They consider the prerequisites that determine the current state of the system before any action is taken. They decide that they need:
- A customer who has an account
- A certain amount of money in that account, let’s say $100
So they write this in Gherkin format by using two Given statements:
1 2 |
Given a customer has an account And the account balance is 100.00 dollars |
Notice here, the use of the word And. This keyword is used when there’s more than one of a specific type of statement. As opposed to saying “Given X, Given Y”, it’s written in a conversational tone: “Given X And Y”.
Now that the team has decided on prerequisites, they describe the action to be performed. This is described using a When statement:
1 |
When the customer withdraws 10.00 dollars |
Finally, the team discusses how the system should behave in this situation. This is done via two Then statements:
1 2 |
Then the account balance should be 90.00 dollars And a new transaction should be recorded |
Now, the team has a complete scenario described and the three amigos know what it is they need to build.
Benefits of BDD
Practicing BDD has many benefits. Team members are able to collaborate and gain a shared understanding before development begins, which means any ambiguity or differences of opinions can be discussed very early in the process and addressed before any development begins.
Everyone is speaking the same domain-specific language so there’s no confusion about things like terminology.
If written in Gherkin syntax, the scenarios can be used as executable artifacts that drive automated testing of those scenarios.
Using Gherkin for Test Automation
The Gherkin-style scenario can be added to what’s called a feature file. This is an example of the feature file and it serves as input into test automation scripts.
However, many teams get stuck here because a lot of the details about how to execute this scenario are missing.
It says a customer has an account. But which customer? And what’s the account information?
It says the customer withdraws ten dollars but doesn’t give the steps on how that’s done? What pages do I need to go to and what UI elements do I need to click on?
It describes the expected result but not where to go to verify that.
Because of this, often times, testers totally rewrite that concise feature file into something like this.
Yes, this provides a lot more detail but there are some cons to this approach.
In addition to reducing the reusability of the steps, these steps contain a ton of implementation detail and explicitly dictate how the automation should be written.
One reason that the three amigos come up with scenarios that focus on behavior and not implementation, is because how you make this happen isn’t really relevant, and by adding all of this detail, you’re losing the intent of the scenario’s behavior.
So, if we don’t provide implementation detail for product development, we certainly shouldn’t do it for automation development either.
Back to our concise scenario that describes behavior. Yes, a lot of detail is missing here. But that’s ok. In fact, it’s great!
You may ask, well how will we know how to implement this?!
We leave that to whomever needs to write the automation code. Just as we trust the developers to be able to develop the feature without the implementation details, we can do the same for those developing the automation code.
Let’s take each one of these steps and see how we can turn this into glue code that actually executes the scenario and also follows good test automation design principles.
Writing the Test Automation (Glue) Code
Using Cucumber, the automation engineer takes the scenarios from the three amigos meeting and places them in an executable feature file. Here’s the one that she will work on:
1 2 3 4 5 6 7 8 9 |
Feature: Withdraw As a customer, I want to withdraw funds from my account Scenario: Withdraw from account when funds are available Given a customer has an account And the account balance is 100.00 dollars When the customer withdraws 10.00 dollars Then the account balance should be 90.00 dollars And a new transaction should be recorded |
The automation engineer creates a Java file to hold the automation code. The code in this file is called step definitions and they map to the steps within the feature file above.
1 2 3 4 5 6 |
package cucumber.stepdefs; public class WithdrawStepDefs { } |
She also adds a runner file to tell Cucumber where the feature files and step definition files are.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package cucumber; import org.junit.runner.RunWith; import cucumber.api.CucumberOptions; import cucumber.api.junit.Cucumber; @RunWith(Cucumber.class) @CucumberOptions( plugin = {"pretty"}, glue = {"cucumber.stepdefs"}, features = {"src/test/java/cucumber/features"}) public class CucumberTestOptions { } |
Step 1
Now she’s ready to tie the steps from the feature file to the glue code! She takes the first step:
1 |
Given a customer has an account |
and adds glue code in WithdrawStepDefs to map to it. This is done by using the Cucumber annotation @Given followed by the text from the feature file.
1 2 3 4 5 6 7 8 9 |
package cucumber.stepdefs; public class WithdrawStepDefs { @Given("a customer has an account") public void createNewAccount(){ } } |
Immediately following this annotated glue step is the method that should be executed when this step is called. This method can be named anything and can execute any code the automation engineer desires.
Again, based on this step, she doesn’t know which customer this is or anything about the account. That’s ok. She can decide on a customer for this scenario and push the details of that customer down to a lower level versus storing it inside of the feature file. She decides to add the customer’s information to a properties file. She already has one that stores the url of her application as well as the location of her chromedriver executable, so she simply adds a new section for the customer details.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
################ # APP # ################ app.url=http://parabank.parasoft.com/parabank/ ################ # SELENIUM # ################ webdriver.chrome.driver=resources/chromedriver ################ # CUSTOMER # ################ customer.id=12212 customer.username=john customer.password=demo |
In order to do anything with this account, the customer needs to be logged in. Again, not something that should be placed in the feature file, as it’s needed for just about every scenario! So, this can also be pushed down to the code.
The automation engineer decides to place this in a separate Java file so that she only has to implement it once and that every test scenario can automatically execute this by using inheritance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package base; import components.LoginComponent; import org.junit.AfterClass; import org.junit.BeforeClass; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import java.io.File; import java.io.FileInputStream; import java.util.Properties; public class BaseTests { protected static WebDriver driver; @BeforeClass public static void launchApplication() { loadTestProperties(); driver = new ChromeDriver(); driver.get(System.getProperty("app.url")); new LoginComponent(driver).login(); } @AfterClass public static void closeBrowser() { driver.quit(); } private static void loadTestProperties(){ Properties props = System.getProperties(); try { props.load( new FileInputStream(new File("resources/test.properties"))); } catch(Exception e) { e.printStackTrace(); System.exit(-1); } } } |
She also updates the step definition file to extend this BaseTests class
1 |
public class WithdrawStepDefs extends BaseTests |
So, now login is taken care of. Time to decide on the account. It’s a good thing that the account details were not specified in the feature file. This allows the automation engineer to follow good practices. She doesn’t want to use an account that already exists because if there are more tests that are running in parallel and they are also accessing this account (and expecting a certain amount to be there), this will cause a clash with the test data and the tests will fail. So, she decides to create a new account on the fly, and because the implementation details were not in the feature file, she can do this.
Another decision the automation engineer makes is to utilize her application’s web services to create this new account. Creating an account is not a part of this test; it’s a pre-req. There’s no good reason to do this on the UI which will take much longer. Again, because those click steps were not dictated by the feature file, she can choose the best way to implement this. She uses the Rest-Assured tool to make the web service call and to parse the response.
Note: The other source code files used here can be found at the Github link at the end of this article
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package cucumber.stepdefs; import base.BaseTests; import cucumber.api.java.en.*; import io.restassured.response.Response; import services.Endpoints; import utils.ServicesUtils; import static java.lang.String.*; import static utils.ServicesUtils.HttpMethod.*; import static io.restassured.path.json.JsonPath.from; public class WithdrawStepDefs extends BaseTests { private Response response; private int accountId; @Given("a customer has an account") public void createNewAccount(){ String customerId = System.getProperty("customer.id"); String endpoint = format(Endpoints.CREATE_ACCOUNT, customerId); response = ServicesUtils.execute(endpoint, POST); accountId = from(response.asString()).get("id"); } } |
This covers the first step of the scenario.
Step 2
Let’s move to the next step.
1 |
And the account balance is 100.00 dollars |
Now the automation engineer needs to ensure that there’s $100 in the new account that she created. Again, this is something she decides to do at the service level to keep her test nice and fast.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@And("^the account balance is (.*) dollars$") public void setAccountBalance(float desiredBalance) { float currentBalance = getCurrentBalance(); if(desiredBalance != currentBalance){ deposit(desiredBalance - currentBalance); } } private float getCurrentBalance(){ String endpoint = format(Endpoints.GET_ACCOUNT, accountId); response = ServicesUtils.execute(endpoint, GET); return from(response.asString()).get("balance"); } private void deposit(float amount){ ServicesUtils.execute(format(Endpoints.DEPOSIT, accountId, amount), POST); } |
Notice on line 1, she uses a wild card (*) for the amount. This makes the step reusable for future scenarios. Because there is a wild card, the associated method must accept a parameter for that value – this is represented by the variable desiredBalance. Look at how efficient she can be since the exact steps were not dictated to her via the feature file.
Step 3
The next step is also one that doesn’t necessarily need to be done on the UI! Thank goodness the step only tells her what needs to occur and not how to make it happen. The automation engineer decides do this via a web service call as well.
1 |
When the customer withdraws 10.00 dollars |
1 2 3 4 5 |
@When("the customer withdraws (.*) dollars") public void withdraw(double withdrawAmount){ String endpoint = format(Endpoints.WITHDRAW, accountId, withdrawAmount); response = ServicesUtils.execute(endpoint, POST); } |
Step 4
What’s next?
1 |
Then the account balance should be 90.00 dollars |
A Then statement. Which means it’s time to verify. The automation engineer needs to make sure the account now holds $90. Another web service call to the rescue!
1 2 3 4 |
@Then("the account balance should be (.*) dollars") public void verifyBalance(float balance){ Assert.assertEquals(balance, getCurrentBalance(), 0.0f); } |
Step 5
Final step. Another verification point.
1 |
And a new transaction should be recorded |
The automation engineer debates this one. She can essentially do this one with a web service call as well, but she knows it’s also important to make sure that the transaction record is showing to the customer how it’s supposed to and that none of the details are in the wrong places.
Here’s how the UI looks when everything is working.
There’s a LOT of details on this page. The automation engineer wants to make sure that everything is correct: the total balance, the available balance, the account number, the account type, the transaction records, etc.
She’s not excited about writing all of these assertions. Then it hits her…she doesn’t have to! Because all of this wasn’t detailed in the feature file, again, she can make wise choices and go for a more efficient approach. She decides to use the Applitools Eyes API to do a single visual assertion which will verify a screenshot image of the entire page. That way she covers all that she needs to verify with less code.
1 2 3 4 5 6 7 8 9 |
@Then("a new transaction should be recorded") public void verifyTransactionRecord(){ AccountActivityPage activityPage = NavigationUtils.goToAccountActivity(driver, accountId); Assert.assertEquals(valueOf(accountId), activityPage.getAccountId()); eyes.open(driver, "Parabank", "Withdraw from account when funds are available"); eyes.checkWindow(); eyes.close(); } |
Because some of the data on the page will change each time the test runs (account id, dates of transactions), she simply annotates those areas using the Applitools dashboard so that the visual check knows those areas are dynamic.
Good Gherkin, Good Automation
So there we have it. Because the automation engineer took the Gherkin file from her three amigos meeting and didn’t modify it to add all of the implementation details, she was able to work smarter when turning it into test code. She’s able to use the UI when she needs to, API when she needs to, database calls and anything else that would make the tests more efficient while following good test automation practices.
See Me Code This
Stella Nkirote M'Mukindia
A great article with clear info and easy to understand. Thanks Angie for your endless feeds which are really helpful. You will always be my Techie Hero!
Pramit Singh
Nailed it !
Jitendra
Great article Angie.
Pingback: Five Blogs – 25 September 2019 – 5blogs
Avinash Shetty
Great article. Simple, clear and crisp. @Angie
Biswajit Pattanayak
” Just as we trust the developers to be able to develop the feature without the implementation details, we can do the same for those developing the automation code.” – This is probably my takeaway from the article. So simple statement, yet so powerful. I have been making arguments against writing procedural statements under the garb of BDD at all places, now I can make use of this powerful argument.
Richard Forjoe
Hi @Angie, Thanks for this, really useful as usual. I was wondering if you could help. I’m struggling to amend the ServicesUtils class to include a body for the the POST method.
Angie Jones
Here’s a good tutorial on using RestAssured for POST calls
https://www.toolsqa.com/rest-assured/post-request-using-rest-assured/
Pingback: Strictly Good Gherkin with Gwen – The Gwen Interpreter
Vinay
Thanks Angie, Great article
Luis V.
This is amazing!!, thanks a lot for sharing