03/07/2012

TDD in practice: ABC's of TDD

Prelude

This is my humble opinion of TDD:
  • PRO: It's super easy to get equipped with a testing framework and learn the basics. 
  • CON: It's unbelievably hard in comparison to start unit testing and applying TDD. 
Hopefully, by now we have a common understanding of what we should be testing. (TDD in practice: Where does it fit in? - We can only test against known results and behaviours).

Let’s reiterate and approach the same conclusion from a different angle:

Method A: void SendEmail()
Analysis: The above method takes no parameters and returns no values. The only clue we have of its purpose and intended behaviour is the method name.
Conclusion: Not testable

Method B: int Sum(params int[])
Analysis: The above method takes parameters and returns a value. We can imply that because the parameters are numeric of nature and the methods' intention is to sum up these values - the expected result should be the sum of the parameters.
Conclusion: Easily testable

Although method A is not testable - be not discouraged - it doesn't imply it can never be.

Test Driven Development revolves around:
  1. Red: Write the test the way you want the implementation to work. 
  2. Green: Implement the functionality required to make the test pass (with a focus on loose coupling and high cohesion). 
  3. Refactor: Refactor your implementation and enforce Separation of Concerns. 
  4. Repeat
Relating to the above-mentioned:
  • (SoC)Separation of Concerns: The process of separating a computer program into distinct features that overlap in functionality as little as possible. 
  • (LC)Loose coupling: In computing and systems design a loosely coupled system is one where each of its components has, or makes use of, little or no knowledge of the definitions of other separate components. 
  • (HC)High cohesion: In computer programming, cohesion is a measure of how strongly-related each piece of functionality expressed by the source code of a software module is. 

I suggest everyone should follow these steps when applying Test Driven Development:

Step 1: Investigate and Plan
Our downfall in regards to TDD is that we've been conditioned via tutorials to think that applying TDD is simple and that we should jump head first into coding.

You have to start planning out the feature you want to implement.

I have found that enforcing the Single Responsibility Principle has allowed me to identify different components. The Liskov Substitution Principle makes for a good guideline to determine if an abstraction should be implemented as an interface or using the Template Method Pattern.

Once you have set out some behavioural guidelines (user stories) and have a pretty good idea of the functionality you have to implement and have identified reusable components - then you are ready to go.

Relating to above-mentioned:
  • (SRP)Single responsibility principle: Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility. 
  • (LSP)Liskov substitution principle: Concrete implementations of abstractions should be interchangeable without altering any of the desirable properties of that implementation (correctness, task performed, etc.) 
  • Template method pattern: A template method defines the program skeleton of an algorithm. One or more of the algorithm steps can be overridden by subclasses to allow differing behaviours while ensuring that the overarching algorithm is still followed. 
  • User Story: A sentence that captures the context (who, what, where, when) and the expected behaviour (then). In agile development (XP,SCRUM etc.) the form is usually As a <user>-When <condition/s>-Then <expected result/s/behaviour/s>. In behaviour driven development (BDD) and acceptance test driven development (ATDD) the form is usually Given<context>-When<condition/s>-Then<expected result/s/behaviour/s>. 
Step 2: Red-Green-Refactor
I'd like to revisit Method A from the prelude.

If Method A was fully implemented within a single method, then it would have been in an untestable state.

We would approach this problem by refactoring in-scope functionality into testable methods and abstracting out-of-scope functionality into implementations that can be tested independently.

This allows us to test the method by testing the abstracted implementations and refactored in-scope methods the method to be tested uses.

Abstract out-of-scope functionality that can be tested independently
If the responsibility of certain functionality lies outside of the scope of the class (SRP), functionality should be refactored out of the method/class into a concrete implementation. The concrete implementation then needs to be abstracted with an interface or an abstract class using a template method pattern (DI, TMP).

This rule also highlights the Dependency Inversion Principle, which is a specific form of decoupling that's very useful while applying TDD. You can then write unit tests for said low level implementations and use Stubs/Fakes/Mocks within your higher level implementations (that depend on the abstraction) to verify behaviour of said high level components.

The implementation of the abstraction can then be supplied via the constructor or via parameters to the method (SP).

Relating to above-mentioned:
  • (DI)Dependency inversion principle: High level implementations should not depend on low level implementations. Both should depend on abstractions. (A high level implementation should depend on the abstraction of a low level implementation) 
  • (SP)Strategy pattern: The strategy pattern allows you to supply a strategy (behaviour) to a high level implementation at run-time. 
In hopes of not making this post too long, it will have to be cut short for now. It is evident that a good understanding is required of SOLID principles, GRASP and design patterns to properly apply Test Driven Development.

Test Driven Development is awesome - it promotes good coding practices and quality code. Once you know implementations adhere to desired behaviours, the fear of maintaining (by refactoring) and extending a system almost entirely disappears.

There are 2 advanced fields within TDD - Behaviour Driven Design and Acceptance Test Driven Design (ATDD) - albeit outside the scope of this post, carry great benefits. For example, ATDD tests double as confirmation that a specific feature is done - which serves as an asset within project management.

My opinion
I still consider TDD as an implementation detail - it's very useful within the context of its application (implementation of functionality).

There are other fields to explore, such as:
  • Architectural design (like CQRS, layering, distributed systems, client-server, online-offline) 
  • Project management (prince II, agile/scrum, xp) 
  • Program design (domain driven design, metadata driven design, model driven design, design by contract, AOP) 
Perhaps we are not intended to learn everything - but I believe we should know enough to fend for ourselves.

Unlike the common expression - a chain is only as strong as its weakest link - it's the average skill within a programming team that dominates the quality of implementation within a project.

No comments:

Post a Comment