top of page

TDD - An Aproach

  • Glasdon Falcao
  • Aug 19, 2016
  • 7 min read

What is Test-Driven Development (TDD)?

Test-driven development (TDD) is an evolutionary approach to development which combines test-first development where you write a test before you write just enough production code to fulfill that test and refactoring.

Test-driven development is not about testing. Test-driven development is about development (and design), specifically improving the quality and design of code. The resulting unit tests are just an extremely useful by-product.

Test-Driven Development Process:

Write the test before writing the implementation code Benefits: ensures that testable code is written; ensures that every line of code gets tests written for it.

Only write new code when test is failing Benefits: confirms that the test does not work without the implementation

Write some code Benefits: ensures that there is no unexpected side-effect caused by code changes.

Run all tests Benefits: focus is maintained on a small unit of work; implementation code is (almost) always in working conditions.

Refactor code Benefits: refactoring is safe

Repeat Starting with another new test, the cycle is then repeated to push forward the functionality.

TDD Best practices

Practices have been separated into following categories:

  • Naming Conventions

  • Processes

  • Development practices

  • Tools

Benefits of TDD:

  • Much less debug time

  • Code proven to meet requirements

  • Tests become Safety Net

  • Near zero defects

  • Shorter development cycles

Limitations Of TDD

One of the chief limitations of TDD is the fact that tests can sometimes be incorrectly conceived or applied. This may result in units that do not perform as expected in the real world. Even if all the units work perfectly in isolation and in all anticipated scenarios, end users may encounter situations not imagined by the developers and testers. The final results of TDD are only as good as the tests that have been used, the thoroughness with which they have been done and the extent to which they mimic conditions encountered by users of the final product.

How should you practise TDD (test-driven development) really?

Test-driven development can produce applications of high quality in less time than is possible with older methods. Proper implementation of TDD requires the developers and testers to accurately anticipate how the application and its features will be used in the real world. Problems are approached in an incremental fashion and tests intended for the same unit of code must often be done many times over. The methodical nature of TDD ensures that all the units in an application have been tested for optimum functionality, both individually and in synergy with one another. Because tests are conducted from the very beginning of the design cycle, time and money spent in debugging at later stages is minimized.

The rules of TDD are dead simple: write a test, make it pass, improve the design. Anyone can do that. Unfortunately, most people just aren't getting the results from TDD that they expect. This stops a lot of people from trying and causes still more to give up just when they're about to turn the corner—and therein lies your advantage. If you learned how to do TDD well...

You'd stand out at work as the go-to person. They'd respect you. They'd rely on you. And they'd have to pay you. (Let's not be coy about that. I left my job at IBM because they couldn't pay me any more money without making me a manager, and 27-year-old me would have made a terrible manager!)

You'd get to walk out of the office with all your work under control while your colleagues scrambled to fix the three bugs they'd introduced that morning. (I remember scrambling. I also remember sleeping like a baby. I preferred the sleeping like a baby.) When you said, "It'll take a week", it'd take a week! Not like Bill who's been saying he's 90% done for the last three weeks. (Legacy code gets in the way, but if your task estimates for legacy code work were even close, you'd be a hero.) There's more, but that's not bad, is it?

The bottom line is this: a lot of programmers have built lucrative careers by practising TDD. Some have done it by teaching others to do it. Some have done it by building high-quality products at high speed and maintaining that speed long enough to outlast their competition. Some have done it by becoming the expert within their company, who works on all the interesting stuff, has the credibility to choose what they want to work on, and either earns more or works with less stress—and often both!

Now wait a moment... I'm not trying to tell you that TDD gets you there. I'm certainly not trying to tell you that TDD is the only thing to get you there, or even the best thing to get you there. I'm only telling you that TDD helps a lot of people get there. Even better, lots of people just don't get TDD, so if you do, then you have a fantastic chance to catch up to them, blow past them, and leave them in your wake.

Development Practices

Practices listed in this section are focused on the best way to write tests.

Write the simplest code to pass the test Benefits: ensures cleaner and clearer design; avoids unnecessary features

