Red-Green-Refactor: TDD Guide for C#

published on 13 January 2025

TDD flips the traditional coding process: write tests first, code second. The core of TDD is the Red-Green-Refactor cycle, which helps you write reliable, maintainable code step by step:

  • Red: Write a failing test to define the desired functionality.
  • Green: Write minimal code to pass the test.
  • Refactor: Clean up the code while keeping tests green.

Here’s why TDD is perfect for C#:

  • Works seamlessly with .NET tools like Visual Studio.
  • Encourages modular, testable code.
  • Improves design by focusing on small, testable components.

Whether you’re new to TDD or refining your skills, this guide walks you through frameworks like xUnit, NUnit, and MSTest, setting up tests in Visual Studio, and advanced techniques like mocking and CI/CD integration. Let’s dive in!

Setting Up TDD in a C# Environment

Let’s break down the key steps to get started with Test-Driven Development (TDD) in a C# environment.

Choosing a Testing Framework

Your testing framework is the backbone of your TDD workflow. Here’s a quick comparison of popular options for C#:

Framework Features Ideal Use Case
xUnit Modern design, supports parallel tests, built-in dependency injection Great for .NET Core projects and microservices
NUnit Extensive assertion library, custom attributes, parameterized tests Suited for large-scale enterprise apps
MSTest Seamless Visual Studio integration, familiar Microsoft tools Perfect for teams already using Microsoft’s ecosystem

Setting Up a Test Project in Visual Studio

Visual Studio

Here’s how to set up a test project in Visual Studio step by step:

  1. Add a Test Project: Right-click on your solution in Solution Explorer, then choose “Add > New Project.”
  2. Name and Structure: Select “Test Project,” give it a name with a .Tests suffix, and organize it to reflect your main project structure:
Solution/
  ├── src/
  │   └── Calculator/
  └── tests/
      └── Calculator.Tests/
          ├── Unit/
          └── Integration/
  1. Link and Install: Reference your main project and install your chosen testing framework through NuGet.

Writing Your First Test

Here’s an example of a basic test using xUnit, following the Red-Green-Refactor cycle:

[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
    // Arrange
    var calculator = new Calculator();

    // Act
    var result = calculator.Add(2, 3);

    // Assert
    Assert.Equal(5, result);
}

This test follows the Arrange-Act-Assert pattern: set up the object, call the method, and check the result.

With these fundamentals in place, you’re ready to dive deeper into refining your TDD approach.

Best Practices for Red-Green-Refactor in C#

Making Incremental Changes

TDD works best when you focus on small, manageable changes. Instead of trying to implement an entire feature at once, break it down into tiny, testable pieces. This keeps things clear and minimizes the chances of introducing bugs.

Here's an example of testing a string manipulation function:

[Fact]
public void Reverse_WordWithSpaces_PreservesSpaces()
{
    var stringHelper = new StringHelper();
    var result = stringHelper.Reverse("hello world");
    Assert.Equal("dlrow olleh", result);
}

Refactoring for Cleaner Code

Once your tests pass, shift your attention to cleaning up the code. The goal is to improve its structure and readability while keeping the behavior intact. Here are some practical techniques:

Technique Description Example
Extract Method Move repetitive code into separate methods Break down complex calculations into smaller methods
Remove Duplication Combine similar code patterns Use shared utility classes for common tasks
Apply SOLID Follow SOLID principles to restructure code Split large classes into smaller, focused ones

Refactoring ensures your codebase stays organized and easy to maintain, with tests acting as a safety net to catch any unintended changes.

Using Continuous Testing

Continuous testing tools, like those in Visual Studio 2022, can automatically run your tests as you write code. This helps catch issues early and speeds up the feedback loop.

To make the most of continuous testing:

  • Configure your IDE to run tests automatically when you save changes.
  • Use the test explorer to monitor real-time results.
  • Leverage test coverage tools to spot untested areas in your code.

For larger projects, you can integrate continuous testing into CI/CD pipelines to maintain quality across all stages of development. Here's an example of a test configuration in Azure Pipelines:

