User-Centred Design: How a 50 year old technique became the key to scalable test automation

Introducing scenario decomposition for BDD practitioners

Jan Molak
Jan Molak

--

Many people think that User-Centred Design techniques only have their place in user interface design and that Behaviour-Driven Development is all about end-to-end testing of said interface. As popular as those opinions may be, they’re also incorrect.

In this article, I’ll show you how applying ideas from UCD to software engineering and problem decomposition can dramatically improve the scalability and usefulness of your automated acceptance tests, reduce maintenance costs and shorten the feedback cycles.

Let’s walk through an example together so you can see how scenario decomposition works in practice.

Imagine that the online store we’re working for has just set a goal to reduce the costs of warehousing. They believe that a capability to encourage more sales of stocked products would help to achieve this goal, so they request a new feature to be added to the application — the feature to enable customers to use discount codes.

Teams practicing BDD could capture the above goal, capability and the examples demonstrating the behaviour of the new feature using the Gherkin syntax, understood by a tool such as Cucumber:

Feature: Discount codes for selected products  In order to increase the sales of the products I want to destock
As a sales manager
I want to enable customers to buy them at a discounted rate
Scenario: Using a discount code received from the online store Given that I have received the SMARTPHONE_15 discount code
When I shop for a smartphone
And I apply my discount code at checkout
Then I should see that the total price is reduced by 15%

If you’re familiar with “BDD in Action” or “The Cucumber Book”, you might recognise that the above snippet could be considered a “gold standard”:

  • It captures the business intent, which helps with efficient prioritisation and allows for any development work to be traced back directly to business objectives,
  • It is expressed in a business domain-focused language, which makes it easy to understand to both technical and non-technical audience and independent from actual implementation.

But here’s a question for you:

How do you transform a business-focused scenario into an executable specification of your system?

Contrary to popular opinion, the answer to this question is not that obvious. It is what separates reliability from “flakiness”, scalable design from code spaghetti and acceptance tests you trust from those no one cares about.

If you think about it, the trick here is to decompose a solution to a business problem so that it becomes an executable specification and can be implemented and executed against different interfaces of your system. Not just the UI.

Executable specifications at all levels

Many people still believe that the purpose of doing BDD is to write end-to-end acceptance tests of the user interface. This understanding is incomplete as it focuses on just one artifact of the BDD process, rather than its goal:

Because of how widespread this notion is, though, a common first instinct when automating executable specifications is to hook up a library like WebDriver to Cucumber step definitions and make it interact with the UI.

There’s a number of limitations this approach imposes upon you by tightly coupling your automated tests to the user interface:

  • First of all, it requires the interface to be present. This means that before you can execute even the very first example, you already need to invest your time in setting up all the infrastructure required to display the interface.
  • It constrains your thinking and makes you reason about your system in terms of the structure of the user interface rather than the business processes it needs to enable — which buttons do I need to click vs what do I need to accomplish?
  • It increases the maintenance cost of the codebase as tests executed against the UI are slower and more difficult to troubleshoot by their very nature of exercising the entire application stack.
  • It limits the application of your executable specifications. If you think about it, the user interface, the HTTP endpoints and so on are all just different ways for people to interact with your domain model. If so, then why shouldn’t you be able to exercise the same business scenario directly against the domain model, the HTTP endpoints and the UI? Why should you need several different test suites to maintain, even though they all verify the same thing? Could one test suite use different adapters to work with the different layers of your application?

It’s also worth mentioning that using the original Page Object pattern or its modernised version doesn’t make those problems go away either. That’s because the design goal of the pattern is to act as a starting point for teams new to test automation, not to give you a scalable way of driving the development effort of a software project.

Model the process, not the implementation

To present an alternative to coupling the executable specifications with the user interface we need to take a step back and see the bigger picture.

Instead of focusing on what buttons we need to click, let’s think about the process we’re trying to model: the tasks that the actor using our system would need to perform in order to achieve their goals.

The technique I’d like to show you has originally been developed in the late ’60 at the University of Hull for training process control tasks in the steel and petrochemical industries. The technique in question — the Hierarchical Task Analysis, is now widely used in a variety of contexts, including interface design and error analysis and, thanks to the Screenplay Pattern and tools like Serenity/JS – also designing executable specifications.

Hierarchical Task Analysis

In The Handbook of Task Analysis for Human-Computer Interaction, Professor John Annett writes:

A functional task analysis begins by defining goals before considering actions by which the task may be accomplished. Complex tasks are defined in terms of a hierarchy of goals and subgoals nested within higher order goals, each goal, and the means of achieving it being represented as an operation.

