Test-Driven Development is a development approach that relies on unit tests to drive the development and - more importantly - the design of applications. In order for software to be considered "testable" it must be adequately decomposable, allowing tests to target specific units of logic (e.g. classes, methods, or even specific portions of a method). The requirement for decomposition drives loosely-coupled, "SOLID" architecture which embraces OO principles.
Benefits of TDD
"True", dogmatic TDD – also called “Test-First Development” - dictates that code may only be written to satisfy a failing test, and only the bare minimum code is written to make that test pass. TDD provides several benefits:
- Loosely Coupled Architecture
The need for tests to completely control a component’s environment drives loosely-coupled components, which – when extrapolated to the system as a whole - leads to a loosely-coupled architecture.
- Focused Development
Scope of code currently written is limited to the needs of the immediate business requirement. If more code is needed to support future requirements, that work is delayed until future tests will drive that development. This keeps developers focused solely on the task/requirement at hand.
- Regression Test Suite
Unit tests act as a regression test for the remainder of the application's lifetime. And, since dogmatic TDD states that no code can be written without a test to back it, this implies that an application developed using TDD will never have less than 100% code coverage (the number of lines of production code covered by unit tests). That said, true 100% code coverage is very impractical for a number of reasons.
- Documentation
Unit tests are merely code that executes other code, and act as extensive “real-world” examples of how components are used, thus providing a form of documentation.
- More Productive Debugging
Since “units under test” are adequately isolated and have at least one unit test focused specifically on them, it is often incredibly easy to locate a failing component by looking for the failing test(s). What’s more, since unit tests are executable, debug-able code, developers can easily attach their debugger to a specific test and execute it.
Detriments of TDD
- More Code
By definition, the test-first methodology produces a test suite which – at a minimum – doubles the size of your solution’s codebase. This leads to: - Increased Lines of Code
Assuming it takes at least the same amount of time and effort to write test code as it does to write production code, TDD literally doubles the time spent writing code produced (and the corresponding time it takes to write said code).
Perspective: In terms of the SDLC, the time spent actually writing code is only a fraction of the Implementation phase – much more time is spent on developer testing/verification, debugging, and bug fixing. Taking this into consideration, the increased coding time introduced by TDD is easily offset by more targeted and productive debugging, not to mention lowering the number of bugs to begin with (both in the long term and the short term!).
- Increased Cost of Change
Since unit test code is so closely tied to production code, changes to business requirements mean that both production code and its corresponding tests will need to change. The implications of this change are the same as the preceding bullet: writing and changing code is only a fraction of the SDLC Implementation phase.
- Even More Code!
Developers can easily become carried away with writing an abundance of unit tests in an effort to achieve the highest level of code coverage they can. The ROI of additional unit tests against an already-tested component can drop quickly as the number of tests goes up.
- False Sense of Security
A high level of code coverage can provide a false sense of security if developers are convinced that the level of code coverage equates to the nonexistence of bugs. However, code coverage only measures whether or not a line of code was executed, not how it was executed (i.e. under what conditions). Consider a highway system: just because you drove your car over every foot of road doesn’t mean those same roads will react the same when traversed by a bus.
An Example of TDD in Action
Business Requirement
The application must produce the sum of two numbers
Step 1: Write a failing test
public void ShouldProduceSumOfTwoNumbers() {
Assert.AreEqual(4, new Calculator().Sum(1, 3)); FAIL!
}
Step 2: Write just enough code to make the failing test pass
public class Calculator {
public int Sum(int number1, int number2) {
return 4; PASS!
}
}
And we’re done! Except that what we’ve produced is a method which returns a hard-coded value! This situation is easy to rectify: write another failing test against the same component.
Step 3: Write another test which specifies a different set of parameters
public void ShouldProduceSumOfTwoOtherNumbers() {
Assert.AreEqual(5, new Calculator().Sum(2, 3)); FAIL!
}
Since the new test asserts a different result based on different inputs, this test fails because the initial implementation of the Sum method returned the hard-coded value different than what this new test expects.
Step 4: Revisit and refactor the production code to pass the new test
public class Calculator {
public int Sum(int number1, int number2) {
return number1 + number2; PASS!
}
}
Though simple and contrived, this example effectively demonstrates the process – and more importantly, the mindset – behind Test-Driven development.
TDD and UI Development
As you move further away from the statically-typed compiled “backend” code and closer to the UI, the unit tests associated with these parts of the system tend to introduce less resilient and reliable methods such as string comparison. As a result, the cost of creation and maintenance grows exponentially.
A word of warning: because of this exponential cost and loss of strong reliability, the ROI of the TDD approach often becomes negative when applied to the UI layers. It is often better to drive the testing of UI layers by professional (QA) testers as they will likely be applying these approaches anyway.
TDD vs BDD (Behavior-Driven Development)
Test-Driven Development – as its name implies – relies on unit tests to drive production code. Ideally, these unit tests derive from business requirements, however strict adherence to the Test First approach often means that developers end up writing unit tests to allow them to write code and ensure that that code works… not that it meets any kind of business requirements.
Behavior-Driven Development (BDD) is a philosophy grown from TDD which focuses on the software requirements of - and human interaction with - “the business” to deliver software that provides value to the business. Though the two approaches are variations on the same theme and the differences are subtle, BDD aims to please customers by satisfying their (ever-changing) requirements, as opposed to simply focusing on “working code”. This usually means less stringent code coverage requirements
Resources
General internet searches for the concepts in this document such as “test driven development” and “behavior-driven development” rarely leave much to be desired. I have not come across many bad resources in regards to Test-Driven Development. Unfortunately, because these are heavily philosophical concepts that go far beyond simply learning a language or syntax, the only way to truly understand it is to find a mentor and do it (and learn from your mistakes).
Regardless, here is a short list of some of the better resources I’ve found recently:
· Test-Driven Development Wikipedia (yes, it’s a great resource!)
· Test Driven Development Ward Bell, et al – the grandfather(s) of XP
· Guidelines for Test-Driven Development Jeffery Palermo
· Introduction to Behavior-Driven Development BddWiki
· Introducing BDD Dan North
· The Art of Agile Development: Test-Driven Development James Shore
· What is a Unit Test? Jess Chadwick
· Test-Driven Development: By Example Kent Beck
· Working Effectively With Legacy Code Michael Feathers (applying TDD to existing codebases)