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

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.

We can define test-driven development as a red, green, refactor programming process. Write a failing test (red), make it pass by writing a missing implementation (green), and make changes to the code so it’s production-ready (refactor). Tests are written before the code, before the actual implementation. The tests guide the developer to the path of the actual implementation. They drive the implementation, hence the name test-driven development (TDD).

It looks so easy, like a painting made by some famous painter. So easy, almost like an art.

What is test-driven development?

The term test-driven development describes a process of software development in which the developer writes tests before writing the actual code for a new feature.

In unit testing, a unit is the basic 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, then it is considered a failed automated test.

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 they can be fixed before substantial time is spent on developing the actual application. By writing tests first, you also make 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 need to go back to your unit test and check its logic.

Test-driven development helps to 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 as a whole.

TDD cycle

TDD approach follows the following phases, as described in Kent Beck’s book Test-Driven Development:

  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 fine. If you add a missing implementation and the new test passes, but another existing test fails, you know for sure 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 from 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. 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. At this point, when your tests pass and you have a working solution, remove any duplication introduced in step 3, and make all necessary refactorings. Some useful resources to be better at this step are:

How do you get started using TDD in C#?

So, how to start with practicing TDD in your C# projects? It depends 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 well-structured and documented the 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’ll 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 you can easily write unit tests for your classes, then start 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# – 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!

What are the advantages of Test-driven development?

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. A second advantage is that TDD helps you more accurately estimate how much work it will take to add a new feature or fix a bug.

If you can accurately test your requirements and successfully finish writing unit tests before coding, this means your time for writing code is minimal, and therefore you really need to think about what needs to be done when writing code.

What are the disadvantages of Test-driven development?

The biggest drawback is that testing new code as you go can be difficult (if not impossible) during a deadline crunch. This isn’t really 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 that 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, and this is repaid in the long term. The proper set of tests will catch a lot of bugs in the future.

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 it 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 in order to validate that the software is meeting the client’s expectations.

There are several important 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 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 that 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 on the automated acceptance test as a whole. 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 using test-driven development methodology that is 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.

The guaranteed method for avoiding 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 shown in this post goes through implementing a new feature using TDD, but TDD can be used to fix bugs. The process when fixing a bug is:

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

Conclusion

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). Having to create tests for something that is not yet built seems weird, some say, which is why they choose to either 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.