Notice that this approach is in stark contrast with a common test automation strategy, which focuses on either recording or scripting a low-level sequence of interactions with the system, rather than capturing the business process and vocabulary and helping you drive the design of a useful product.

Consider the scenario we talked about in the beginning:

Scenario: Using a discount code received from the online storeGiven that I have received the SMARTPHONE_15 discount code
When I shop for a smartphone
And I apply my discount code at checkout
Then I should see that the total price is reduced by 15%

If we recursively decompose each of the tasks required for the actor to “use a discount code”, we might end up with the following mental hierarchy of tasks and sub-tasks:

├── Given that I have received the SMARTPHONE_15 discount code
│ └── Remember a discount code SMARTPHONE_15
├── When I shop for a smartphone
│ ├── Visit the store
│ ├── Find a smartphone
│ │ ├── Visit the electronics and photo department
│ │ └── Pick a smartphone
│ ├── Remember the price of the smartphone
│ └── Add the smartphone to the basket
├── And I apply my discount code at checkout
│ ├── Proceed to checkout
│ └── Apply the discount code remembered
└── Then I should see that the total price is reduced by 15%
├── Read the total price of the basket
└── Ensure that the 15% discount is applied

As you can see, so far the definitions of the tasks, such as “Find a smartphone” or “Proceed to checkout” are not tied to any particular implementation of the interface and could be applied to either the programmatic interface of the domain model, a REST API or the web UI.

In fact, this very technique can help us build the domain model of our system guided by executable specifications before we even have the infrastructure in place to render the user interface.

To do that, we need to model the low-level interactions with our system in a similar way.

Modelling the interactions

In the Screenplay Pattern, I define an interaction as a “low-level activity directly exercising the actor’s ability to interact with a specific external interface of the system”.

This means that we could have alternative implementations of a given task, all achieving the same goal but exercising different external interfaces of the system.

For example, when the actor interacts with the web interface, the task to “Proceed to checkout” could be decomposed as follows:

└── Proceed to checkout
├── Click on 'View basket'
└── Click on 'Proceed to checkout'

Here, the Click interaction invokes the corresponding WebDriver API.

If the actor was exercising a HTTP API, “Proceed to checkout” could mean:

└── Proceed to checkout
└── POST basket to /api/checkout

And in the context of the domain model, the task could be translated to a call to the domain model classes:

└── Proceed to checkout
└── store.checkout(basket);

Having alternative, swappable implementations of business tasks, such as “Proceed to checkout” has a number of benefits:

  • Each task and interaction can be modelled as a class, grouped into packages and distributed using mechanisms like NPM modules or JARs. This allows for easy code sharing across projects and teams and helps to avoid a popular problem where multiple teams working on the same project duplicate the test automation effort.
  • An automated scenario could use tasks exercising the model or the HTTP APIs to bring the system to a desirable state before its user interface is exercised. This can dramatically reduce the time needed for data setup and is especially useful in driving the design and verifying the behaviour of complex workflow systems.
  • Your test framework could register the tasks being executed and include that information in the test report. That’s precisely what Serenity/JS is doing to help projects meet their audit requirements by answering how exactly each activity is performed.

There’s also another important benefit to modelling your acceptance tests using this approach: the number of unique tasks a user of the system can perform is typically at least an order of magnitude smaller than the number of possible interaction scenarios. By focusing your design effort on building a reusable, composable DSL rather than scripting interactions you create building blocks anyone on the team could assemble into high-quality automated tests.

Summary

Modelling the acceptance tests using techniques learnt from User-Centred Design and Hierarchical Task Analysis, in particular, helps you drive the design of a software system and focus on what really matters, that is, the actor interacting with your application, their goals and the tasks they perform to achieve them.

Reasoning in terms of the business processes rather than the UI structure can also help you design automated test suites that can be exercised against different external interfaces of your system, such as the domain model, the HTTP endpoints or the UI, improving their usefulness and reducing the maintenance costs.

To learn more about the practical implementation of this technique, visit the Serenity/JS website and try the tutorial. I also train teams and individuals around the world in BDD, software craftsmanship and test automation, so get in touch if you’d like to find out how this technique could help your team.

Many thanks to John Ferguson Smart, author of “BDD in Action” for reviewing the article and suggesting the title.

Enjoyed the reading? Please hit the 💚 below so other people will see this article here on Medium. You might like my other articles and tutorials too!

--

--

Consulting software engineer and trainer specialising in enhancing team collaboration and optimising software development processes for global organisations.