// Example test configuration in Azure Pipelines
steps:
- task: DotNetCoreCLI@2
  inputs:
    command: 'test'
    projects: '**/*Tests/*.csproj'
    arguments: '--configuration Release'
sbb-itb-29cd4f6

Advanced Techniques for TDD in C#

Techniques like mocking and integrating with CI/CD pipelines can refine your TDD workflow, helping you build reliable and scalable C# applications.

Using Mocks and Stubs

Mocks let you simulate dependencies, isolating components for precise testing. For example, the Moq library in C# allows you to control and verify dependency behavior:

[Fact]
public void OrderService_ProcessOrder_SendsEmailNotification()
{
    // Arrange
    var mockEmailService = new Mock<IEmailService>();
    var orderService = new OrderService(mockEmailService.Object);
    var order = new Order { Id = 1, CustomerEmail = "customer@example.com" };

    // Act
    orderService.ProcessOrder(order);

    // Assert
    mockEmailService.Verify(x => x.SendEmail(
        order.CustomerEmail,
        It.IsAny<string>(),
        It.IsAny<string>()
    ), Times.Once);
}

Here are some tips for effective mocking:

  • Focus on mocking interfaces to test interactions like sending emails or logging events.
  • Avoid overusing mocks; limit them to external services or dependencies that are hard to test directly.

By isolating dependencies, you can confidently test how your code handles edge cases and intricate logic.

Testing Edge Cases and Complex Logic

Systematic testing of edge cases ensures that your "Red" phase in TDD identifies potential failures early. Here's an example of edge case testing:

[Theory]
[InlineData(-1, "Value must be positive")]
[InlineData(0, "Value must be positive")]
[InlineData(101, "Value cannot exceed 100")]
public void ValidatePercentage_EdgeCases_ThrowsException(
    int value, string expectedMessage)
{
    var validator = new BusinessRuleValidator();
    var exception = Assert.Throws<ValidationException>(
        () => validator.ValidatePercentage(value));
    Assert.Equal(expectedMessage, exception.Message);
}

For testing more complex business rules, tools like SpecFlow can make scenarios easier to understand and maintain:

Feature: Order Processing
Scenario: Apply bulk discount
    Given a customer has 5 items in cart
    And total order value is $500
    When the order is processed
    Then a 10% bulk discount should be applied

Once your tests cover all critical scenarios, integrating them into your CI/CD pipeline ensures consistent quality checks.

Integrating with CI/CD Pipelines

Incorporating automated tests into a CI/CD pipeline helps maintain quality throughout development. Below is an example Azure DevOps configuration for running tests and collecting code coverage:

trigger:
- main

steps:
- task: DotNetCoreCLI@2
  inputs:
    command: test
    projects: '**/*Tests/*.csproj'
    arguments: '--collect:"XPlat Code Coverage"'

- task: PublishCodeCoverageResults@1
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'

The goal of TDD is to write clean code that works. [1]

Conclusion and Next Steps

The Red-Green-Refactor cycle is at the heart of effective Test-Driven Development (TDD) for C# projects, offering a clear framework for creating reliable and maintainable code.

Key Takeaways

TDD transforms the way C# applications are developed by focusing on writing tests before the actual code. This approach reduces bugs, makes code easier to maintain, and organizes development through the Red-Green-Refactor process. By following the setup and best practices outlined earlier, you can create dependable and maintainable applications.

This structured method is especially useful for complex C# projects where reliability is non-negotiable. For example, running continuous tests during development helps identify issues early, saving both time and resources as the project progresses.

Helpful Resources for C# Developers

Once you're comfortable with the fundamentals of TDD, these resources can help you improve your skills and keep up with the latest practices:

Resource Type Description
Daily Updates .NET Newsletter: A curated source of C# and .NET updates, including TDD tips

The best way to master TDD is through regular practice. Start with smaller components and gradually tackle larger, more complex ones as your confidence grows. Keep in mind that successful TDD isn't just about having a lot of tests - it's about making your codebase more reliable and easier to maintain.

Related posts

Read more