Software engineering is cluttered with technical terms, methodologies, and frameworks. Pick any of these, and someone will tell you it’s the best and only way to produce reliable software quickly. As a developer, you need to find the tools that work best for you and your team.
Test-driven development (TDD) is, in my opinion, one of the most practical ways of improving both quality and timeliness.
What is Test-Driven Development?
As the name suggests, you write tests before you even begin to write code. Kent Beck, who was instrumental in defining Agile Software Development, discovered the idea in an old programming book and refined it. TDD is one of the techniques that make up his Extreme Programming methodology.
Kent’s book, Test-driven Development by Example, describes the concept in detail.
TDD refers to program development and unit testing rather than to later test phases such as integration tests. Acceptance Test-driven development (ATDD) is a related methodology that applies similar principles to systems design.
TDD is also known as Test-First Programming.
How Test-Driven Development Works
Kent Beck, in his article Canon TDD, states the objectives of TDD as:
Making sure everything that used to work continues to work.
Checking that new behavior works as expected.
When these conditions are true, you’re ready to start the next change. The process is as follows.
Step 0: Create a list of tests
Make a list of what the new system, or the changes, should do. These are known as behaviors. Include variants of each behavior by asking: What if….? For example, what if the customer’s details don’t exist in the database? You should have a separate item on the list for each variant. You should also include tests to prove the changes haven’t broken the original system.
The list should consist of small goals. For example, “Can the customer place an order?” is too large a goal. You should rather break it down into a series of smaller items such as, “Can the server respond to an HTTP request?”
At this point, avoid implementing design decisions.
Step 1: Pick an item and create a test for it.
Choose a single item from the list. With practice, you’ll learn to make wise decisions regarding the testing order. It’s a good idea to start with something you know you can implement easily.
Write an automated test for this item. It should be a full test, including:
Setup tasks.
Assertions that allow you to check that all aspects of the test work.
Cleanup tasks.
The test must work stand-alone; it shouldn’t rely on any previous tests.
At this point, do not write more than one test. Some developers make the mistake of writing tests for all the items on the list at the beginning. This quickly becomes tedious, and you’ll be tempted to skimp. It’ll also confuse you: you need to keep your mind focused on this one single item. You need to fully understand it when you start coding.
There’s also the chance the list may change as you get deeper into development. If it does, you will have wasted your time writing tests that you might not need.
Step 2: Make the test pass.
Run the test. You may say: “Hey, but I haven’t written any code yet! How’s the test going to pass?” Good point. It should fail. This is important because it proves your test works – it’s not just generating a false pass.
Now, write the bare minimum of code to make the test pass. Nothing more, nothing less. Don’t do any refactoring at this point, and don’t make the code pretty. Just make the test pass. If the test fails, fix the problem and retest until it passes.
Step 3: Refactor
Once your test passes, you can refactor your code – both the new code and any code you’ve written previously. Refactoring involves improving the internal structure of the code without changing its behavior. Get rid of duplications, improve readability, and simplify the code if needed. This is the time to make any design decisions.
You should then rerun your test to make sure it still works. When you’ve successfully completed this step, mark the test off your list.
Repeat steps 1 to 3 until you’ve marked off all the items on your list. The process is an iteration of Fail (Red), Pass (Green), and Refactor (Blue).
You can visualize the entire process as follows:
What are the pros and cons of Test-Driven Development?
Investing early in Test-driven development can help teams deliver high-quality software within time and budget constraints. Although Test-Drive Development requires initial investment in creating additional tests, this effort is often offset by reduced challenges in later testing phases. Here’s a quick look at the pros and cons of Test-Driven Development:
Pros of Test-driven development:
It forces the developer to write unit tests for everything. This has two advantages:
The finished software is more reliable.
The tests are already in place if future changes require regression testing.
It ensures the developer focuses on and fully understands the user’s requirements.
It eliminates unnecessary code. The developer writes minimal code to produce a specific result.
It speeds up the debugging process. Since each test relates to a small chunk of code, it’s easy to find the source of any problems.
It encourages simplicity as defined by the rules of Agile programming.
It reduces the number of defects in the finished product and the number of issues the team must deal with in later test phases.
Cons of Test-driven development:
Creating and maintaining the tests adds to the developer’s workload.
If the programmer has misunderstood the specifications, TDD won’t solve the problem.
Not all team members may agree to adopt it, resulting in a lack of team cooperation.
When starting a project the requirements are still being designed, so it's hard to make tests that capture the indented requirements. As a result, many tests may get thrown away.
Test-Driven Development Tools
To implement TDD successfully, you need to automate all your tests. Not only do you need to test the current step, but you need to run all the previous tests to make sure your changes haven’t broken anything. Any future regression tests can use the same scripts.
Before you start, you should have a good test framework in place, although it’s possible to build the tests into your code. You may need to create fake or mock objects to test external processes.
Most languages, including C#, Java, and Python, have a choice of excellent tools for unit testing. Here are some test frameworks you might choose, depending on the language you’re building with.
Variations of Test-Driven Development
There are two major schools of thought regarding the order of testing. These are known as Outside In and Inside Out. Outside In may work better for large and complex systems, whereas Inside Out works well for simpler systems.
Outside In Test-Driven Development
This is also known as Top-Down TDD, London School, and the Mockist Approach. Tests begin with user scenarios. The system's entities are wired together from the beginning, which often means you need to create mock entities to begin with and later replace them with real entities. An entity is only created when it’s requested by another entity.
In full-stack development, the tests should work down through the layers, starting at the user interface. You’ll only develop functionality at a lower layer as and when the tests need it.
Inside Out Test-Driven Development
This is also known as Bottom-Up TDD, Chicago Style, and Classic Approach.
Tests begin at entity level. This approach reduces the need for fake and mock objects. The downside is that you sometimes need to rework tests if you find that entities don’t integrate well. It can also be challenging to see which entity is at fault.
Test-Driven Development and Other Software Methodologies
Test-Driven Development and Agile
As mentioned earlier, TDD is a feature of Extreme Programming, which is one of the frameworks of Agile. It fits perfectly with the Agile principle of ‘inspect and adapt’. The software is inspected through testing, and adapted to make the test pass.
Test-Driven Development and Behavior Driven Development
Behavior-Driven Development (BDD), also known as Spec by Example, is a methodology defined by Cucumber – a testing and collaboration tool. Users, testers and developers collaborate to define the system’s behavior in detail. Cucumber provides an English-like language called Gherkin to achieve this.
BDD works well with TDD since the Gherkin specifications are a good source of information when preparing your test list.
Test-Driven Development vs Acceptance Test-Driven Development
Acceptance Test-driven development (ATDD) applies some of the principles of TDD to systems analysis and design. In this method, a team of customers, developers and testers collaborate to draw up user acceptance tests (UATs). These tests form a blueprint of the system, and form the basis of the design.
Each team member has a unique perspective of the system. Users are focused on what they would like the system to do, whereas developers focus on the right way to build the system. Testers can often envisage ways to test different scenarios.
Through discussion, the team translates user stories into actual automated tests. For each UAT, the team follows the same pass/fail/refactor process as TDD recommends for unit tests. The developers only write code when it’s needed to move the test from the red phase to the green phase.
Since ATDD works at system level, whereas TDD works at unit level, the two methods complement each other. Each UAT instigates a TDD cycle:
Run the UAT, which should fail.
Draw up a list of unit tests for this UAT.
Work through this list following the pass/fail/refactor cycle until all units pass.
Run the UAT again and repeat the process until it passes.
ATDD can work in conjunction with BDD, since Gherkin specifications form a sound basis for the UATs.
The Bottom Line: Should You Use Test-Driven Development?
Most developers who’ve used TDD agree that it gives a deeper understanding of the system before coding begins. TDD ensures that comprehensive unit tests are written and run. This improves the quality of the system and reduces the number of errors during later testing phases.
You may not choose to rigidly implement this method, but it’s well worth applying some of the principles to your own development methodology.
Taking Control of Testing
While smoke testing ensures your system's readiness, flaky tests can still undermine your confidence in the build's stability. That’s why Trunk is building a tool to conquer flaky tests once and for all. You’ll get all of the features of the big guy's internal systems, without the headache of managing it. You’ll be able to:
Autodetect the flaky tests in your build system
See them in a dashboard across all your repos
Quarantine tests with one click, or automatically
Get detailed stats to target the root cause of the problem
Get reports weekly, nightly, or instantly sent right to email and Slack
Intelligently file tickets to the right engineer
If you’re interested in getting beta access, sign up here.