Join us
@obedrina ă» Jan 04,2024 ă» 12 min read ă» 1023 views ă» Originally posted on semaphoreci.com
What is Test-Driven Development (TDD)? Explore its history, relationship to testing, and the benefits you can get from it.
Test-Driven Development (TDD) is an established practice that has been favored by many developers for years. This article helps you understand the history behind TDD, its purpose, its relationship to testing in general, and the benefits you can gain from its use.
TDD is one of the technical practices of eXtreme Programming. The invention of TDD is usually attributed to Kent Beck; one of the first âextreme programmersâ.
This practice shook the common sentiment of programmers at the time. The practice of testing was already widespread, but no one before had ever suggested writing tests before writing the actual code that needs testing. This act is itself counterintuitive if one thinks of TDD as a testing practice. As the author has repeatedly pointed out (and with him many other distinguished programmers), TDD was not born as a testing practice but as a software design practice.
The mantra of the early extreme programmers was to take things that worked well and âexert them to the extremeâ. Pair Programming, i.e. the practice of two people writing code collaboratively on the same computer, also arose from this programming movement.
In the same way, TDD was born: âIf I test the code I write, I get better quality code: what would happen if I took the process to the extreme: writing tests before the code itself?â
If I test the code I write, I get better quality code. What would happen if I took the process to the extreme: writing tests before the code itself?
Kent Beck answered this by developing TDD, and starting a small revolution within the eXtreme Programming revolution.
TDD thus began as a practice related to testing, but it soon turned out that the resulting tests were just a nice side-effect. The point of writing tests before code had much more to do with the design of the code itself than its testing.
Writing the test before the code helps the programmer put himself in the shoes of the user, making it easier to create clear software APIs. Using TDD helps make you more comfortable with circumscribing the scope of your code, writing shorter but more focused code, and producing easily-composable modules.
The act of thinking about how to test code early in the process helps with imagining concrete examples. In addition, this allows the developer to focus on concrete cases, avoiding premature generalizations and optimizations.
âThe simplest thing that could possibly workâ is a phrase you often hear from long-time XP programmers.
Another benefit you get from TDD is rapid feedback on what you produce. Extensive testing is no longer necessary to determine if the code works correctly because there are already tests in place to ensure just that.
eXtreme Programming puts a lot of emphasis on feedback loops. And, among these, shorter cycles that allow you to get quick confirmation are preferable. Of all the XP practices, TDD has the second-fastest feedback loop (second only to Pair Programming), as it provides feedback in a matter of minutes.
Another exciting feature of TDD is the more or less veiled constraint that leads programmers to take smaller and smaller steps. Those who have been doing TDD for a long time surely know the 3 laws of TDD, by Robert C. Martin,, also known as âUncle Bobâ.
In one of his famous articles, Uncle Bob reinforces the TDD process by formulating 3 simple laws:
If you follow this approach, it is clear that you favor incremental development, i.e. writing one test at a time. In this continuous cycle of very short iterations, there is space built in for refactoring. This term is often used as a synonym for âreengineeringâ, but it has a different meaning, at least in the TDD approach.
In this context, in fact, it represents the most important phase of the whole cycle, where the emphasis is on code quality. In eXtreme Programming, thereâs the concept of Simple Design, i.e. the continuous effort to make the code produced simple to evolve.
It is in the refactoring phase that the programmer concentrates on modifying limited portions of code to remove duplication or increase efficiency without changing behavior, which is strengthened by tests that guarantee adequate security. This can be done both in purely practical terms (e.g. introducing a more efficient version of an algorithm), in terms of design, or by modifying or introducing a new abstraction.
TDD was created as a tool for thinking, to increase the focus on small, well-defined portions of code. It helps to proceed in very small steps, adding functionality and value to the software in very small increments in a safe and consistent way. Finally, it enables constant refactoring, one of the most effective practices for keeping software under development in good shape.
Today TDD is no longer a novelty. There are many teams and developers who rely on this practice to ensure a sustainable pace in product development. Over time, there have also been several studies that support its validity.
An interesting aspect that frequently emerges is related to the adoption of this practice. The initial learning curve may be more or less steep, but it cannot be ignored. Just as the investment of time in test writing, although several studies shows positive ROI.
The evidence is that once developers have obtained sufficient and necessary preparation to enable the practice, projects end up with good results most of the time. This indicates that they are not successful products that enable TDD, but rather that TDD contributes to the success of projects, products and teams.
Another common objection to TDD is about writing a test first. The usual claim is that there are scenarios where writing tests first doesnât make sense, others where it is very difficult, if not impossible. On the first statement, there is no debate: TDD is not a one-size-fits-all tool, but it has a specific purpose: to support the developer in the design of the software solution.
On the fact that it is difficult -sometimes very difficult- this is also true: oftentimes the defendants here are the underlying code, too rigid to allow the introduction of tests, or the lack of tools. However, both are solvable problems: with a little work, it is possible to facilitate the adoption of the practice with the consequent benefits.
How do you get started in TDD? The answer alone would deserve an in-depth study. It is, however, possible to get a brief taste.
Letâs assume that we need to write the software for a cash register. The first functionality requested is related to the item scanner: when a specific item is passed (scanned), e.g. an apple, the system must charge a certain amount, e.g. 50 cents.
The following could be a first User Story:
And these are the related acceptance criteria:
Using the TDD approach, the first thing we need to do is write a test that shows that, currently, we do not have a checkout system that can checkout an apple:
The example is in Java. It allows us to point out a few things.
In the first few lines, we notice references to JUnit libraries. JUnit is a testing framework for Java projects, one of the many xUnit frameworks available.
For those approaching TDD, one of the first things to explore is the testing frameworks available. These days you have so many choices, and you can be certain that thereâs at least a âxUnit-likeâ framework for the language in use.
It is vital to include tests in dedicated files. In Java, the usual practice is to create test classes in dedicated âtestâ packages:
Classes and test methods make up the âtest suiteâ, which is the set of tests accompanying the software. Therefore, it is crucial to pay proper attention to the organization of the test suite. A good test suite separates tests by scope, making it easy to execute different types of tests separately, e.g. unit tests and End-To-End (E2E) tests.
This separation allows you to run the different types of tests on-demand and independently. It also makes it easy to run them via triggers or schedules in Continuous Integration systems.
Finally, we have our test:
Note the @Test
annotation: a peculiarity that allows the JUnit framework test runner to understand which portions of code should be executed and verified.
The method name is long and unusual, but it is pretty common to use the test method signature to express intent when writing tests.
Finally, we have the â3Asâ, which help define a good test. The first A, Arrange, reminds us that itâs a good idea to first do the test setup, instantiating objects and any variables weâll need to use for executing the test.
The second A, Act, focuses on the lines of code that put our System Under Test (SUT) into operation; in this case this is the CashRegister
object.
Finally, the third A, Assert, represents the main point: this is where we define the expected result of the test. If the assertion turns out to be true, our test will be satisfied.
In this example, we have our first test arranged to develop the first required functionality, but there is no mention of the production code. This is not an omission: the first test (and the others to come) are written assuming the presence of production code, even if it has not yet been implemented.
This approach, quite counterintuitive at first, is one of the signature characteristics of TDD. It is, therefore, normal to write code that doesnât even compile and to exploit this situation in order to obtain the so-called âred testâ, i.e. the test that, by failing, proves the need to write code.
Only at this point, after having respected the first law of TDD, can we move on to the second, which allows us to write the minimum amount of code necessary to pass the first test.
How can we pass this first test? By implementing a CashRegister
object, obviously. Below is a simple implementation:
If you find yourself scoffing at this implementation, youâre probably not the only one. This code allows the system to pass the test, but itâs clear that this CashRegister
implementation needs to be revised: as it is, it only works if a customer buys a single apple.
But thatâs the point of TDD: to do the minimum necessary. And in this situation, the minimum required to pass the first acceptance criteria is to build a CashRegister
that charges 50 cents for one apple.
Once the assertion of the test is satisfied, itâs time for refactoring. In this first example, it is difficult to highlight this step, since the code is already stripped down to the bare bones.
For the sake of brevity, letâs jump ahead and skip the handful of TDD cycles that would allow us to arrive at the following situation:
Letâs see the code implementation:
You can see the additional tests, which allow us to extend our code one bit at a time. They are all green, but the code is starting to creak: the number of conditional statements is growing, and the code is becoming ârigidâ.
At this point, it is possible to refactor the code, introducing, for example, the concept of PriceRule
, which determines the price for each product, taking into account any current special offers.
Take a look at the result of the refactor below:
With the green tests acting as a safety net, it is possible to refactor the code by introducing a new abstraction (PriceRule
). Furthermore, the use of Java Streams has made the product filtering operations more expressive. Now the addition of new products to the catalog is greatly simplified: in essence, it is sufficient to implement a new PriceRule
.
This article is not sufficient to give an in-depth explanation of all aspects of TDD, but this small example offers a glimpse of the value that it provides.
Weâve already said it, and now it should be clearer: TDD is a code design technique, not a testing technique. The resulting tests are, in fact, âonly a pleasant side effect.â
TDD is a code technique, not a testing technique.
The purpose of TDD is to give the programmer quick feedback about the code they have just written: if something is wrong, a test will signal it. Another goal of TDD is to enable refactoring, as we have just seen: you donât postpone to the near future (maybe to the distant futureâŠ) when refactoring code, paying some of the accumulated technical debt; you act in the moment, when the mind is still fresh and refactoring is both cheaper and easier.
The tests obtained using TDD practices generally fall into the family of unit tests. These kinds of tests are fast: in the order of milliseconds. The result of this approach is hundreds, if not thousands of tests. They continuously run on the programmerâs workstation and in the Continuous Integration (and Deployment/Delivery) pipeline.
Over time, different approaches to development have emerged, and with them, different ways of doing TDD.
There are two main TDD approaches. The first is the classical school approach (i.e. the definition comes from XP, and has three names: âClassicist,â âChicago style,â or âInside-outâ). The second one was born in London a few years later: âMockist,â âLondon style,â or âOutside-in.â
These two approaches are not in conflict, on the contrary, they are complementary. The two different approaches can coexist, as they each shine in different situations.
We have already had a taste of the classicist approach: starting from a first small test, we expanded our solution from the inside out, adding functionality from time to time.
The âoutside-inâ approach works on a different level and uses the TDD âred failing testâ approach on more extended functionalities.
For example, look at this test written with the Gherkin syntax, in full Behavior Driven Development (BDD) style:
Returning to our previous exercise, we know that there is no âbuy 3, pay 2â offers implementation. The outside-in approach makes developers think about the complete functionality, implementing it as simply as they can, even faking some parts when necessary. Inside a bigger âred failing testâ like this, development continues via classic short TDD loops. Development continues until this test turns green, which confirms the correct implementation of the required functionality.
Given the larger surface of this kind of test, programmers often use âtest doublesâ during writing and implementation; an example of which are mocks (hence the adjective âmockistâ).
This article underlines the main concepts behind TDD:
It also distinguishes TDD from the other common testing techniques.
Crafting software is somewhere between a science and an art. Mastery of relevant techniques is essential to fully express your art, and TDD is a solid tool to have in your toolbox.
Join other developers and claim your FAUN account now!
Influence
Total Hits
Posts
Only registered users can post comments. Please, login or signup.