๐Ÿงช TDD in C# โ€” From Basics to Advanced

This article is a complete guide to Test-Driven Development (TDD) in C#. Weโ€™ll walk through the core concepts, explore the main testing frameworks, compare syntax styles, implement both unit and integration tests with real examples, and wrap up with best practices and complementary tools.


๐Ÿ“š Table of Contents

  1. Introduction to TDD

  2. Benefits of TDD

  3. Popular Testing Frameworks

  4. Syntax Comparison

  5. Project Setup

  6. Red-Green-Refactor Cycle

  7. Unit Testing

  8. Integration Testing

  9. Advanced Best Practices

  10. Complementary Tools

  11. Conclusion


1. ๐Ÿš€ Introduction to TDD

Test-Driven Development is a development approach where you write tests before implementing the actual functionality. The cycle is:

  • Red: Write a failing test

  • Green: Write the minimum code to make it pass

  • Refactor: Clean up the code while keeping tests green

This process leads to cleaner design, more testable code, and fewer bugs in production.


2. โœ… Benefits of TDD

  • Immediate feedback on regressions

  • Modular and well-defined code

  • Living documentation through tests

  • Safer refactoring

  • Greater confidence in continuous deliver



4. ๐Ÿ” Syntax Comparison

Same test case for Calculator.Add(2, 3) == 5:

// xUnit
using Xunit;

public class CalculatorTests
{
    [Fact]
    public void Add_TwoPlusThree_ReturnsFive()
    {
        var calc = new Calculator();
        Assert.Equal(5, calc.Add(2, 3));
    }
}
// NUnit
using NUnit.Framework;

[TestFixture]
public class CalculatorTests
{
    [Test]
    public void Add_TwoPlusThree_ReturnsFive()
    {
        var calc = new Calculator();
        Assert.AreEqual(5, calc.Add(2, 3));
    }
}
// MSTest
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class CalculatorTests
{
    [TestMethod]
    public void Add_TwoPlusThree_ReturnsFive()
    {
        var calc = new Calculator();
        Assert.AreEqual(5, calc.Add(2, 3));
    }
}

5. ๐Ÿ› ๏ธ Project Setup

  1. Create two projects:

    • MyApp (class library)

    • MyApp.Tests (test project)

  2. Install NuGet packages in the test project:

dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Moq
dotnet add package Microsoft.EntityFrameworkCore.InMemory
  1. Add reference to the main project:
dotnet add reference ../MyApp/MyApp.csproj

6. ๐Ÿ” Red-Green-Refactor Cycle

  • Red: Write a test that fails

  • Green: Write the minimum code to pass

  • Refactor: Improve the design while keeping tests green

This cycle is the core of TDD.


7. ๐Ÿงช Unit Testing

Unit tests validate isolated functionality. Example using xUnit and Moq:

// MyApp/Services/OrderService.cs
namespace MyApp.Services
{
    public interface IPaymentProcessor
    {
        void Charge(decimal amount);
    }

    public class OrderService
    {
        private readonly IPaymentProcessor _paymentProcessor;

        public OrderService(IPaymentProcessor paymentProcessor)
        {
            _paymentProcessor = paymentProcessor;
        }

        public void ProcessOrder(decimal total)
        {
            _paymentProcessor.Charge(total);
        }
    }
}
// MyApp.Tests/OrderServiceTests.cs
using Moq;
using Xunit;
using MyApp.Services;

public class OrderServiceTests
{
    [Fact]
    public void ProcessOrder_WhenCalled_ChargesPaymentProcessor()
    {
        var mockPayment = new Mock<IPaymentProcessor>();
        var service = new OrderService(mockPayment.Object);
        decimal orderTotal = 99.90m;

        service.ProcessOrder(orderTotal);

        mockPayment.Verify(p => p.Charge(orderTotal), Times.Once);
    }
}

8.๐Ÿ”— Integration Testing in C# โ€” Code Breakdown

The goal of this integration test is to verify that the ProductRepository can:

  1. Add a product to the database

  2. Retrieve that product correctly

We use Entity Framework Core with an in-memory database to simulate real persistence without relying on an external database.


๐Ÿงฑ 1. Model: Product.cs

// MyApp/Data/Product.cs
namespace MyApp.Data
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

๐Ÿ” What it does:
Defines the Product entity with two properties:

  • Id: the primary key

  • Name: the product name

This class is mapped by Entity Framework to a table in the database.


๐Ÿ—๏ธ 2. DbContext: AppDbContext.cs

// MyApp/Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;

namespace MyApp.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options)
            : base(options) { }

        public DbSet<Product> Products { get; set; }
    }
}

๐Ÿ” What it does:

  • Inherits from DbContext, the EF Core base class

  • Accepts configuration via DbContextOptions (e.g., database type)

  • Exposes a Products collection, representing the table of products

This context manages the connection to the database and tracks changes.


๐Ÿ“ฆ 3. Repository: ProductRepository.cs

// MyApp/Repositories/ProductRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MyApp.Data;

namespace MyApp.Repositories
{
    public class ProductRepository
    {
        private readonly AppDbContext _db;

        public ProductRepository(AppDbContext db)
        {
            _db = db;
        }

        public async Task<IEnumerable<Product>> GetAllAsync()
        {
            return await _db.Products.ToListAsync();
        }

