In 2020, I finished a series of 72 blog posts that expanded on the first set of “Seven Strategies…” for each practice in my book Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software. I included two sets of “Seven Strategies…” in my book and so I am expanding on the second set here. The next seven blog posts are expanded versions from “Seven Strategies for Splitting Stories”. You can find the original post here.
Many years ago, I asked my friend and Agile coach Darian Rashid how he likes to split stories. Without hesitation, he replied that he prefers to split stories on acceptance criteria. I thought this was a brilliant response.
What are ‘acceptance criteria?’ It is how you know that you’re done implementing the story. Sometimes people ask me at what level of abstraction or detail should a story be and my response is also at the same level of abstraction as the acceptance criteria.
Our acceptance criteria are the goals of our story. They’re what we’re trying to accomplish with the story and represent the main value of the story.
If there are multiple acceptance criteria, then it usually means that the user wants something that can be composed of other little things. If this is the case, then we can split these little things into separate stories, most typically. Remember, our goal is to make our stories as small as possible while still providing some value.
And each acceptance criterion corresponds or could correspond to its own acceptance test, which embodies that criterion in the form of an assertion.
There are several acceptance testing frameworks to automate the process of creating and running acceptance tests. Some frameworks are HTML-based such as FIT. Others are based on a more generalized language called Gerkin, such as SpecFlow or Cucumber. The purpose of these higher-level acceptance test languages is to allow the creation of acceptance tests that test acceptance criteria and not the implementation details associated with the acceptance criteria.
These higher-level acceptance test languages allow acceptance tests to be more durable so that when code is refactored, the test doesn’t break. Refactoring is when we want our tests to service us the most because that’s when we’re likely to make a mistake in code, so we want to write tests in such a way that they support us when we refactor code, rather than break. The way we do this is simply to test the behaviors for the end results that we’re trying to create rather than incorporating implementation details in our tests.
I know that’s easy to say and sometimes not so easy to do. We may realize that we need to reach into implementation somewhat to feel confident that we’re actually testing a feature. This is not uncommon. I’m really doing a bit of quality assurance in the process. That’s not bad but it helps to identify that we’re writing tests in a slightly different way because those kinds of tests might break when we refactor our code, so they may require additional maintenance but what they give us is an additional level of assurance that our code is working as we expect.
However, acceptance tests should have as few implementation details as possible. In fact, by their very definition, they should be free of implementation details. That’s what an acceptance test is. We’re testing that a system does something and not testing how the system does it.
I find that it’s highly useful to think about developing software based upon acceptance criteria. It helps me be more immersed in the domain that I’m building in and create systems that are better encapsulated. I’ve come to recognize that acceptance criteria are the main driving forces that I use for implementing the behaviors that my customers request.
Previous Post: « Bonus: Free Chapters from my book, Beyond Legacy Code: Nine Practices to Extend the Life (and Value) of Your Software
Next Post: Minimize Dependencies »