Summary
Tables are a desirable way to specify values in a Cucumber Given/When/Then step. Here’s an example of a table:
Given Customer is: | Id | Name | Street | Zip Code | | 123 | John | 1 Apple Lane | 27701 |
Advantages of Tables
Here are some advantages of using tables:
A.) The domain terms in the scenario can match the terms used in the code. This helps in understanding how the implementation relates to the requirements. The domain terms are the names of each column. Using text such as:
Given a customer whose ID is 123 and whose name is John
makes it slightly more difficult to identify which words are domain terms.
Note that It is easy to parse the column headers to find domain terms if you wanted to create a glossary of terms.
B.) You do not need to specify every value in the table. You can list only those relevant to the test. The remaining values will be either null or filled with defaults (see explanation later). Example:
Given Customer is: | Id | Zip Code | | 123 | 27705 |
C.) You can add additional values by just entering the column headers and values and by adding an attributes to the corresponding class. You can use the same step definition. Without a table you would need to create another step definition. For example, suppose you wanted to add a city field to CustomerData. You just add it to the class.
class CustomerData { //.. other fields String city; } @Given("^Customer is:$") public void customer_is(List<CustomerData> data) throws Throwable { // Use data as a list } #And add it to the table: Given Customer is: | Id | City | | 123 | Durham |
If you used parameters in one of the statements, you’d have to create a new step definition. The original one would be:
Given a customer whose ID is 123
And the new one would be:
Given a customer whose ID is 123 and whose city is Durham
D.) Every entry in a table is treated as a string, so there is no need for quotation marks around the values
E.) There is no need to deal with regular expressions for parameters.
Setting Values in Tables
With the fields in the table, you have at least three ways of setting the values in a data object:
1.) Use the data object directly
2.) Use a derived object that can set default values if null values are not acceptable
3) Use a separate object that creates the data object
Here are details on the three ways:
1.) Use the data object directly
@Given("^Customer is:$") public void customer_is(List<CustomerData> data) throws Throwable { // Use data as a list }
2.) Use a derived object:
@Given("^Customer is:$") public void customer_is(List<CustomerDataTest> data) throws Throwable { List<CustomerData> list = CustomerDataTest.translate(data); // Use as desired } } // The derived class: public class CustomerDataTest extends CustomerData { public void fillInAttributes() { //CustomerData def = new CustomerData(); // Could use the values in def, if they were initialized if (getID() == null) setID(new CustomerID("ABC")); if (getName() == null) setName("George"); if (getStreet() ==null) setStreet("Somewhere Street"); if (getZipCode() == null) setZipCode(new ZIPCode("11111")); return ; } public static List<CustomerData> translate(List<CustomerDataTest> data) { List<CustomerData> cdList = new Vector<CustomerData>(); for( CustomerDataTest cdc: data) { cdc.fillInAttributes(); cdList.add(cdc); } return cdList; } }
3.) Use a separate object
@Given("^Customer is:$") public void customer_is(List<CustomerDataCuke> data) throws Throwable { List<CustomerData> cd = CustomerDataCuke.translate(data); // Use as desired } public class CustomerDataCuke { // The default values are not used by Cucumber // when converting as a data table private CustomerID id; private String name; private String street; private ZIPCode zipCode; public CustomerData toCustomerData() { CustomerData here = new CustomerData(); if (id != null) here.setID(id); else here.setID(new CustomerID("ABC")); if (name != null) here.setName(name); else here.setName("George"); if (street !=null) here.setStreet(street); else here.setStreet("Somewhere Street"); if (zipCode != null) here.setZipCode(zipCode); else here.setZipCode(new ZIPCode("11111")); return here; } // Do a translation to production data object public static List<CustomerData> translate (List<CustomerDataCuke> data) { List<CustomerData> cdList = new Vector<CustomerData>(); for( CustomerDataCuke cdc: data) { cdList.add(cdc.toCustomerData()); } return cdList; } }
In case #1 and #2, the column headers must match the data object attribute names. If the names are obscure, these will be reflected in the feature file
In case #1, any attributes that are not set in the table will be null. This may not cause a problem if the attributes are never accessed by any underlying implementation for the scenario that is being tested. A null exception may be useful in determining unnecessary code (or it may just be noise).
In case #2, any attributes not set in the table can be set to defaults. This will ensure that null values are not propagated anywhere. If the default value does not meet the needs of the scenario, then it is important to the scenario and needs to be specified in the table.
In case #3, the names of the separate object can be different than those in the underlying data object. The translation will allow the column headers to be in business domain terms. The attributes that are not set in the table can be set to defaults (or left as null, if desired).