It’s Easier Than You Think to Start With TDD in C#


I don’t play the lottery. I don’t care what my horoscope says.
I think most things about the world could be improved if people thought more about what they’re doing.
When someone gets upset with their computer, I tend to side with the computer. I think art is overrated, and tests are underrated. In fact, I don’t understand why tests aren’t art.

– Max Berry + my edit in bold italic

When it comes to coding, there are many ways to get the job done.

Some developers prefer to dive head-first into their code, getting as much done as possible as fast as possible.

Others prefer to take a more measured approach, where they start with the big picture and work out smaller tasks as they go. But one of the most popular approaches today sits in between these two extremes: test-driven development.

Test-driven development (TDD) is a software development methodology in which tests are written before production code. The tests define the functionality that has to be implemented. The code is then written to pass the tests. TDD is designed to ensure that code meets the software requirements.

This article contains answers to many questions about TDD in C# you might have, so read on.

What is test-driven development?

Test-driven development is a software development methodology in which the developer writes tests before writing the code for a new feature.

This approach can simplify development and ensure that code meets requirements. TDD allows developers to focus on individual requirements and ensure that code meets those requirements. This approach also simplifies development, as you can write code to meet specific tests.

Test-driven development is an iterative process. In each iteration, you write unit tests for a specific requirement. You then use those tests to drive code development to meet that requirement. You repeat this process until all requirements are met.

In unit testing, a unit is the fundamental testable part of an application. It is a cohesive part of the application that represents the main goal/functionality of the program. Unit tests validate these units by supplying various inputs and verifying outputs. If the output does not match what was expected, it is considered a failed automated test.

unit testing

Unit tests are good protection from bugs. And they check that small units of code work correctly in isolation. For more information on unit testing, check out the comprehensive guide on unit testing in C#. It has all answers to your questions.

In unit testing, you usually write and execute unit tests after you write the code. The TDD flips this idea on its head. You first write your unit test and then add the code.

Why?

The goal of TDD is to find errors in the code as early as possible so that you can fix them before substantial time is spent on developing the application. By writing tests first, you also check that the test is correct. If the test fails, you make the changes, and the test passes after that, then the test is written correctly. If not, you must go back to your unit test and check its logic.

Test-driven development helps reduce the number of defects in the final application by increasing internal quality. This improves customer confidence in your application and consequently benefits the organization.

TDD cycle

Test-driven development is a cycle of repeating short development iterations. In each iteration, developers write tests for new functionality before writing code to implement it. This cycle helps ensure that new code meets requirements and doesn’t break existing code.

The TDD cycle consists of three steps:

  • Red – write a failing test.
  • Green – write the minimal amount of code to pass the test.
  • Refactor – improve the code. All tests still must pass.
test-driven development (tdd)

This is a short overview of the TDD. If you dive deeper, those three steps can be divided into five more granular steps that you perform in every iteration.

The five steps of test-driven development

As described in Kent Beck’s book Test-Driven Development, the five steps of test-driven development are:

  1. Quickly add a test case.
  2. Run all tests and see the new one fail.
  3. Make a little change.
  4. Run all tests and see them all succeed.
  5. Refactor to remove duplication.

The explanation of the above steps:

1. For new, missing functionality, add a small test code indicating a missing implementation. A compile error also counts as a failing test. Check out the post about unit testing for more details on how to start with testing.

2. Run all tests in your test project to ensure that the new test fails and all the existing tests pass. This is an essential step. You want to be 100% sure that all the existing tests in the test suite are OK. If you add a missing implementation and the new test passes, but another existing test fails, you know that the new code has broken the test.

3. Make the change. The implementation at this stage should be a quick and dirty solution, code copy/pasted from stack overflow or the existing codebase.

Whoah, whoah, wait a minute. What kind of unholy word are you preaching here, I hear you ask? Shouldn’t I always write clean production code and avoid duplication? Well, you will get to it. This step is not about production-ready code. It’s about the testable code that works. And turns the test to green. Then, after you make a solution, any solution that works, you will make the production-ready code in step 5.

4. All tests should pass now. If that’s the case, then great, you can move to the last step. But if some tests fail now, you should go back to step 3, and make changes to our solution, so that all tests pass.

5. The last step, refactoring, is crucial, and you shouldn’t skip it. When your tests pass, and you have a working solution, remove any duplication introduced in step 3, and make all necessary refactorings. Some valuable resources to be better at this step are:

