From Theory to Practice: Leveraging Test Driven Development (TDD) Step-by-Step Guide

Alya Azhar Agharid
13 min readMar 16, 2023

--

Source: dreamstime.com

In today’s fast-paced software development, the urgency of releasing high-quality software products is very important. High-quality software is also meant to be free from all possible human errors or other errors when it is run. Is there an effective implementer that can achieve such maximum software results? Yup, there is one. It’s called Test Driven Development (TDD) method.

Get to Know: Test-Driven Development

TDD, or Test-Driven Development, is a software development method that relies on tests that are made before the code is developed during software projects. The idea of mixing process refactoring and iterative development, TDD has recently gained a lot of popularity.

TDD is one of the approaches to producing high-quality software output as mentioned above, familiar with the above goals? Yup, TDD is a good development concept applied to Agile Scrum processes. TDD also supports the Agile Scrum Framework to produce reliable, maintainable, and scalable code.

3 Rules of TDD by Uncle Bob
Robert C. Martin, or, Uncle Bob wrote down the TDD rules in chapter 5 of Test Driven Development of his book The Clean Coder as follows:

1. You are not allowed to write any production code unless it is to make a failing unit test pass.
2. You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures.
3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

Pro vs. Counter-argument The Use of TDD

🧑🏻‍🦱: “But isn’t TDD challenging to put into practice?”
👱🏻: “Why can’t we just start writing the code now?”
🧔🏻: “Do you work twice if you develop a test, write some code, and then refactor? Time wasted!”

Well, the use of TDD has generated a lot of contra-argument. Some claim it is challenging and not worthwhile. While some may be correct, in saying that TDD takes far more work than simply writing code right away. More of the contra is talked about the TDD is ineffectiveness:

  1. Is TDD always necessary?
    Some developers argue that TDD can be time-consuming and may not be necessary in all cases, while others believe that it is an essential part of writing high-quality, maintainable code.
  2. TDD and team productivity:
    Some developers believe that TDD can slow down development by requiring additional time for writing tests, while others argue that it can actually improve team productivity by catching bugs earlier and reducing time spent on debugging.

But did you realize that TDD can be more advantageous than its work? Let’s go deeper looking for its advantage on implement it!

  1. Investment in Your Team Project
    Developers can easily find flaws during the development process by writing tests before writing code. Eliminating problems, as opposed to later fixing flaws in the codebase, saves a lot of time and money.
  2. Increase Code Quality
    Written code may be more modular, testable, and simple to maintain when using TDD. Moreover, this reduces technical debt and produces higher-quality code.
  3. Safer and Facilitated Refactoring
    TDD ensures that tests function adequately to detect problems. So, less risky code may be created (and refactored afterward) because it only needs to follow standard code passing.
  4. Saves time
    Although TDD might take more work initially, it ultimately saves time because less time is needed for debugging and maintaining the code.

Indeed, it takes a lot of time to get used to the TDD process. But some many tools and frameworks can help with TDD work, such as testing tools, mock object frameworks, and testing frameworks.

In my opinion, more like the contra is about the time and the productivity of the waste time to handle the test rather than the code. In my opinion, TDD may not be the best fit for every project or team.

Then, when is the best and suite time to implement TDD and when is not?

Long-term projects
Projects that are expected to be maintained and updated over a long period of time can benefit from TDD as it helps to ensure that code remains maintainable and continues to work as expected over time.

Complex projects
Projects with complex business logic, workflows, or user interfaces can benefit from TDD as it helps to ensure that all aspects of the project are functioning as expected and makes it easier to identify and fix bugs and errors.

Agile projects
Projects that follow an Agile methodology, with a focus on iterative development and continuous testing and integration, can benefit from TDD as it helps to ensure that new features and changes do not introduce unexpected bugs or issues.

✅Collaborative projects
Projects involving multiple developers or teams can benefit from TDD as it helps to ensure that all developers are on the same page and working towards a common goal, and helps to identify potential issues and conflicts early on.

❌Tight Deadlines, Budget Constrains
It may not be practical to implement TDD. In these cases, it may be more efficient to focus on writing the code first and testing it later, or to use other testing methodologies, such as automated testing or manual testing.

❌A lot of uncertainty or requirements are constantly changing.
In these cases, it may be more beneficial to focus on a more flexible development approach that allows for more rapid iteration and testing.

