Test-Driven Development and The Power of Self-Validating Code

Georges Lteif

Georges Lteif

Software Engineer

Last Updated on September 7, 2022.
Subscribe now to stay posted!
About Us
7 min read

1. Overview

Test-Driven Development, or TDD, is a discipline that advocates writing unit test cases before any production code.

While some influential people in the software community are fully committed to TDD, others have declared it dead.

In this article, we examine Test-Driven Development from a micro and macro point-of-views.

The examination at the micro level will allow us to see the immediate advantage (as well as caveats) of TDD. These sections look at how TDD is supposed to work, what challenges it faces, and what opportunities become available.

On the other hand, the macro examination will show us how instrumental TDD can be when trying to improve your SDLC processes, especially when it comes to optimizing resource usage and shortening the delivery timeframes while preserving quality.

Examining TDD would be part of an overall effort to achieve Operational Excellence, the term that combines quality, efficiency, and flawless execution.

2. Table of Contents

3. TDD – The Micro Examination

3.1 What is TDD

Test-Driven Development is a discipline in software development that consists of writing failing unit tests before developing the actual production code to support them.

A Word of Caution on Unit Testing

TDD practices often include writing unit tests as the primary type of software testing tools to be used. We have argued at length about the utility, or lack thereof, of unit testing in certain situations, particularly when the “unit” is very small (class or function level).

For the purpose of this article, when we use the term “unit testing”, we generally refer to either unit tests or other types of tests, such as System Integration Tests, depending on applicability.

I found Bob Martin’s definition of TDD in the form of three laws quite useful. This is what we will be using here as well:

Law 1

You are not allowed to write any production code unless it is to make a failing unit test pass.

Law 2

You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures.

Law 3

You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

3.2 Advantages of Using TDD

TDD presents three distinct advantages.

#1 Avoid Wasting Effort Debugging Your Code

Debugging is an effort-intensive and time-consuming process. You should not do it if you can help it.

This is a direct implication of the three laws where you are not allowed, by design, to write any code that needs heavy debugging.

The rationale is as follows: if your code was working one minute ago, all you have to do to debug it would be to undo those last few changes. You don’t have to use the debugger at all.

Granted, that does NOT mean you will never ever have to use the debugger; it just brings its usage to a minimum.

#2 Decent Battery of Tests

You have completed writing your production code and now have decided to write some unit tests because only it’s part of the process. Your heart will not be into it (since you know the code is already tested and working). TDD helps you avoid that.

A battery of incomplete test cases, full of holes, or with less-than-ideal test coverage adds little value to your development practices. Passing those tests means very little.

On the contrary, having a decent battery that can give you enough confidence to release into production is essential for improving your Agile practices.

Another equally important advantage of decent testing capabilities is keeping your code clean through regular refactoring exercises.

Clean code means running a well-oiled machine. Bad code will slow you down until the cost of ownership becomes too prohibitive.

#3 Designing Testable Code

Using TDD can push developers to design and develop code that can be easily tested. This is somehow a natural consequence of the three laws of TDD.

Ideally, you want all your code to be testable so that you can pass that task to a test automation framework. Manual testing is one of the primary sources of waste that can be easily eliminated.

3.3 Challenges of Using TDD

TDD, when incorrectly implemented, will present some daunting challenges.

#1 Cost of Ownership

A growing body of code, perhaps a few times the size of the production code, whose sole purpose is to test another body of code, will introduce a hefty price in terms of cost-of-ownership.

Even when the unit tests are exquisitely designed and extremely robust, they still require effort to create and maintain, which can be redirected to develop code that customers are happy to pay for.

#2 Time-to-Market

The outcome of Test-Driven Development, when coupled with badly designed software testing strategies, is a body of test scripts that nobody wants to maintain. This can become a major drag on your project momentum, i.e. lower-profit and slower time-to-market.