        public async Task AddAsync(Product product)
        {
            _db.Products.Add(product);
            await _db.SaveChangesAsync();
        }
    }
}

๐Ÿ” What it does:

  • Receives AppDbContext via dependency injection

  • AddAsync: adds a product and saves it to the database

  • GetAllAsync: retrieves all products from the database

This repository encapsulates data access logic, making it easier to test and maintain.


๐Ÿงช 4. Integration Test: ProductRepositoryIntegrationTests.cs

๐Ÿ”ง Create In-Memory Database Options

// MyApp.Tests/ProductRepositoryIntegrationTests.cs
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Xunit;
using MyApp.Data;
using MyApp.Repositories;

public class ProductRepositoryIntegrationTests
{
    private DbContextOptions<AppDbContext> CreateInMemoryOptions()
    {
        return new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase("TestDb")
            .Options;
    }

    [Fact]
    public async Task AddAsync_ThenGetAllAsync_ReturnsInsertedProduct()
    {
        var options = CreateInMemoryOptions();

        using (var context = new AppDbContext(options))
        {
            var repo = new ProductRepository(context);
            var newProduct = new Product { Name = "Test Product" };
            await repo.AddAsync(newProduct);
        }

        using (var context = new AppDbContext(options))
        {
            var repo = new ProductRepository(context);
            var products = await repo.GetAllAsync();

            Assert.Single(products);
            Assert.Equal("Test Product", products.First().Name);
        }
    }
}

๐Ÿ” What it does:
Creates a configuration for EF Core to use an in-memory database named "TestDb". This allows us to simulate real database operations without physical persistence.


๐Ÿงช Add and Retrieve Product

[Fact]
public async Task AddAsync_ThenGetAllAsync_ReturnsInsertedProduct()
{
    var options = CreateInMemoryOptions();

    using (var context = new AppDbContext(options))
    {
        var repo = new ProductRepository(context);
        var newProduct = new Product { Name = "Test Product" };
        await repo.AddAsync(newProduct);
    }

๐Ÿ” What it does:

  • Creates a new EF context with the in-memory database

  • Instantiates the repository

  • Adds a product named "Test Product"

  • Saves it to the database

This simulates a real insert operation.


๐Ÿ” Verify Product Retrieval

    using (var context = new AppDbContext(options))
    {
        var repo = new ProductRepository(context);
        var products = await repo.GetAllAsync();

        Assert.Single(products);
        Assert.Equal("Test Product", products.First().Name);
    }
}

๐Ÿ” What it does:

  • Creates a new context with the same in-memory database

  • Retrieves all products

  • Asserts that there is exactly one product

  • Asserts that its name is "Test Product"

โœ… Expected result: The test passes if the product was correctly saved and retrieved.


๐Ÿง  Why Is This an Integration Test?

Because it tests the real integration between:

  • The repository

  • The EF Core context

  • The database (simulated in memory)

Unlike a unit test, this does not use mocks โ€” it validates actual behavior across components.


9. ๐Ÿง  Advanced Best Practices

  • Use naming convention: Method_Scenario_ExpectedResult

  • Use [Theory] (xUnit) or [TestCase] (NUnit) for parameterized tests

  • Keep tests fast and isolated

  • Avoid external dependencies in unit tests

  • Use code coverage tools to identify untested areas


10. ๐Ÿงฉ Complementary Tools

  • Code Coverage: coverlet, OpenCover

  • CI/CD: GitHub Actions, Azure Pipelines

  • Static Analysis: SonarQube, Roslyn Analyzers

  • Test Data Generators: AutoFixture, Bogus


11. ๐Ÿ Conclusion

TDD in C# is a powerful practice for building reliable and maintainable software. By combining unit and integration tests, following the Red-Green-Refactor cycle, and integrating modern tools, you turn testing into a design ally.

Start small, grow consistently, and make testing an essential part of your development workflow.

#TDD #CSharp #DotNet #xUnit #NUnit #MSTest #SoftwareTesting #CleanCode #DevOps #TestAutomation #QualityAssurance #CodeQuality #RefactorWithConfidence #EntityFramework #InMemoryTesting #Moq

0
Subscribe to my newsletter

Read articles from Johnny Hideki Kinoshita de Faria directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Johnny Hideki Kinoshita de Faria
Johnny Hideki Kinoshita de Faria

Technology professional with over 15 years of experience delivering innovative, scalable, and secure solutions โ€” especially within the financial sector. I bring deep expertise in Oracle PL/SQL (9+ years), designing robust data architectures that ensure performance and reliability. On the back-end side, Iโ€™ve spent 6 years building enterprise-grade applications using .NET, applying best practices like TDD and clean code to deliver high-quality solutions. In addition to my backend strengths, I have 6 years of experience with PHP and JavaScript, allowing me to develop full-stack web applications that combine strong performance with intuitive user interfaces. I've led and contributed to projects involving digital account management, integration of VISA credit and debit transactions, modernization of payment systems, financial analysis tools, and fraud prevention strategies. Academically, I hold a postgraduate certificate in .NET Architecture and an MBA in IT Project Management, blending technical skill with business acumen. Over the past 6 years, Iโ€™ve also taken on leadership roles โ€” managing teams, mentoring developers, and driving strategic initiatives. I'm fluent in agile methodologies and make consistent use of tools like Azure Boards to coordinate tasks and align team performance with delivery goals.