Ultimately, the decision to implement TDD should be based on the specific needs and requirements of the project, as well as the team’s development process and resources. If implemented correctly, TDD can be an effective way to improve code quality, catch errors early, and increase team collaboration and communication.

How’d you feel?
Want to dive into the TDD concept deeper?
Let’s go!🌊

The Cycle of TDD

Source: https://www.nimblework.com/

1 — [RED] — Write the Fail test

Before writing the implementation code, a test must be written. All that needs to be done is to create tests for any scenario that can occur in the program’s functionality. Do you know about F.I.R.S.T Principle? It’s a guide that may help a lot during writing a test:

FIRST Principle:

F — Fast, tests should run quickly and provide quick feedback to the developer when changes are made to the code. Slow tests will complicate the development and difficult to iterate with a slow feedback response.

I — Independent, the tests must be independent of each other so that if a failure occurs in one test feature it does not affect the other tests. That way the issues that occur are isolated and make the debugging process easier.

R — Repeatable, the test must be able to be run repeatedly and give the same result every time it is run or be repeatable. This principle is consistent and reliable in the testing process

S — Self-validating, the test, of course, must have a valid decision by itself regarding the pass or failure when it’s run without human intervention which can increase the possibility of human error.

T — Timely, the test must be made before implementing the code to make a test comprehensive and cover all aspects of the code.

If the test fails, the code must also fail. It’s written as a [RED] tag in the commit message or means code and test still fail.

For implementation in this PPL project, I write the test first with a commit message [RED] that gives a flag this code has no implementation and test only.

Remember!
It is important to consider the test must have both positive as well as negative test scenarios. Most of the bugs were found during negative testing. So, what are positive tests and negative tests in testing?

Positive Test
Testing the “happy” path of the functionality. The input data and the path should meet the business requirements and fulfill the conditions in the right way with no strange behavior coming out from the functionality also has expected values. So, a positive test is aim to demonstrate that the system behaves correctly when it is used as intended.
All clear and smooth like butter!

Set Up
Example of Positive Code

The above code is the implementation of a positive test that describes the dashboard retrieving the lowongan successfully because the role is admin or super-admin.

Negative Test
Testing the application and the functionality with unusual input data and producing uncorrect path to test it, by this negative test we can understand the behavior of the application to the negative entries. This test also helps to uncover potential issues and vulnerabilities that may arise under unexpected scenarios.

Example of negative test

Implementation in this project is like the example of the above code about the handle of the edge cases when someone who has no role as admin (for example, the alumnus or the company) tries to access this dashboard by forcing access to the URL. It will be redirected to the login page.

2 — [GREEN] — Time to Code

At this point, the developer can avoid code duplication by writing the minimum of code required to pass a test. The program that is executed must successfully pass each test. That way, the test that was passed is also called in the message commit as [GREEN].

The GREEN commit, yeay!
Example of GREEN commit implementation

For implementation in this PPL project, I write the code later with a commit message [GREEN] that gives a flag this code has already implemented and the test has to pass and succeed, also the best practices are already covered 100% coverage.

Passed means code coverage on tests up to 80%
Coverage test in Sonarqube CS
The coverage report, my job is in dashboard_proposal_lowongan apps.

Yay! the coverage of the code is passed means the coverage is > 80% and the test running well. It is all because of the implementation of TDD!

3 — [REFACTOR] — Improve!

The developer can now make changes to the code to make it more maintainable. What has changed is the code that has been implemented, which must pass all tests and not change the behavior that is already in place. This process is essential for improving code quality and lowering technical debt.

In the implementation, I want to improve the code by cleaning the code smells that are detected in Sonarqube CS UI. The commit message contains [REFACTOR] flag which means I change my code to improve.

Refactor code to remove the code smells and have a best practice code.

Best Practices for Running TDD Cycles

Remember the 3 Rules by Uncle Bob we talked about earlier? Here you go.

3 Rules of TDD by Uncle Bob
1.
You are not allowed to write any production code unless it is to make a failing unit test pass.
2. You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures.
3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

TDD process can be challenging to implement effectively, and there are several best practices and techniques that developers can follow to ensure that their TDD process is effective and efficient. By following these best practices, developers can ensure that their TDD process is effective, efficient, and produces high-quality, well-tested code.

