The Basics of Test-Driven Development: Code for Code

TDD is an investment in the future.

Test-driven development, decoded

There are operational checks to most systems, and in the world of software engineering, test-driven development can help maintain those checks, and in turn, code. Test-driven development (TDD) is a software engineering technique and practice that takes a test-first approach to writing code.

TDD requires unit testing, which is where individual units or components of a software are tested before the code they are supposed to validate. The point is to test the smallest part of any software, with only a few inputs and a single output, in order to validate that each unit of the software performs as designed. Unit testing is the first level of software testing performed, usually prior to integration testing. Unit testing is important because the cost of fixing a bug during unit testing is less than fixing bugs at higher levels of testing.

In a test-driven development cycle, an engineer writes a unit test that defines a desired function, produces the minimum amount of code to pass that test, and then restructures the new code to satisfactory standards.

For example, a TDD cycle will look like the following:

  • Write a test
  • Run a test and see if it fails
  • Write code
  • Run tests again
  • Rewrite code
  • Repeat

During the 1990s, test-driven development was originally formalized by Kent Beck as one of the pillars of Extreme Programming (XP) methodology. XP is a specific, agile framework regarding engineering practices. Its maxim: to produce higher quality software as well as a higher quality of life for engineering teams. XP’s five values are communication, simplicity, feedback, courage, and respect.

Today, however, TDD is recognized as its own discipline with a focus on the continuous improvement of code. As a result, investments made on tests should save time and money in the future when adding new features or re-writing operations. Yet, in order to maintain a return on investment (ROI), it’s crucial to keep test code clean, organized, and dense in quality. For example, ask yourself, is it easily readable? Is it concise? Does the test say as much as possible with a minimum amount of code? To keep tests concise and clear, unit tests should contain only one output.

The ROI of test-driven development

While test-driven development can seem time-consuming, it allows for problems to be exposed early, saving time and reducing the cost of resolution (and bugs) in the long run.

TDD can seem like a lot of upfront work, but when done right, it can lead to maintainable, reusable code that also makes the addition of new functionalities easier—which can mean an increase engineer productivity overall. TDD also encourages engineers to refactor and continuously improve their work.

According to Kent Beck’s book on TDD, teams and TDD practitioners report:

  • Significant reductions in the percentage of failed outputs, at the cost of a moderate increase in initial development effort
  • Offset of overheads by a reduction in effort in projects’ final phases
  • Improved qualities of code design, as well as a higher degree of technical quality

What are the pitfalls?

The test-driven development approach is a big shift for engineers in how they tackle coding. It’s necessary to allow a significant amount of time for engineers to learn the TDD approach. This can seem costly upfront, but when there’s proper onboarding for test-driven development, the long-term benefits increase as the engineer gets more comfortable refactoring code and making sure it’s reusable for others.

However, it’s also up to the engineer’s ability to create a roadmap and document repository in the form of planned tests. Do the tests have executable specifications and application documentation? Tests need to describe the requirements they’re validating.

Typical engineer mistakes include:

  • Not running tests frequently enough
  • Writing tests that are too large
  • Writing too many tests at once
  • Writing insignificant tests
  • Writing tests for trivial code, for instance, over-abstraction

Typical team pitfalls include:

  • Partial adoption: when teams only have a few developers that make use of TDD, it can lead to chaotic processes for checking code
  • Poor maintenance: when code isn’t maintained it can lead to a test suite with a long-running time that prohibits other work
  • Deserted test suite: when TDD is abandoned, it can lead to confusion and chaos for code organization—similar to the pitfall of partial adoption

How to properly conduct test-driven development

Aside from the basics of a test-driven development cycle, a clean test should follow 5 rules that follow the acronym FIRST:

  • Fast: tests must be fast in order to be executed often
  • Independent: tests can’t depend on one another other
  • Repeatable: a test must be able to be reproduced
  • Self-Validating: a test must be binary (pass or fail) for a quick, easy conclusion
  • Timely: a test must be written just before the code it will validate.

Is your code up to code?

Test-driven development requires unit-testing—an important first step—but there are four levels of tests that are necessary to run afterwards in order to make sure your code and software all run smoothly. Integration testing, the second level, comes after unit testing. Integration testing is where individual units are combined and tested as together as a group. This is to test whether there are any bugs between integrated units. Third is system testing. System testing tests finished and integrated software in order to examine the system as a whole and if it meets the specified requirements. Fourth is acceptance testing, where a system is tested to determine if it meets business requirements and if it’s acceptable for delivery.

A chair is a good analogy for how to think about these four levels of testing. When a chair is manufactured, the legs, arms, backrest, upholstery, and seat are built separately and unit tested separately. When two or more parts (or units) are ready, they are put together and integration testing can be performed. When the chair is put together and integrated, system testing is performed. When that is done, acceptance testing confirms the chair is ready for end-users.

In order to ensure code is up to par, you can also run smoke tests and regression tests. Smoke tests ensure that the most important functions of system work. Regression testing ensures that any changes, bug fixes, or enhancements to the software have not negatively impacted it.

Best practices around test-driven development

Test-driven development can keep your team’s code clean and organized, and save time in the long-run, but the long-term benefits of test-driven development reside in whether engineers have enough time to learn the process for TDD and whether they can wholly adopt and practice testing and refactoring.

With TDD, it’s important to remember the following:

  • Speed is key. It’s less important to make perfect code. It’s more important to keep everything rolling until it’s time to refactor the code
  • TDD is a mindset—it’s a way to approach design and think about implementation before writing the code
  • Documentation is crucial so that others can reuse and practice the code.

TDD is an investment in the future and can ultimately help engineers be better overall programmers. It saves time, makes refactoring code easier, allows for better design, ensures living documentation and that your code will work. As with anything, practice is important.

Amanda Roosa is a freelance writer. You can find her work on AngelList, Zendesk, and Data-Driven Investor.

Continue reading
All articles

‘Super Angel’ GP Launches Rolling Fund with Global LP Base


What We’re Seeing in Early-Stage VC So Far in 2020


The State of Women in Venture (Part 3)