Refactoring is a crucial part of TDD. But some developers make four common mistakes in this step. Check out the separate article on those mistakes and how to avoid them.

The three laws of TDD

TDD is a simple and robust methodology that can give rapid feedback about your code. If you have tried TDD before, you know it is a fantastic method to develop your code. But if you are just starting with TDD, then it can be easy to want to write more tests or code than it’s necessary.

The three laws of TDD are:

  1. Write production code only to pass a failing test.
  2. You may not write more of a unit test than is sufficient to fail.
  3. You may not write any more production code than is sufficient to make the failing unit test pass.

Working on a new feature for a project can be daunting. You may know what you want the result to be but don’t know where to start. Or you may want to start coding and not testing the code until you write several hundreds of lines. But the goal of TDD is to have a testable production code at every point in time. That’s the only time you write a production code is when you need to pass a failing test (law #1).

Therefore, an excellent place to start is by writing a failing test. This will give you a direction to go in and something to work towards. Once you fail a test, you may not write more production code than is necessary to pass the test (law #3). This is important because it helps keep your code clean and focused on the task. It can be tempting to write more code than needed, but resist the urge and stick to the failing test.

In TDD, the goal of a unit test is to initially show that there is a missing functionality in your code. There is a common misconception that unit tests must be long and comprehensive to be effective. However, this is not the case. In fact, to move fast with TDD, write a short failing unit test (law #2) that will indicate you are missing the production code. In this case, even compile errors count as test failure. For example, you are referencing a class you haven’t yet created.

Is test-driven development agile?

Most people who work with the agile methodology focus on delivering working software as fast as possible to get their customer’s feedback. Agile development is an excellent method to keep the development cycle short.

Test-driven development is an agile process. It is a process that focuses on writing tests first and then coding to pass those tests. In TDD, you constantly run tests to get the feedback that the codebase is stable and that you can add new features without breaking existing functionality.

The test-driven development technique is based on the Agile Manifesto principles and extreme programming.

TDD is not different from Agile. They are both development methodologies that emphasize iterative and incremental development, where requirements and solutions evolve through collaboration between self-organizing, cross-functional teams.

How do you get started using TDD in C#?

So, how to start with practicing TDD in your C# projects? It depends on whether you are working on a new project or you need to maintain an existing project.

TDD in a new project

For a brand new project or for a project to which you want to apply TDD practices, there are some things that you need to do before starting with TDD:

  1. Make sure you have a well-structured and documented system design of the system. The system design is a north star you will follow towards creating your application. The tests in the TDD process serve as small steps to get you there.
  2. You can also write down test scenarios and requirements for the features you will develop using TDD. You will find them very useful in the next steps when you write tests: they will help you think about what should be tested before writing any tests!
  3. Write some tests that verify the correctness of your existing code, if you have any. You need a testing framework, such as xUnit, NUnit, or MSTest. You also need a mocking framework, such as moq, FakeItEasy, or NSubstitute. And lastly, use FluentAssertions to improve the readability of assert statements in the tests.
  4. Write the first feature of your application using TDD practices.
  5. Start practicing TDD in your application, implementing the test scenarios, and writing more tests for the newly added features. At this point, you should have a pile of well-tested code that works for most cases.

TDD in an existing project

For an existing project, things become somewhat more complex. If you already have unit tests for your project and can quickly write unit tests for your classes, then starting using TDD won’t be too hard. However, if you don’t have unit tests and classes are tightly coupled in your legacy code, then the first step should be converting your project to a more unit-oriented architecture:

  1. You need to start using dependency injection to decouple the existing classes.
  2. Classes should have as few responsibilities as possible.
  3. Adhere to other best practices and design principles.

On the other hand, if your existing project has a good system design, meaning classes are not tightly coupled or talking directly to the external resources (API or database), then you can start more easily with the TDD. The next time you need to add a feature, start by adding a test method first.

TDD in C# – Real life code example

Let’s go through one example to see how TDD works in practice. Suppose you were given the task of making a register module.

And the first task you need to do is to make sure the password your users type in is secure enough.

hacker
Sorry, but your password must contain:
an uppercase letter, a number, a haiku, a gang sign, a hieroglyph, and the blood of a virgin!

The password in the app has to:

  1. be at least 8 characters
  2. contain an upper-case character
  3. contain a digit (we won’t ask for the blood of a virgin in our app.)

You will place this logic in the IsPasswordSecure method of the RegisterViewModel class.

Implement first requirement

Start by creating a failing test to check the first requirement.

[Fact]
public void IsPasswordSecure_returns_false_if_password_has_less_than_8_characters()
{
    var registerViewModel = new RegisterViewModel();

    bool result = registerViewModel.IsPasswordSecure("1234567");

    Assert.False(result);
}

If you build and run the tests, you’ll get the following error:

The error marks start of the journey

This can be fixed by adding the following class:

public class RegisterViewModel
{
    public bool IsPasswordSecure(string password)
    {
        return true;
    }
}

Run the tests, and it fails again, this time with Assert.False() Failure error message. Let’s make the test pass by implementing the following in the IsPasswordSecure:

if (password.Length < 8)
{
    return false;
}

Now the test passes. A quick look at the code, nothing to refactor for now.

Implementing the second and third requirement

The second requirement is next. The password must contain an upper-case character. First, the failing test.

[Fact]
public void IsPasswordSecure_returns_false_if_password_does_not_contain_uppercase_character()
{
    var registerViewModel = new RegisterViewModel();

    bool result = registerViewModel.IsPasswordSecure("12345678");

    Assert.False(result);
}

The implementation, put this after the first if statement:

if (!password.Any(char.IsUpper))
{
    return false;
}

The second test now passes. The last step is refactoring. By looking at the tests, the initialization of the RegisterViewModel is duplicated in both tests. This can be refactored by applying the extract method refactoring. After refactoring, RegistrationViewModelTests looks like this:

public class RegisterViewModelTests
{
    [Fact]
    public void IsPasswordSecure_returns_false_if_password_has_less_than_8_characters()
    {
        var registerViewModel = CreateRegisterViewModel();

        bool result = registerViewModel.IsPasswordSecure("1234567");

        Assert.False(result);
    }

    [Fact]
    public void IsPasswordSecure_returns_false_if_password_does_not_contain_uppercase_character()
    {
        RegisterViewModel registerViewModel = CreateRegisterViewModel();

        bool result = registerViewModel.IsPasswordSecure("12345678");

        Assert.False(result);
    }

    private static RegisterViewModel CreateRegisterViewModel()
    {
        return new RegisterViewModel();
    }
}

The last requirement is that a password needs to contain a digit. The failing test case:

[Fact]
public void IsPasswordSecure_returns_false_if_password_does_not_contain_digit()
{
    RegisterViewModel registerViewModel = CreateRegisterViewModel();

    bool result = registerViewModel.IsPasswordSecure("ABCDEFGHI");

    Assert.False(result);
}

And the implementation. Add the new check after the first two if statements:

if (!password.Any(char.IsDigit))
{
    return false;
}

The test passes now.

The last test we can write is to check that IsPasswordSecure returns true when the password is ok:

[Fact]
public void IsPaswordSecure_returns_true_when_password_is_secure()
{
    var registerViewModel = CreateRegisterViewModel();

    bool result = registerViewModel.IsPasswordSecure("ABCDEFGH112");

    Assert.True(result);
}

All tests pass now, which means we are done.

Test explorer all green

The final implementation of IsPasswordSecure method looks like this:

public bool IsPasswordSecure(string password)
{
    if (password.Length < 8)
    {
        return false;
    }
    if (!password.Any(char.IsUpper))
    {
        return false;
    }
    if (!password.Any(char.IsDigit))
    {
        return false;
    }
    return true;
}

You are done with the implementation, and you have tests covering the new code. Great!

The most important thing to do when developing software is to test the code. This helps identify problems with the code as soon as they occur and fix them before the code can go out to production.

As I have mentioned, one of the goals of test-driven development is to find errors as early as possible. Because of this, TDD decreases the chance that major bugs will slip through to release.

The most significant advantages of test-driven development are:

  • Better code quality
  • Testing and debugging are built into the process
  • Better project documentation
  • Better productivity
  • High test coverage
  • It’s easier to refactor
  • Quality assurance is less expensive

(A detailed explanation of TDD advantages is in a separate article.)

Suppose you can accurately test your requirements and successfully finish writing unit tests before coding. In that case, this means your time for writing code is minimal. Therefore, you need to think about what needs to be done.

What are the drawbacks of TDD?

TDD can give you peace of mind and prevent you from making mistakes that might result in a broken app or even a security vulnerability. However, TDD can be a bit of a pain sometimes. Here are five drawbacks to using TDD:

  • It can slow down development, especially in the beginning.
  • It can be time-consuming.
  • It’s hard to introduce to existing legacy apps.
  • There can be a lot of tests to maintain.
  • It can be challenging to test some types of functionality.

The biggest drawback is that testing new code as you go can be difficult (if not impossible) during a deadline crunch. This isn’t an issue with TDD, but implementing a TDD process on a large project with many developers and tight deadlines can be confusing and frustrating for everyone involved.

Many developers don’t like writing unit tests and think they are a waste of time or even detrimental to the overall development process. This is because testing things manually is faster than writing code to test them. At least, that’s what they think.

However, the reality is different. You invest the initial time into writing tests, which is repaid in the long term. The proper test set will catch many bugs in the future.

Test-driven development best practices

Testing is always needed when it comes to creating a new feature or updating an existing feature to your application. It’s easy to assume that just because you have an application that does something, you know it works correctly. The problem arises when you have a bug or some functionality doesn’t work as expected. This is where you need to start testing.

Test-driven development is a methodology used for developing software that is efficient, high quality, and reliable. When it comes to creating software, we all know that testing is critical. But, how do you write tests?

To make the most out of TDD, follow the next best practices:

  • Focus on one feature at a time
  • Make sure each test fails initially
  • Write a test before writing production code
  • Have a good naming convention
  • Write independent tests
  • Automate tests
  • Tests should execute fast
  • Write short tests
  • Use mocks to reduce coupling
  • Refactor often
  • Know when to use TDD

For more information on the above practices, check out the separate article about them.

Test-driven development in DevOps

DevOps methodology

When we talk about DevOps, we mean the process of combining software development and operations so that a product gets released more often and has better quality. When it comes to software development, DevOps practices have been gaining in popularity in recent years.

DevOps teams rely on various tools to streamline the software development process. One of these tools is test-driven development (TDD). TDD is an essential tool for DevOps teams because it helps ensure that code is of high quality and meets the end-users’ requirements.

It also helps speed up the development process by catching errors early on.

TDD can be used in conjunction with other DevOps practices, such as continuous integration (CI) and continuous delivery (CD). When used together, these practices can help speed up the software development process and improve the quality of the code.

Critics of the test-driven development

Kent Beck invented TDD. It was initially developed as a way to improve the productivity of a team that was already using a unit testing approach. In practice, TDD has been shown to improve the quality of the code and the developer’s productivity. Since then, it has become a widely used software development process.

Some software developers argue that TDD is a good idea, but they claim that it is not practical because it requires a lot of investment and consistency. Let’s go over some common questions about TDD.

Why is practicing TDD so hard?

TDD is a handy tool for developers but can be challenging to practice. One of the biggest reasons it can be tough to stick to TDD is that it can be time-consuming. In a fast-paced work environment, it can be hard to justify taking the time to write tests before coding. Additionally, it can be difficult to think about edge cases and test for them ahead of time. It’s often easier to just write the code and test it after the fact. However, with TDD, if a test isn’t written for a particular piece of code, it’s likely that that code will never be tested. This can lead to significant issues down the road.

Is TDD worth it?

Test-driven development requires programmers to write tests for each piece of code before they write the code itself. The thinking behind test-driven development is that programmers can think more clearly about what the code should do and how to write it by writing the tests first. In addition, because the tests act as a safety net, test-driven development can help programmers catch bugs early and prevent them from becoming bigger problems later on.

There is a lot of debate on whether test-driven development (TDD) is worth the time and effort. Some developers believe that TDD slows down the development process, while others believe that it helps to find bugs early on and prevent them from becoming bigger issues later. Valid points are coming from both camps.

But there are seven reasons why TDD might be worth your time:

  • It helps you write better code
  • You can see the design of your code before it is written
  • Reduces the number of bugs in your code and improves quality assurance
  • The tests are easy to read and debug when something goes wrong
  • You can save time by writing fewer, more reliable lines of code
  • You can safely experiment with different solutions
  • It enables refactoring

Is TDD slow?

Yes. TDD is slower than traditional development. It is a process that requires a lot of testing during development time. This means that it takes time to get it right. This is why we recommend that you don’t rush the development process and always plan your tests.

TDD can be slow at first, but it becomes quicker and more efficient with practice. In the long run, you end up with the code covered with tests and fewer bugs.

Is TDD still popular?

Popularity is difficult to measure. However, from observing various online forums and coding communities, TDD is still quite popular among developers. This is likely because TDD is a very efficient way to ensure that code is high-quality and bug-free.

TDD is one of the most commonly used Agile methodologies, but lately, some say it has become less popular. People are starting to feel like the benefits of using it have been lost in the process, even though it has made many companies successful.

Some people are beginning to question its value and whether it should be replaced by something else. Some even say that TDD is dead, but there will always be projects where developers can use TDD effectively.

Why TDD is not usually used?

It sounds logical that a practice like Test-driven development is a good way to develop a new piece of software. But why do most developers don’t practice it?

First of all, the TDD is usually not mandatory to be used in old and new projects. The first thing most developers do when they start a new project is to write code. They forget about testing and then, after releasing the project, find that it doesn’t work.

There are several reasons why TDD is not usually used:

  •  It can be time-consuming to write tests first. This can make TDD impractical for projects with deadlines or tight schedules. 
  • TDD can produce tests that are too coupled to the code implementation. This makes refactoring more difficult.
  • Some developers don’t use TDD because they find it unnecessary.

What is the difference between TDD and BDD?

TDD and BDD (Behavior Driven Development) are both software development styles, but they don’t exactly mean the same thing. However, they can go hand in hand, so learning one helps you better understand the other.

TDD, or Test-Driven Development, is a development methodology where tests are written before production code. BDD, or Behavior-Driven Development, is a development methodology where tests are written in a natural language that can be understood by business analysts, developers, and testers.

When you are testing your code, you can write unit tests to test small pieces of functionality. But if you are writing more complex code, you can use behavior-driven development. BDD helps you to organize the tests into scenarios. You can then implement the desired behavior in your code.

What is the difference between TDD and ATDD?

One term that pops up often when talking about TDD is the ATDD. ATDD is an extension of test-driven development and stands for Acceptance Test Driven Development.

Acceptance Test-Driven Development (ATDD) can provide a rigorous and repeatable way to test the software to ensure that the desired functionality is achieved. The objective is to create a set of automated tests, called acceptance tests, which are used to evaluate software development. ATDD is a collaboration between the client, developer, and tester as they work together to define a set of test cases to validate that the software meets the client’s expectations.

TDD and ATDD are both methods of software development that focus on tests. However, TDD focuses on unit tests, which test the functionality of individual pieces of code. In contrast, ATDD focuses on acceptance tests, which test the software’s functionality as a whole.

There are several essential differences between test-driven development and acceptance test-driven development:

  1. The purpose of TDD is to write code that passes the unit test; the purpose of ATDD is to write code that fulfills a requirement from the stakeholders’ or user’s perspective.
  2. In TDD, the developer writes unit tests. In ATDD, the developer writes end-to-end tests. This can be integration tests or UI tests.
  3. In TDD, the developers define and write tests. In ATDD, the stakeholders define the tests a software application must satisfy.
  4. In ATDD, you can also run manual acceptance tests regularly to verify that your code is still working as intended by the stakeholders.
  5. In ATDD, you write tests that correspond to a user’s expectations, while in TDD, you write tests that are more specific to your code.
  6. In ATDD, you concentrate on the user’s expectations and the automated acceptance test. In TDD, you concentrate on the structure of your application code.
  7. In ATDD, all developers work together to create automated acceptance tests. In TDD, one developer works alone or with a small team creating unit tests based on real-world examples where you implement features and then perform manual testing to confirm that they are working correctly.
  8. In TDD, you focus on one feature at a time, while in ATDD, you focus on entire scenarios (e.g., user registration) or the overall flow of the application.

TDD can help you avoid fixing the same bug over and over

Test-driven development is a great way to produce high-quality code with high test coverage. The example in this post goes through implementing a new feature using TDD, but you can use TDD to fix bugs.

To prevent the regression bugs, follow the process:

1. Write a failing test that indicates the code is broken.

2. Find a fix the bug.

3. Run all tests. All should pass now.

4. Refactor if necessary.

Investing in testing it’s worth the effort because it will help you avoid regressions.

Conclusion

In conclusion, TDD is a software development method that focuses on writing tests before writing code. You write the test first to ensure that your code works as expected. In addition, you also make sure that your code is easy to maintain and that it adheres to best practices.

As a software engineer, it is not hard to see why so many people these days are against the idea of Test-Driven Development (TDD). Some say that having to create tests for something that is not yet built seems weird, which is why they choose to skip TDD altogether or do it only occasionally.

The truth is TDD is not about how it feels when you code, but it’s about mastering a skill that will make you a better programmer.

Recent Posts