TDD Workflow Example

Last modified by chrisby on 2023/12/08 07:51

Introduction

A complete TDD workflow can be hard to grasp for beginners, especially when combined with other practices and tools. So I have written down my usual development workflow, which combines TDD with refactoring and versioning using Git.

The Role of Git

The goal of TDD is to take many small, simple, and safe steps iteratively. A save step, meaning all tests pass, should be physically saved to provide the ability to compare current changes to the last safe step, or to roll back. If something goes wrong, the last safe step is not far away, and finding the problem in a small snippet of changed code makes debugging much easier.

  • When a test passes, stage the changes via git add .
  • When a test passes and is complete, commit the staged changes via git commit -am "..."

Combining TDD + Git + Refactoring

  1. Create a new test, add an assertion function. Implement the test code and run it frequently.
    • If the test fails at any point, proceed to step 2.
    • When the test is complete and has not failed, perform a check for an 'Always-Passing Test'*.
  2. Implement production code until it passes all tests. Stage changes.
  3. Refactoring: If code quality is sufficient proceed to step 4. Otherwise, refactor and run all tests.
    • When all tests pass, stage the changes. Repeat step 3 until the code quality is satisfactory.
    • If one or more tests fail, fix the production code.
  4. Test Completeness Check
    • If test is incomplete, implement test code until it fails and return to step 2.
    • If test is complete, commit the staged changes and proceed to step 5.
  5. Feature Completeness Check
    • If feature is incomplete, go to step 1.
    • If feature is complete, you are done.

(When teaching this workflow to beginners, I have found it helpful to physically print it out, place it next to their keyboard, and give them a little figure like a paper clip to place on the step they are in. They do the tasks described in the steps and move the paper clip on their own. This way they can practice the flow correctly even if they are working alone).

*Always-Passing Test

An always-passing test is a test that passes independently of the production code it tests. They are problematic because they indicate working functionality while verifying nothing, and they are hard to detect. Avoiding them should be a major concern. A common cause of always passing tests is the absence of an assertion in the test, so in the workflow above, the assertion is added to the test first to prevent such cases.

If a test is suspected of always passing, mutate the production code to try to trigger a failure of that test. One approach is to simply change the return value of the function being tested to a definitely wrong return value, often null, empty collection, bad data, etc. If the test fails, all is well, we can undo the production code mutation and move on. Otherwise, the test is proven to always pass, so it must be debugged until the cause is found and fixed, which is achieved when the test can finally fail.