Test-driven development (TDD) is all about writing lean and mean code with a high level of test coverage. Developers write the test for a requirement before writing the code, and the code is considered finished as soon as the test passes. Sounds great, but how do you successfully roll out TDD in your organization? We’ll explore this here, and I’ll give you some tips to maximize the benefits of adopting TDD.
The primary benefit of TDD is that it helps developers create maintainable and testable code. TDD also prevents feature-creep and “gold plating” of the code by ensuring that the minimum code necessary to implement functionality is created. Validating that the correct code is being written also makes the teams more efficient and avoids wasting precious development resources on building the wrong functionality.
Following TDD enforces unit testing as a practice within the organization. But this isn’t unit testing for unit testing’s sake. It’s a way to ensure that you’re creating testable code, helping you reduce maintenance costs and keep technical debt low. By ensuring that you have a solid regression suite, you are instantly notified when something breaks as a result of code changes.
A big reason for the increasing interest in TDD is that many organizations are transitioning to agile development practices and are realizing that their existing testing practices rely too heavily on end-of-cycle manual testing. There is certainly a place for end-to-end testing practices, but in order to scale development innovation at the speed necessary to remain competitive, organizations must shift testing efforts left. TDD is a process that all but guarantees that you start development projects with a healthy base of tests to ensure software quality throughout the development lifecycle.
For practical purposes, very few people who develop software follow the pure vision of TDD, for several reasons. Modern software development usually involves integrating libraries, connecting legacy code, and extending existing functionality. Many people don’t have the luxury of writing completely new code, so pure TDD isn’t practical in many cases. Instead, many people “doing TDD” sit somewhere on a spectrum that looks something like this:
While each organization has its own set of specific challenges with TDD, the issues we hear about the most from our customers fit into the three basic types above. But within that spectrum, there are many types of TDD practitioners:
On one end of the spectrum are people who are successfully practicing TDD at the “black belt” level. These folks are fully committed to TDD principles and don’t write anything before writing tests — no skeleton, no definitions, no nuthin’ — and are finished as soon as the test passes. As a result, the code is highly efficient and highly maintainable. When we talk to TDD ninjas, they often acknowledge that their success implementing TDD is uneven across teams within their organization.
The next on the spectrum are people who might design classes, method signatures, etc., and then write tests against those definitions. At the API level, this is equivalent to writing the OpenAPI/Swagger or WSDL definition. We call this the pragmatic approach to TDD.
This approach is a little easier to adopt because it provides more structure and greater clarity. Everything compiles so that a squad can more easily work together. The potential trade-off, however, is that TDD pragmatists may not always achieve the highly-efficient, minimal code design achieved by TDD ninjas.
There are many people who would love to fully commit to TDD, but don’t have the luxury of starting with greenfield code. These folks often create tests to reproduce a defect, or to test expected behavior, in order to change or extend existing functionality based on legacy code.
In our experience, a large segment of those who self-identify as practicing TDD are at or near this point in the spectrum. The logic is that although the test and code are inextricably linked, the test does not necessarily precede the code. Tests do, however, precede the changes to the code.
On the other end of our spectrum of those who self-identify as TDD are people who test and code in parallel. For TDD-ish practitioners, as long as the test and code are committed and managed together, the development is considered to be driven by testing.
So how do you successfully adopt TDD? Follow the four tips below to achieve TDD success.
A common TDD implementation problem rears its ugly head when an organization has inherited untestable code and it doesn’t have the ability to pay off the technical debt. Should the code be refactored? If so, how much refactoring is necessary to start practicing TDD in a meaningful, achievable way?
If there is a mandate to start doing TDD on legacy code, then trying to implement ideal TDD is not going to work. The code you’re inheriting wasn’t built with testability in mind, the original author may no longer be on the team or even in the organization, the dependent libraries may have changed, and so on.
So, if the legacy code is already out there and working, the risk associated with the technical debt is low relative to the risk of new untested work. By applying TDD to the new code you’re writing, i.e., changes in the existing code, you minimize the risk and don’t increase the technical debt.
To overcome the challenges of testing the existing code, you can use Parasoft Jtest’s unit testing capability to quickly create meaningful tests. Parasoft Jtest analyzes the legacy code and its dependencies, reducing the amount of time required to create JUnit test cases and implement the complex stubbing/mocking required for the pre-existing code.
Microservice-based architectures have far more dependencies and complexity than traditional application stacks, introducing significantly more volatility into the test environment. Creating tests for applications based on microservices and other complex architectures can be difficult due to the requirement for advanced mocking and stubbing.
A microservice-based architecture, however, presents the opportunity to leverage TDD best practices in a very efficient way. If you think about treating the microservice as the unit, rather than the compilation unit, then the TDD pragmatist approach becomes very useful.
When applying TDD at the API level, your API is defined in the contract (OpenAPI/Swagger, WSDL). An API testing solution, such as Parasoft SOAtest, can then automatically create tests based on those definitions. Now you’re ready to develop functionality according to the definitions until the SOAtest tests pass.
TDD is commonly viewed as a developer-focused activity, typically encapsulated by creating tests in a unit testing framework, such as JUnit or NUnit. Most organizations, however, simply don’t have enough developers or time to cover all of their use-cases. This is especially a problem for enterprise organizations that have a mix of roles and skills contributing to its projects.
Leveraging TDD practices at the API level, as described above for microservices, helps you address the issue of limited technical resources by enabling you to leverage tester expertise to define tests against the contract. With this approach, business analysts, testers, and other non-developer resources are able to contribute to the TDD testing efforts.
You can also use a service virtualization solution, such as Parasoft Virtualize, to immediately create simulated functionality that you can build and execute tests against before any code is written.
As we’ve discussed, there are several benefits of implementing TDD, but by itself TDD does not necessarily translate to code that matches the requirements. It only ensures that the code is covered by tests and that the tests pass. Here is where Behavior-driven development (BDD) comes in. Rather than writing code until the test passes, developers write code until the behavior is implemented.
It’s worth pointing out that there have been attempts in the past to define the functional framework before writing the code. (Does anybody remember design by contract (DbC)?) In fact, BDD is the most recent attempt to implement such an approach. In BDD (or DbC, for that matter), the pre- and post-conditions must be defined before creating a test or code.
BDD represents an opportunity to take TDD to the next level. If you are using a BDD language, such as Gherkin, you can scale automation testing. And Parasoft SOAtest (for API testing) and Parasoft Selenic (for Selenium driven UI testing) can lower the technical complexity of writing the necessary glue code and step definitions that normally requires developer resources.
The promise of test-driven development is one based on a lean development ethos. It seeks to help you produce efficient, testable, and maintainable code. But real world conditions do not always make TDD adoption easy. Parasoft helps you adopt TDD practices in a manner that’s practical for your organization, enabling you to maximize your dev/test resources. Contact us for details on how we can help you implement TDD in your organization.
VP of Products at Parasoft, Mark is responsible for ensuring that Parasoft solutions deliver real value to the organizations adopting them. Mark has been with Parasoft since 2004, working with a broad cross-section of Global 2000 customers, from specific technology implementations to broader SDLC process improvement initiatives.