What I offer here is an approach that is more reductive, but still iterative – still follows a process of refinement, still allows rapid turnaround, maintains high test coverage, and preserves the ability to make changes quickly.
Key goals for the approach are,
- Keep change impedance low - to preserve agility;
- Maintain design integrity from the beginning, and throughout - to preserve maintainability and robustness.
In my approach there are phases as well. Below I describe each phase and its activities.
For stories that are simple to implement, simply use a BDD or ATDD approach. For complex stories, augment BDD or ATDD with the following:
1. Understand the story:Given a story, at any level - epic level, feature level, or "sprint" level,
- Seek to understand the intent. Don't pay attention to details, such as inputs and outputs, unless those are the actual requirements at the highest level. Try to understand what is being accomplished by the story.
- Seek to understand existing components that need to interact with the story: how do they behave? What issues do you see?
2. Write specs for the story:Steps 2.a and 2.b may be done in parallel. The person(s) who does step 2.a should be a different person than the one who does step 2.b. Otherwise, misunderstandings about the story will be present in both the test specs and the application design.
2. a. Write behavioral test specs for the story:For each of the story's acceptance criteria, identify test scenarios that collectively verify the criteria, including "edge cases" and "error paths".
Organize the tests by feature (not by story), and tag scenarios with the story IDs that they pertain to. To facilitate this, it is often useful to maintain a "feature map". Note that over time, stories will tend to require additions and changes to existing feature test specs. It is sometimes also the case that a story is obsoleted by a new story, and so the old story's test scenarios are no longer applicable and should be removed, modified, or enhanced.
Tag the "error path" test scenarios as such.
2. b. Create story's initial design:
- Identify a set of algorithms that are needed to implement the story. Write the algorithms down in a precise notation: pseudocode and, if applicable, mathematical notation. Define any data structures or object structures that are also needed, and diagram them using Simple Modeling Language. The design's paths should be complete, in that error paths and edge cases are all identified. Edge cases should all be completed, but error paths can be left empty for now.
- Mentally validate the design by walking through it and refining it, until you are confident that it is correct.
- Request peer review of the algorithms.
3. Translate specs into code ("elaboration"):Steps 3.a and 3.b may be done in parallel. The person(s) who does step 3.a should be a different person than the one who does step 3.b. Otherwise, misunderstandings about the story will be present in both the tests and the application code.
3. a. Code the story's behavioral tests:Write source code that implements the story's test scenarios.
3. b. Code the story's initial design:During this phase the initial attempt at coding the story is performed. The code is merely a translation of the design's structures and algorithms into a computing language. This translation should be straightforward and occur quickly.
4. Validate and refine the design:
- Execute the "happy paths" of the behavioral tests that cover the story's acceptance criteria; also run behavioral tests that cover any features that might have been impacted by the code changes. Run these as functional integration tests, using actual deployed components for all components that are potentially affected. The tests should be at multiple levels, including the integration level, end-to-end, inclusive of all affected parts of an application or platform. The precise testing approach is a matter of testing strategy, but tests should be automated, and runnable locally. Some outer levels may be mocked, but as little mocking should be used as possible before considering a story done.
- If the design appears to be valid, based on the results of the tests, complete the code by filling in the error paths. If the design has issues, iteratively refine the design and the code and tests until you feel that the design is correct and the tests pass. Keep the design in sync with the code.
- Run the behavioral tests that validate the error paths.
- The code and design are not "done" until the code and design have been verified to match, the tests have been verified to have sufficient coverage, and all of the tests pass - including any that existed beforehand but were "broken" by the code changes introduced by the story.
ReflectionThis is a test-first approach. It is also iterative, and it is an Agile approach:
- It is story-centric.
- The tests are maintained over time.
- While the approach is design-centric, it is incremental: design and code changes are made on a story-by-story basis.
- It is easy to make changes to the code in the future, because the design has high cohesion and is accurately and concisely documented. Thus, both agility and maintainability are present.
- The process is mostly reductive and top-down, at the level of the story's requirements, but proceeds bottom-up when the tests do not pass and the design must be revisited to see why.
- The inputs and outputs are merely an aspect of the design - they are not central.
- The design is as important as the code: a story is implemented by making changes to both the design and then the code, and then iterating with both until the tests pass.
- The design and code are kept in sync.
- Risk is managed by managing the test coverage.
- The design tends to retain high cohesion, since design changes are thought through carefully before making code changes.
- The code tends to be highly robust and maintainable, and error cases are covered from the outset.
- The resulting code is easy to understand, because it is supported by an algorithmic specification, and the code matches the algorithm.
- Deep thinking is encouraged: instead of going straight to code, one thinks things through thoroughly before coding.
It is not necessary to do this for all of the parts of an application: only for the complex parts. It should be done at the highest level, for the aspects of a system that "glue everything together". It should also be done for every complex aspect.
When designing a feature, one should look beyond the current story: determine the length of “runway” needed. This is a matter of judgment and experience. One should avoid the anti-patterns of “gold plating”, and “adding unnecessary complexity”, but also avoid “building an inadequate foundation”.
ExamplesA simple example is provided here.
You will see that the algorithms are named, and if you search the code repo for those names, you will find the implementations.