A poorly-implemented test strategy can slow you down considerably. You can check the list below for signs of a crappy implementation:

  1. Slow execution. Ideally, a full test suite should complete in a few minutes, otherwise, it’s unusable.
  2. Testing implementation rather than functionality. This happens when you couple code designed for testing your software with code from production.
  3. Plenty of mocking (ideally, you want to run a test on an environment that is as close to production as possible). To make things worse, mocking seriously couples your test code with the production one.

3.4 Taming the Challenges

You can engineer your test strategy and automation framework smartly to mitigate these problems.

The following guidelines for implementing a powerful test strategy can be applied:

  1. Use unit testing ONLY where applicable. This means redefining the “unit” in “unit testing”.
  2. Avoid mocking (databases, external systems, filesystems, etc.) at all costs.
  3. Test functionality, never implementation. This is otherwise known as Behaviour-Driven Development.
  4. Design a simple, robust, and automated framework leveraging existing tools on the market. Look at DevOps practices for inspiration.
  5. Design code that can be easily tested (more on this later).

In summary, the challenges presented by poor implementation of TDD are neither unavoidable nor unsurmountable.

4. TDD – The Macro Examination

Test-Driven Development is a discipline that will allow us to glue together some of the practices designed to reduce waste in the development and testing stages, the waste reduction being the number one target of Operational Excellence.

The next subsections will explain how this can be done.

4.1 Streamlining Development and Testing Efforts

The traditional approach to testing new code invariably involved QA having to wait until all the development (and its testing) have been completed before carrying on with any of their activities.

It is true that during this time, testers usually occupy their time with preparing test plans, perhaps even writing down (or creating) the specific test cases.

All that is slow admin work, and times are much less stressful until testing starts.

During the testing period, the development team usually takes an informal break, waiting testers to uncover any bugs.

The sequential nature of development and testing can be a major source of waste, and the only way to eliminate it is by creative redistribution and reorganising testing activities.

This is where TDD comes in. Have a look at the below diagram.

test driven development tdd
Test-Driven Development in Operational Excellence

The solution we propose is as follows:

Stage 1

Once the internal and external stakeholders sign off on the business requirements, developers and testers start writing test cases. Test cases from both parties are integrated together in a seamless fashion, typically through Source Code Versioning tools.

Stage 2

Developers using Test-Driven Development practices start writing production code and using the test cases generated in Stage 1 to validate their changes. Testers simultaneously enhance the test suites created by the developers and integrate them together.

Stage 3

The production code is finalised at the end of the development cycle, and all the test cases are now passing. The Development AND Testing stages would are simultaneously concluded.

4.2 Embedding Quality Controls in the Delivery Process

Operational Excellence involves a relentless drive for better quality products. This squares nicely with the way Test-Driven Development works.

To illustrate how that works, here is a small example.

You can include a step in the development and testing processes that says the following: Once a bug has been found, it is required that the developer or tester who uncovered it add a test case that tests that specific scenario.

The developer can then patch the code to make that test case pass. Once it’s done, there is no need to test it again.

A release can be deployed into production ONLY when it passes the full suite of regression tests, and your test framework should provide a report to that effect.

4.3 Continuous Improvement

The first version of any code that you write is usually crap. Everybody knows that. Everybody acknowledges the necessity of getting things done, so you can get away with it the first time.

Sometime later, perhaps when trying to extend that code, you think there might be a better way of writing it. This process is called refactoring and is really what keeps your code healthy.

No developer will have the courage to refactor any code unless she is confident that there is a battery of test cases that can easily uncover any broken features.

Continuous improvement requires confidence to be present, and the latter depends on how well you apply TDD.

5. Conclusion

Test-Driven Development can be a powerful discipline with the right tools when applied properly.

TDD is essential for achieving Operational Excellence by A) ensuring an optimal distribution of resources, B) making the most of their limited time, and most importantly, C) staying on top of your quality issues.

DO NOT religiously follow procedures (such as testing internal classes and mocking databases) that were invented later on by people who perhaps did not understand the author’s views as he meant them.

These serve no purpose but to slow you down until you finally give up on TDD.

6. Further Reading

Two great talks on the subject.

Uncle Bob Martin on TDD
Challenges of TDD by Ian Cooper