Test Driven Development

tech, testing, junit, java, programming,

Test Driven Development

tl;dr - This blog focuses on what paradigms to follow for test driven development, and how to do TDD in Java.

References and shameless copy paste-

  • https://en.wikipedia.org/wiki/Test-driven_development

What is Test Driven Development? Why is it important

Test driven development is the process of writing tests first before writing any production code. The idea behind this approach is to think of what you are trying to do first and design behavioral tests before hand for each module you wish to write code for. More importantly following TDD is a way to avoid the risk of fitting your tests into the code you have now written, this can often skip important edge cases and might fail to capture the behavior since you are thinking more in terms of how you implemented the code rather than what you want it to do.

Not doing TDD can lead to:

  • When developers don’t write tests upfront they forget to weigh in complexities of the method.

  • Inevitably time-crunch can lead to less effort in testing and thereby you haven’t focused enough on test-generation. Writing functional coding gives a false sense of security regarding completion of the project. (Ideally you should have a balance of doing testing partially upfront and partially towards the end, the more done upfront the better)

Advantages of TDD

  • Writing the tests first requires you to really consider what do you want from the code.
  • You receive fast feedback.
  • TDD creates a detailed specification.
  • TDD reduces time spent on rework.
  • You spend less time in the debugger.
  • You are able to identify the errors and problems quickly.
  • TDD tells you whether your last change (or refactoring) broke previously working code.
  • TDD allows the design to evolve and adapt to your changing understanding of the problem.
  • TDD forces the radical simplification of the code. You will only write code in response to the requirements of the tests.
  • You’re forced to write small classes focused on one thing.
  • TDD creates SOLID code.
  • TDD supports a clean interface.
  • TDD creates code that is maintainable, flexible, and easily extensible.
  • The resulting unit tests are simple and act as documentation for the code. Since TDD use cases are written as tests, other programmers can view the tests as usage examples of how the code is intended to work.
  • The development time to market is shorter.
  • The programmer’s productivity is increased.
  • Development costs are cut.
  • Quality is improved.
  • Bugs are reduced.
  • TDD gives programmers the confidence to change the larger architecture of an application when adding new functionalities. Without the flexibility of TDD, developers frequently add new functionality by virtually bolting it to the existing application without true integration, which can cause problems down the road.

TDD Global Lifecycle.png
By Xarawn - Own work, CC BY-SA 4.0, Test Driven Development Process

How it works

Red -> Green -> Refactor

  1. Add a test In test-driven development, each new feature begins with writing a test. Write a test that defines a function or improvements of a function, which should be very succinct. To write a test, the developer must clearly understand the feature’s specification and requirements. The developer can accomplish this through use cases and user stories to cover the requirements and exception conditions, and can write the test in whatever testing framework is appropriate to the software environment. It could be a modified version of an existing test. This is a differentiating feature of test-driven development versus writing unit tests after the code is written: it makes the developer focus on the requirements before writing the code, a subtle but important difference.

Key things to remember -

  • These tests should be attuned mostly to features, and do not need to work at helper function level (Ideally you could go that deep, but I believe in a balanced approach and helper function and really small functionality can be left for later)
  • The idea is to clearly understand the specification, and be able to design higher level API’s and workflows. This will also help you to avoid issues later. It also helps you think what all do you need when you are writing the actual functionality.
  • While modularity is a tangential topic, and one could go top-down when writing tests, it is important to not keep increasing scope of funcitons and keep modularity in mind.
  1. Run all tests and see if the new test fails This validates that the test harness is working correctly, shows that the new test does not pass without requiring new code because the required behavior already exists, and it rules out the possibility that the new test is flawed and will always pass. The new test should fail for the expected reason. This step increases the developer’s confidence in the new test.

  2. Write the code The next step is to write some code that causes the test to pass. The new code written at this stage is not perfect and may, for example, pass the test in an inelegant way. That is acceptable because it will be improved and honed in Step 5. At this point, the only purpose of the written code is to pass the test. The programmer must not write code that is beyond the functionality that the test checks.

  3. Run tests If all test cases now pass, the programmer can be confident that the new code meets the test requirements, and does not break or degrade any existing features. If they do not, the new code must be adjusted until they do.

  4. Refactor code The growing code base must be cleaned up regularly during test-driven development. New code can be moved from where it was convenient for passing a test to where it more logically belongs. Duplication must be removed. Object, class, module, variable and method names should clearly represent their current purpose and use, as extra functionality is added. As features are added, method bodies can get longer and other objects larger. They benefit from being split and their parts carefully named to improve readability and maintainability, which will be increasingly valuable later in the software lifecycle. Inheritance hierarchies may be rearranged to be more logical and helpful, and perhaps to benefit from recognized design patterns. There are specific and general guidelines for refactoring and for creating clean code.[6][7] By continually re-running the test cases throughout each refactoring phase, the developer can be confident that process is not altering any existing functionality. The concept of removing duplication is an important aspect of any software design. In this case, however, it also applies to the removal of any duplication between the test code and the production code—for example magic numbers or strings repeated in both to make the test pass in Step 3. Repeat Starting with another new test, the cycle is then repeated to push forward the functionality. The size of the steps should always be small, with as few as 1 to 10 edits between each test run. If new code does not rapidly satisfy a new test, or other tests fail unexpectedly, the programmer should undo or revert in preference to excessive debugging. Continuous integration helps by providing revertible checkpoints. When using external libraries it is important not to make increments that are so small as to be effectively merely testing the library itself,[4] unless there is some reason to believe that the library is buggy or is not sufficiently feature-complete to serve all the needs of the software under development.


Dev Task Across Dev Task Single