Last week, you voted for options on specifying hierarchies of entities with Given-When-Then. There was no clear winner in the votes, which perhaps isn’t surprising as there are just no perfect solutions to that problem.
Visualizing hierarchies usually requires recursive structure formatting and often involves graphical notation or strict indentation, none of which is possible with Given-When-Then. However, there are some potentially good workarounds for this problem in different contexts.
When in doubt, optimize for reading
An important guideline for Given-When-Then scenarios is that they need to be easy to understand. If it takes a long time to figure out what’s going on, people will not be able to discuss expected outcomes easily or distinguish between bugs and outdated tests in the future. I suggest always optimizing a scenario for reading, and dealing with the rest in the automation layer (step implementations).
Very often, when I’ve seen data hierarchies specified in real projects, they were driven by one of the following two needs:
- the same data set is used to demonstrate lots of different scenarios (usually in reporting systems)
- a scenario wants to show some behavior related to several different groups of entities, such as showing aggregation across different accounts
There are good workarounds for both situations that do not require listing the whole hierarchy of records.
Focus on one scenario at a time
When a data set is used to demonstrate lots of different scenarios, a business domain expert usually prepares a “golden data source” that can demonstrate a whole range of features, which almost always maps to database tables.
This approach optimizes writing the scenarios but makes it very difficult to understand failures, and becomes very tricky to maintain. Over time, this set of scenarios becomes cement that prevents code from changing. Any modification that requires an additional record usually breaks hundreds of unrelated tests and puts people off from changing the pristine data. Scenarios become more and more convoluted as they try to reuse existing data, that was set up for a different purpose.
It’s far better to break up the test configuration so that each scenario prepares only what it needs, and so that individual scenarios can focus on whatever level of the hierarchy is important for them.
Sometimes the “golden data source” approach makes execution faster, as the automation system can prepare data only once before all tests. We can start from a well-known state, and individual scenarios can only add the extra bits that they need. Listing the hierarchy inside Given-When-Then is not necessary in this case. We can use an external file with a technical structure to set up the scenarios, or even a binary database file that can be easily restored. Scenarios can modify this only by tweaking individual records that are truly important for them.
An important trick to use in such cases is to quickly bring back the clean state before each test. I suggest running tests inside database transactions. After each scenario, automatically roll back the transaction to cancel any pending changes, and restore the database to the well-known state.
Show aggregate properties only
When a scenario shows some behavior related to groups of entities, it’s usually possible to avoid listing the entire hierarchy by just focusing on the aggregate properties. The automation layer can use that information to create the appropriate data. For example, if we wanted to demonstrate that individual users can only access their own transactions, it doesn’t matter so much what the individual amounts are. We could prepare the scenario like this:
Given 2 users with credit and current accounts And each account has 3 transactions from 1 January to 3 January and amounts between 101 and 103 ... When user-1 requests a "credit" account listing Then the following transactions are reported | account | amount | date | | user-1-credit | 101 | 1 January | | user-1-credit | 102 | 2 January | | user-1-credit | 103 | 3 January |
The second step implementation can just fill in the blanks and automatically create the appropriate accounts and transactions. Note that in this case, I’m also specifying the date and amount ranges. We could also infer those and make the scenarios simpler, but sometimes it’s good to be able to control the variation.
Specify target properties more directly
When a scenario cares about individual records, it’s rare that it needs to be specific about all levels of the hierarchy at the same time. Usually, a scenario focuses on one concept to explain how it works. In such cases, it’s better to specify that level of hierarchy directly, and infer the other levels in the automation to just fill in the blanks.
For example, if we really care care about the transactions, we could collapse the user and account and present the key properties with the transaction amounts, such as in the table below:
Given the following transactions: | owner | account type | date | amount | | leto | credit | 2 January | 2000 USD | | leto | credit | 3 January | 500 USD | | leto | current | 3 January | 1500 USD | | vladimir | credit | 2 January | 5000 RUB |
This can lead to some redundancy, but if only a small number of items are relevant, this is usually a good compromise.
Denormalising data in this way helps with reading, but it makes it possible to introduce inconsistencies. For example, because we’re not separately specifying accounts with currency configuration, someone could easily add transactions for two different currencies into the same account. However, we can protect against that in the automation layer, preventing inconsistent entries. This approach makes test automation slightly more complicated but helps with reading and understanding. I think this is a good trade-off.
What if we really want the whole hierarchy?
Assuming that none of the mentioned workarounds make sense, and that for a specific scenario we actually want to list the whole hierarchy, my preference would be option 1 from the original challenge (chain together individual entities). Each level provides the context for reading the next one, which makes it easy to understand what’s going on. It also makes it easy to modify any piece of information. As before, this approach makes automation more complex but makes it easier to read scenarios.
With option 2 (normalized tables), adding a transaction might require modifying things in the accounts table, and this is difficult to track for humans (that’s why we have RDBMS systems). SQL normalization is optimized for storage and CPU processing, not for reading. Even when the information is inside a relational database, people rarely consume it directly by listing full tables. We usually produce queries or views that make the contents more accessible. Ken Pugh proposed an interesting alternative that visualizes a hierarchy almost like a hierarchical view of relational data, fitting into the syntax of Gherkin pipe-separated tables. Check out his example for more information on it.
With option 3 (embedding hierarchy as text), the contents usually have a special structure that either ends up too technical or too brittle. For example, the YAML format from the original example puts special meaning on leading white space in each line. Slightly messing with that can change the meaning of the hierarchy in unexpected ways. Inserting the wrong number of spaces, or using a tab instead of a space can make a transaction an orphan or assign it to the wrong account, and that will be difficult to spot in the scenario description when shown on the screen. Gherkin editors do not place so much importance on white-space, and mixing Given-When-Then with a format where indentation is critical makes it very error-prone. The alternative is to use a format that implies structure in some other way, such as XML or JSON, but then the scenario itself becomes cluttered with formatting marks and difficult to read.
Come back next week for a new challenge!