What is TDD?

Nuno Cancelo
7 min readMar 10, 2021

--

Image by James Osborne from Pixabay

In the last few years, I have been a big advocate of continuous code quality. With a few minor changes in our development cycle and strategies, we can manage and improve the software’s quality.

In this article, I’ll focus on Unit Tests, Test Driven Development, how this mind shift improves the code quality, and, therefore, the software’s quality. There are other types of tests, but they are out of this article’s scope, although they will be mention.

The Challenge

In a scenario without unit tests, our development plan is pretty forward:

We design the feature, and then we rush to implement the feature. Once done, we run the application, manually work the application until it reaches what you want to test, and then manually try the part. If it works, great. If not, redo the implementation.

This methodology has been working for decades in development around the world, so what is the challenge? You may ask.

Well, I can highlight a few challenges.

Productivity is not optimized.

Every time we execute the application to do our tests, we are wasting time:

  • loading the application
  • working the application until we reach the feature
  • manually test the feature

Let’s assume that for a simple feature (fill a form and when clicked the submit button, the database is updated), this process will take 2 minutes. Two minutes to test if the feature updates the database. It is too long, considering that it will take much more time in more complex parts.

Breaking changes risk

While we are manually testing the feature, we are not testing other features. Does the change that we made impact other related features?

The risk of breaking something increases through time increases even more if the development team changes.

Missing scenarios Coverage

Getting back to our simple feature scenario: do we cover all methods? Do we test all possible and impossible inputs? Maybe initially, but with time programmers tend to focus on the case scenario in their hands and neglect the others.

Inability to add tests to the DevOps cycle

Without tests, there is no step in the DevOps cycle to execute automated tests. Without this step, we cannot assure the quality of feature development. And if we can’t guarantee, well, we are not efficient.

Test-Driven Development

One strategy to improve our application quality is by shifting our development methodology from the traditional Code First Approach to the Test Driven Development, also known as TDD.

Martin Fowler defines TDD as:

Test-Driven Development (TDD) is a technique for building software that guides software development by writing tests. Kent Beck developed it in the late 1990’s as part of Extreme Programming. In essence, you follow three simple steps repeatedly:

  • Write a test for the next bit of functionality you want to add.
  • Write the functional code until the test passes.
  • Refactor both new and old code to make it well structured.

These three simple steps are the foundations of TDD and are the inspiration to the well-known Red, Green, Refactor diagram:

Another renowned software engineer, Robert Martin (also known as ‘Uncle Bob’), complement Martin Fowler definition with ‘The Three Laws of TDD’:

Over the years, I have come to describe Test Driven Development in terms of three simple rules. They are:

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

The hints of both authors provide enough information to clarify what the Red, Green, Refactor stages are all about designing the test then develop the code.

Red Stage

In Red Stage, we start by writing the unit test code without having any business code implemented. The test should describe what is to be executed, and the test should fail.

This stage is fair to assert that a compilation error fails because the test didn’t pass.

Green Stage

In Green Stage, we implement enough code to pass the defined unit test. At this time, this implementation doesn’t have to be performant, efficient, optimized, or elegant after writing just enough code to pass the test.

Refactor Stage

In Refactor Stage, we do some code cleaning by refactoring the implementation to be:

• Optimized

• Efficient

• Clean

• DRY

• SOLID

By the end of this stage, the test must be executed and must pass.

And the cycle goes on until you don’t have more features to implement.

Unit Testing

So TDD is all about writing unit tests, but what is a unit test?!

First, we need to define what is our “Unit.” The “Unit” is the smallest part of the system that makes sense to identify with being tested. In most cases, the “Unit” is equivalent to a method or function. So, in “Unit Testing,” we develop tests to assert the methods use cases.

In Software Testing, “Unit Testing” is the lowest layer of the Testing Pyramid:

Definition by ISTQB

o Integration testing: A test level that focuses on interactions between components or systems.

o End to End Testing: A type of testing in which business processes are tested from start to finish under production-like circumstances.

o UI testing: Testing performed by interacting with the software under test via the graphical user interface.

As mentioned before, these layers of testing are out of the scope of this article.

FIRST Principles

FIRST is an acronym to identify quickly five principles to keep in mind while developing the tests, and therefore the feature development.

F — FAST

The tests must have fast executions. Otherwise, the development team will not execute them as part of the development process.

I — ISOLATED/INDEPENDENT

The test only depends on himself. No test should rely on the result of other tests.

R — REPEATABLE

The test can repeatedly execute and get the same outcome; otherwise, it is a lousy test.

S — SELF-VALIDATION

The test should be able to validate their assertion and result.

T — THOROUGH

The tests should cover most, if not all, case scenarios. Should aim to case Coverage and not to code Coverage. It is better to have less code coverage and better case coverage than the opposite.

With these five principles, we can develop unit tests with quality.

Recommended Tests

Which scenarios could we test?

  • Corner/edge/boundary values.
  • Large data sets
  • Security with users having different roles
  • Large values
  • Exceptions and errors
  • Illegal arguments or bad inputs

In summary, if it is essential to assert the best-case scenario, it is even more important that when something goes wrong (and end-users can be very creative), we also cover all our bases.

AAA Pattern

The AAA pattern is also an acronym for the unit test structure, and it stands for Arrange, Act, Assert.

Arrange

In this section, we prepare all data structures, object instantiation, parameters, dependencies, etc.

Act

In this section, the code will execute the code necessary for the given test.

Assert

Finally, we evaluate the result is what is expected or not.

The code should be similar to the following:

[Theory][InlineData(1,100)]public void processRange_Should_Print_Expected_String(int min, int max){// ArrangeFizzBuzz fizzBuzz = new FizzBuzz();// ActString result = fizzBuzz.ProcessRange(min,max);// Assertresult.ShouldNotBeNull();result.Trim().ShouldNotBeEmpty();result.ShouldBe(FizzBuzzTestsExpectations.ExpectedString);}

So far, we have been talking about writing the tests first and develop the feature after. So, how about the legacy application without unit tests? And all that other applications that are under maintenance that don’t have difficulties?

Unit Testing on Existing Applications

In this case, and it is more frequent than it should be, we have to make some cost-benefit analysis. Meaning, we will not stop all developments from applying unit tests, but if the application has new features to be added or bug fixing, it will add unit tests to the new parts and the components that involve them.

Before changing a method, we should create a unit test to assert the process’s behavior, then develop more tests to cover the usage scenarios and finally develop new tests to the new features.

Simple enough?

Depending on how the application is structured, there will be (some more than others) challenges like mocking the code dependencies, access to external components, databases, filesystems, web services, etc. Another challenge is how to write a test that covers 200, 500, 1000, nK lines of code in a method.

In these scenarios, we have two plans:

  • Due to the complexity, leave it.
  • Refactor the method, apply some design patterns, and test small parts of the technique.

It is not an easy choice, I admit. So, choose wisely.

Conclusion

This article was a gentle introduction to Test Driven Development and Unit Testing. We can apply the software engineering belt tool to increase the quality of our software to automate the tests, add them as a step in a DevOps cycle, and apply continuous quality management.

Developing unit tests needs a mind shift because they are essential. In the beginning, the development is slower because we are getting started, but after a while, it becomes part of us as second nature. It would help if you embraced the shift.

References

--

--

Nuno Cancelo
Nuno Cancelo

Written by Nuno Cancelo

Senior Software Engineer | Dev Lead | Tech Lead | Code Cleaner | Community Leader | Puzzle Solver | Dev Evangelist | Beer Lover

Responses (1)