The idea is that the simpler the implementation the better and easier to maintain is the product. The idea adheres to the “keep it simple stupid” (KISS) principle. It states that most systems work best if they are kept simple rather than made complex; therefore simplicity should be a key goal in design and unnecessary complexity should be avoided.

Write assertions first, act later Benefits: clarifies the purpose of the requirement and test early.

Once assertion is written, purpose of the test is clear and developer can concentrate on the code that will accomplish that assertion and, later on, on the actual implementation.

Minimize assertions in each test Benefit: avoids assertion roulette; allows execution of more asserts.

If multiple assertions are used within one test method, it might be hard to tell which of them caused a test failure. This is especially common when tests are executed as part of continuous integration process. If the problem cannot be reproduced on a developer’s machine (as may be the case if the problem is caused by environmental issues) fixing the problem may be difficult and time-consuming.

When one assert fails, execution of that test method stop. If there are other asserts in that method, they will not be run and information that can be used in debugging is lost.

Last but not least, having multiple asserts creates confusion about the objective of the test.

This practice does not mean that there should always be only one assert per test method. If there are other asserts that test the same logical condition or unit of functionality, they can be used within the same method.

Do not introduce dependencies between tests Benefits: tests work in any order independently whether all or only subset is run

Each test should be independent from others. Developers should be able to execute any individual test, set of tests or all of them. Often there is no guarantee that tests will be executed in any particular order. If there are dependencies between tests they might easily be broken with introduction of new tests.

Tests should run fast Benefits: tests are used often

If it takes a lot of time to run tests, developers will stop using them or run only a small subset related to the changes they are making. Benefit of fast tests, besides fostering their usage, is fast feedback. Sooner the problem is detected, easier it is to fix it. Knowledge about the code that produced the problem is still fresh. If developer already started working on a next feature while waiting for the completion of the execution of tests, he might decide to postpone fixing the problem until that new feature is developed. On the other hand, if he drops his current work to fix the bug, time is lost in context switching.

Use Mocks Benefits: reduced code dependency; faster tests execution.

Mocks are prerequisites for fast execution of tests and ability to concentrate on a single unit of functionality. By mocking dependencies external to the method that is being tested developer is able to focus on the task at hand without spending time to set them up. In case of bigger teams, those dependencies might not even be developed. Also, execution of tests without mocks tends to be slow. Good candidates for mocks are databases, other products, services, etc. Mock objects are a big topic and will be described in more details in a future article.

Use setup and tear-down methods Benefits: allows setup and tear-down code to be executed before and after the class or each method.

In many cases some code needs to be executed before test class or before each method in a class. For that purpose JUnit has @BeforeClass and @Before annotations that should be used as the setup phase. @BeforeClass executes the associated method before the class is loaded (before first test method is run). @Before executes the associated method before each test is run. Both should be used when there are certain preconditions required by tests. Most common example is setting up test data in the (hopefully in-memory) database. On the opposite end are @After and @AfterClass annotations that should be used as tear-down phase. Their main purpose is to destroy data or state created during the setup phase or by tests themselves. As stated in one of the previous practices, each test should be independent from others. More over, no test should be affected by others. Tear-down phase helps maintaining the system as if no test was previously executed.

Do not use base classes Benefits: Test clarity.

Developers often approach test code in the same way as implementation. One of the common mistakes is to create base classes that are extended by tests. This practice avoids code duplication at the expense of tests clarity. When possible, base classes used for testing should be avoided or limited. Having to navigate from the test class to its parent, parent of the parent and so on in order to understand the logic behind tests introduces, often unnecessary, confusion. Tests clarity should more important than avoiding code duplication.

Tools TDD, coding and testing in general are heavily dependent on other tools and processes. Some of the most important are following. Each of them is a too big of a topic to be explored in this article so they will be described only briefly.

Code coverage Benefit: assurance that everything is tested.

Code coverage practice and tools are very valuable in determining that all code, branches and complexity is tested. Some of the tools are JaCoCo, Clover and Cobertura.

Continuous integration (CI) Continuous Integration (CI) tools are a must for all but most trivial projects. Some of the most used tools are Jenkins, Hudson, Travis and Bamboo.


Comments


You Might Also Like:
bottom of page