One of the fundamental principles of TDD by Uncle Bob, these rules help to ensure that the TDD process is effective and efficient and that the resulting code is well-tested, well-designed, and easy to maintain

#1 — Write Tests First/Write Failing Test

Write basic and failed unit tests before writing any code or production code. This helps to ensure that the code is testable and that it meets the requirements specified by the tests. Then write small and focused tests that verify a single piece of functionality at a time. This makes it easier to debug and maintain the tests.

#2 — Write The Minimum Amount Code to Pass The Test

Write the minimum amount of code necessary to make the failing test pass. This ensures that we are not adding any unnecessary code to the system and that the code is only developed when it is required. By writing the minimum amount of code necessary, you can keep the code simple and easy to maintain, and avoid introducing any unnecessary complexity.

#3— Refactor Code Continuously

Refactoring allows us to make changes to the codebase without changing its functionality. Refactor the code to improve its design, readability, and maintainability. This helps to keep the code clean, maintainable, and easy to work with. Refactoring also helps to avoid technical debt, which can arise when code is left unrefactored over time. By continually refactoring the code, you can ensure that it remains easy to work with and modify, even as the codebase grows larger and more complex.

Learn To Make A Good Test

Until now we have learned about the cycle of Test-Driven Development. But before running the TDD cycle, we have to understand and learn about a good test because it’s critical to ensuring the quality of the software. A good test should verify that the code is working as expected and catch any defects before they make their way to the production environment. Here are some of the keys criteria of how to make a good, effective, efficient, and easy to maintain over time test (MC-FIRE stands for Maintainability, Completeness, Fast, Isolated, Reliable, Expressive) :

#1 — Test Completeness

All potential scenarios and edge cases that the code might run across should be covered by a good test. Additionally, all of the code’s expected outcomes should be tested. In other words, a good test should cover all aspects of the code’s functionality and needs.

A test should completely cover the application’s codebase and will identify any changes or additions that went wrong. A thorough test gives assurance that the software will perform as planned.

#2 — Test Accuracy & Reliability

An effective test should precisely confirm that the code is operating as intended. Neither false positives nor false negatives should exist. In other words, neither should it succeed when the code is broken nor fail when it is.

Regardless of changes that might take place beyond the parameters of a particular test, a good test should offer consistent feedback. An unreliable test suite may have tests that occasionally fail without providing us with any useful information regarding the changes we’ve made to our application.

#3 — Test Isolation

Isolating a test from other tests and external dependencies is essential. It shouldn’t be dependent on how other tests are run or on outside sources like databases or network connections. Therefore, it ought to execute without affecting other tests in the suite. And because of this, we might need to clean away persistent data once a test in our test suite has been performed.

#4 — Test Maintainability

A good test should be simple to update and maintain. It shouldn’t be extremely complicated or challenging to comprehend. As with the code itself, a good test should be written in a consistent manner and to the same coding standards.

We should be able to add, modify, and remove tests with ease from a maintainable test suite. Being structured, adhering to coding best practices, and creating a repeatable process that works for us and our team are the best ways to keep our test suite manageable.

#5 — Test Speed

A successful test should run rapidly. Testing that takes too long might be time-consuming and slow down development. It’s critical to create tests that can be executed fast without losing precision or thoroughness. A speedy test suite will give feedback faster and speed up the development process compared to a slow test suite. Therefore, the time required to run the test suite repeatedly for the fix bug scenario will be less than that of a sluggish speed test.

#5 — Test Expressive

Test suites are an excellent form of documentation since they are simple to read. It is best practice to always write code that describes the functionality we are testing.

We should make an effort to create a test suite that is detailed enough for another developer to read and comprehend the web application’s function. Additionally, our test suite is more likely to be kept up-to-date than a README or documentation that isn’t a functional part of the software because it is a component of it.

Here, Let’s Sum Up!

Test-driven development (TDD) helps us to ensure that the code is developed correctly. reliable, and maintainable by writing tests first and code later. So that the writing test will pass or fail based on the expected behavior of the code. TDD has been widely used in today’s programming and become such important practice in the development process. Although it takes a lot of effort to write the code first, it has many benefits including investment on the team to has more reliable and maintainable code. TDD provides a safety net that enables developers to develop and deploy software more quickly and with greater confidence.

--

--