Agile Best Practice: Executable Specifications

Recently reviewed With a test-driven development (TDD) approach, part of the overall Agile Model Driven Development (AMDD) approach, your tests effectively become detailed specifications which are created on a just-in-time (JIT) basis. Like it or not most programmers don't read the written documentation for a system, instead they prefer to work with the code. And there's nothing wrong with this. When trying to understand a class or operation most programmers will first look for sample code that already invokes it. Well-written unit/developers tests do exactly this – they provide a working specification of your functional code – and as a result unit tests effectively become a significant portion of your technical documentation. Similarly, acceptance tests can form an important part of your requirements documentation. This makes a lot of sense when you stop and think about it. Your acceptance tests define exactly what your stakeholders expect of your system, therefore they specify your critical requirements.

Test-Driven Development (TDD)

The steps of test first development (TFD) are overviewed in the UML activity diagram of Figure 1. The first step is to quickly add a test, basically just enough code to fail. Next you run your tests, often the complete test suite although for sake of speed you may decide to run only a subset, to ensure that the new test does in fact fail. You then update your functional code to make it pass the new tests. The fourth step is to run your tests again. If they fail you need to update your functional code and retest. Once the tests pass the next step is to start over.

Figure 1. Test-first development (TFD).

I like to describe TDD with this simple formula:

TDD = Refactoring + TFD.

TDD completely turns traditional development around. When you first go to implement a new feature, the first question that you ask is whether the existing design is the best design possible that enables you to implement that functionality. If so, you proceed via a TFD approach. If not, you refactor it locally to change the portion of the design affected by the new feature, enabling you to add that feature as easy as possible. As a result you will always be improving the quality of your design, thereby making it easier to work with in the future.

Instead of writing functional code first and then your testing code as an afterthought, if you write it at all, you instead write your test code before your functional code. Furthermore, you do so in very small steps – one test and a small bit of corresponding functional code at a time. A programmer taking a TDD approach refuses to write a new function until there is first a test that fails because that function isn't present. In fact, they refuse to add even a single line of code until a test exists for it. Once the test is in place they then do the work required to ensure that the test suite now passes (your new code may break several existing tests as well as the new one). This sounds simple in principle, but when you are first learning to take a TDD approach it proves require great discipline because it is easy to “slip” and write functional code without first writing a new test.

Tests as Requirements

Figure 2 depicts a customer acceptance test description (it is shortened for the sake of brevity, it would need to be expanded with more steps to truly validate the functionality described). As you'd expect, the test has instructions for setting up and then running it. Additionally, a description, test ID (optional), and expected results are also indicated. Acceptance tests should be fully automated so that you can run them as part of your application's regression test suite. The FITNesse testing framework is a popular choice for doing so.

Figure 2. Acceptance test for validating a business rule.

ID T0014
Description Checking accounts have an overdraft limit of $500. As long as there are sufficient funds (e.g. -$500 or greater) within a checking account after a withdrawal has been made the withdrawal will be allowed.
  1. Create account 12345 with an initial balance of $50
  2. Create account 67890 with an initial balance of $0
  1. Withdraw $200 from account #12345
  2. Withdraw $350 from account #67890
  3. Deposit $100 into account #12345
  4. Withdraw $200 from account #67890
  5. Withdraw $150 from account #67890
  6. Withdraw $200 from account #12345
  7. Deposit $50 into account #67890
  8. Withdraw $100 from account #67890
Expected Results Account #12345:
  • Ending balance = -$250
  • $200 Withdrawal transaction posted against it
  • $100 Deposit transaction posted against it
  • $200 Withdrawal transaction posted against it

Account #67890:

Ending balance = -$500

  • $350 Withdrawal transaction posted against it
  • $150 Withdrawal transaction posted against it
  • $50 Deposit transaction posted against it

Errors logged:

  • Insufficient funds in Account #67890 (balance -$350) for $200 Withdrawal
  • Insufficient funds in Account #67890 (balance -$450) for $100 Withdrawal

Tests as Design Specifications

Similarly developer test can form the majority of your detailed design specification. Developer tests are typically written with xUnit family of open source tools, such as JUnit or VBUnit. These tests can be used to specify both application code as well as your database schema.

Are Tests Sufficient Documentation?

Very likely not, but they do form an important part of it. For example, you are likely to find that you still need user, system overview, operations, and support documentation. You may even find that you require summary documentation overviewing the business process that your system supports. When you approach documentation with an open mind, I suspect that you will find that these two types of tests cover the majority of your documentation needs for developers and business stakeholders. Furthermore, they are a wonderful example of AM's Single Source Information practice and an important part of your overall efforts to remain as agile as possible regarding documentation.

Agile Modeling