๐งช 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
Introduction to TDD
Benefits of TDD
Popular Testing Frameworks
Syntax Comparison
Project Setup
Red-Green-Refactor Cycle
Unit Testing
Integration Testing
Advanced Best Practices
Complementary Tools
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
3. ๐งฐ Popular Testing Frameworks
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
Create two projects:
MyApp
(class library)MyApp.Tests
(test project)
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
- 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:
Add a product to the database
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 keyName
: 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 classAccepts 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 injectionAddAsync
: adds a product and saves it to the databaseGetAllAsync
: 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 testsKeep 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
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.