What is Test Driven Development?

As developers, a large portion of our time is spent testing code that we’ve just written. In a traditional testing cycle, we’ll write some code and then manually test. We’ll enter some inputs and find that it causes an error so we’ll write a little bit more code to fix that error and test again only to find another set of inputs that fails. We’ll slowly work through this process until we feel confident that everything is operating correctly. But how do we know that the first set of bad data we found is still error-free? How do we know it’s still error-free a year from now?

We could continue to manually test all these permutations but almost all of the time they’re going to continue to run perfectly. Because it’s very time consuming to run through all these permutations we’re not going to keep this up. Using tools like PHPUnit we can create automated tests that will make testing our code infinitely more repeatable. Embracing Test-Driven Development (TDD) will allow us to quickly build a suite of automated tests that will allow us to improve the maintainability and reliability of our code.

What is Test Driven Development?

TDD is a test-first software development process that uses short development cycles to write very specific test cases and then modify our code so the tests pass. TDD consists of five phases that we will repeat as we modify our code. Each of the phases happens very quickly and we might go through all five phases in less than a minute.

TDD was “rediscovered” by Kent Beck while working on the SmallTalk language and has been documented extensively in his book “Test Driven Development: By Example”. The book is an excellent primer for working with TDD as it works through several examples of how to use TDD and also explains some techniques for improving code.

Why You Should Be Using TDD

Let’s say it’s 4 PM before a long weekend and your boss comes to you and asks you to make a small change to your codebase. How confident are you that if you make the change you’re not going to get a call on Sunday? Because TDD forces you to create automated tests, you’ll have the confidence that when you make the change your weekend isn’t going to be interrupted with a new bug.

While TDD works best for greenfield applications, it’s a benefit to anyone working on a brownfield application as well. Getting automated tests wrapped around untested code can quickly allow us to determine if changes we’ve made cause unexpected changes in behavior. Brownfield applications tend to require a little more finesse to get them set up for automated testing so don’t get discouraged if it’s frustrating at first.

How Do You Use Test Driven Development?

In “Test-Driven Development by Example”, Kent Beck outlines the five phases of TDD.

  1. Quickly add a new test
  2. Run all tests and see the new one fail
  3. Make a little change
  4. Run all tests and see them all succeed
  5. Refactor to remove duplication

Each step should be small with, at most, ten new lines of code being added. If we find ourselves doing more than that we’re working on too large a change and we need to break it into smaller pieces.

#1. Quickly add a new test

The first thing we’re going to do is write a failing test. We’ll use this failing test to help determine when we’ve achieved our expected functionality. It’s important that the test is succinct and that it’s looking at how a single change will affect our code.

2. Run all tests and see the new one fail

In this step, we’re going to run the test to make sure our test fails before we move on to the next phase. It’s very easy to write a test that doesn’t fail so we always run our test to verify it’s failing before moving to the next phase.

Figure 1: Red Phase Output from PHPUnit

As a small aside, the wording for this phase says “run all the tests” but as our test suite (a collection of tests) grows this will take an unproductively large amount of time. We’ll want to short circuit this and only run the test file or just our new test. Many IDEs can run a single test or single file and it’s worth spending time figuring out how to get this working as it will make us more productive.

#3. Make a little change

Now our goal is to change the smallest amount of code possible to get that test to pass. We don’t want to change any more than is necessary because that extra bit of change wasn’t made using TDD and is potentially not tested. We don’t need perfect code in this phase we just need code that makes the test pass. It’s very easy to get caught up in making sure everything is perfect but that’s not the goal here. Perfect comes later.

#4. Run all tests and see them all succeed

Now that we’ve made our change we can run our test and see that it passes. If it doesn’t then we just jump back to phase #3 and keep making small changes until it does.

Figure 2: Green Phase Output from PHPUnit

#5. Refactor to remove duplication

Now that we have our tests passing we’re going to take a break and inspect both our test code and our code under test to see where we can make changes so it’s easier for future developers to read and understand. As features are added, method and classes will get larger and contain duplication so our primary focus in this phase is to remove that duplication.

We’re using TDD so changes to remove duplication are painless because we can quickly run our unit tests again to make sure we didn’t make a mistake.

Other things we should look for as we’re doing this process:

  1. Are the method/class/variables easy to read? Do they express intent?
  2. Can we move logic into the superclass?
  3. Can we move logic into a shared trait?

Because we’re using TDD, changes are painless because we can quickly run our unit tests again to make sure we didn’t make a mistake.

Repeat

Now that we’ve completed a single TDD cycle we can start back at the beginning with a new test.

Conclusion

Test-Driven Development is a software development process that focuses on a test-first methodology so we can develop software that allows us to feel comfortable making changes. This is done using the steps: write a test, verify it fails, write some code, verify the test pass, and refactor. Using this process will improve the maintainability and reliability of our code.

References and Additional Readings