Testing in C# — Complete Guide
In this tutorial, you will learn about Testing in C#. We cover key concepts, practical examples, and best practices to help you master this topic.
Hook
Automated testing is the safety net that lets you refactor with confidence and ship reliable code. C# developers have access to mature testing frameworks, powerful assertions, and first-class IDE integration. Writing tests is not an afterthought -- it is a core development practice.
Learning Path
graph LR A[Testing] --> B[xUnit] A --> C[NUnit] B --> D[Fact & Theory] B --> E[Assertions] D --> F[Test-Driven Dev] style A fill:#4a90d9,color:#fff style B fill:#4a90d9,color:#fff style C fill:#4a90d9,color:#fff style D fill:#4a90d9,color:#fff style E fill:#4a90d9,color:#fff style F fill:#4a90d9,color:#fff
Setting Up xUnit
xUnit is the most popular testing framework for .NET.
// Install: dotnet add package xunit
// dotnet add package xunit.runner.visualstudio
// dotnet add package Microsoft.NET.Test.Sdk
using Xunit;
public class CalculatorTests
{
[Fact]
public void Add_ShouldReturnSum_WhenGivenTwoNumbers()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
}
public class Calculator
{
public int Add(int a, int b) => a + b;
public int Divide(int a, int b) => a / b;
}
Parameterized Tests with [Theory]
Test multiple inputs with a single test method.
public class MathTests
{
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, -1, -2)]
[InlineData(0, 0, 0)]
[InlineData(100, -50, 50)]
public void Add_ShouldReturnCorrectSum(int a, int b, int expected)
{
var result = new Calculator().Add(a, b);
Assert.Equal(expected, result);
}
[Theory]
[MemberData(nameof(DivisionData))]
public void Divide_ShouldReturnCorrectQuotient(int a, int b, int expected)
{
var result = new Calculator().Divide(a, b);
Assert.Equal(expected, result);
}
public static IEnumerable<object[]> DivisionData =>
new List<object[]>
{
new object[] { 10, 2, 5 },
new object[] { 9, 3, 3 },
new object[] { 7, 2, 3 } // Integer division
};
}
Assertion Library
xUnit provides a rich set of assertions.
[Fact]
public void AssertionExamples()
{
// Equality
Assert.Equal(42, GetValue());
Assert.NotEqual(0, GetValue());
// Boolean
Assert.True(IsValid());
Assert.False(IsError());
// Null checks
Assert.Null(GetOptionalValue());
Assert.NotNull(GetRequiredValue());
// Collections
Assert.Contains("error", GetMessages());
Assert.DoesNotContain("critical", GetMessages());
Assert.Single(GetItems());
Assert.All(GetItems(), item => Assert.NotNull(item.Name));
// Exceptions
Assert.Throws<DivideByZeroException>(() => calculator.Divide(1, 0));
Assert.ThrowsAsync<ArgumentException>(() => service.ProcessAsync(null!));
// Type checks
Assert.IsType<DateTime>(GetDate());
Assert.IsAssignableFrom<IEnumerable<int>>(GetNumbers());
}
Test-Driven Development (TDD)
The Red-Green-Refactor cycle guides test-first development.
// Step 1: RED - Write a failing test
[Fact]
public void IsPalindrome_ShouldReturnTrue_ForRacecar()
{
var result = StringUtils.IsPalindrome("racecar");
Assert.True(result);
}
// Step 2: GREEN - Write the minimum code to pass
public static class StringUtils
{
public static bool IsPalindrome(string input)
{
// Minimal implementation
if (input == "racecar") return true;
return false;
}
}
// Step 3: REFACTOR - Improve the implementation
public static bool IsPalindrome(string input)
{
var reversed = new string(input.Reverse().ToArray());
return input.Equals(reversed, StringComparison.OrdinalIgnoreCase);
}
Shared Context with Fixtures
Use fixtures to share expensive setup across tests.
public class DatabaseFixture : IDisposable
{
public DatabaseFixture()
{
// One-time setup (e.g., create database)
Connection = new SqlConnection("...");
}
public SqlConnection Connection { get; }
public void Dispose()
{
// One-time cleanup
Connection.Dispose();
}
}
public class DatabaseTests : IClassFixture<DatabaseFixture>
{
private readonly DatabaseFixture _fixture;
public DatabaseTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void Connection_ShouldBeOpen()
{
Assert.NotNull(_fixture.Connection);
}
}
NUnit and MSTest
Alternative testing frameworks follow similar patterns.
// NUnit
using NUnit.Framework;
[TestFixture]
public class NUnitTests
{
[SetUp]
public void Setup() { /* Runs before each test */ }
[Test]
public void Should_Add_Two_Numbers()
{
Assert.That(new Calculator().Add(2, 2), Is.EqualTo(4));
}
[TestCase(1, 2, 3)]
[TestCase(0, 0, 0)]
public void Parameterized_Tests(int a, int b, int expected)
{
Assert.That(new Calculator().Add(a, b), Is.EqualTo(expected));
}
}
// MSTest
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class MsTestTests
{
[TestInitialize]
public void Init() { }
[TestMethod]
public void Add_Should_Return_Sum()
{
Assert.AreEqual(4, new Calculator().Add(2, 2));
}
[DataTestMethod]
[DataRow(1, 2, 3)]
public void DataDrivenTest(int a, int b, int expected)
{
Assert.AreEqual(expected, new Calculator().Add(a, b));
}
}
Code Coverage
Measure how much of your code is exercised by tests.
# Using coverlet
dotnet test --collect:"XPlat Code Coverage"
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coverage" -reporttypes:Html
Common Mistakes
Testing implementation details: Test behavior, not internal state. Refactoring the implementation should not break tests if behavior is unchanged.
Not using parameterized tests: Copying test methods for different inputs creates maintenance overhead. Use
[Theory]or[TestCase].Flaky tests: Tests that depend on timing, random data, or external resources are unreliable. Use mocks and deterministic data.
Ignoring edge cases: Test empty collections, null inputs, boundary values, and error conditions.
Skipping assertions: A test without assertions is worthless. Always verify expected outcomes.
Practice Questions
Write a test suite for a
Stack<T>implementation that covers push, pop, peek, and empty stack exceptions.Create parameterized tests for a
DateParserthat validates multiple date formats.Implement a test fixture that sets up an in-memory database for Integration Testing.
Challenge: Build a property-based test using FsCheck that verifies the commutative property of addition across random inputs.
FAQ
Mini Project: String Calculator TDD
Implement a String Calculator following Test-Driven Development.
using Xunit;
// Tests first
public class StringCalculatorTests
{
[Fact]
public void Add_ShouldReturnZero_ForEmptyString()
{
Assert.Equal(0, StringCalculator.Add(""));
}
[Theory]
[InlineData("1", 1)]
[InlineData("1,2", 3)]
[InlineData("1,2,3,4,5", 15)]
[InlineData("1\n2,3", 6)]
public void Add_ShouldReturnSum_ForCommaOrNewlineSeparated(string input, int expected)
{
Assert.Equal(expected, StringCalculator.Add(input));
}
[Fact]
public void Add_ShouldSupportCustomDelimiter()
{
Assert.Equal(3, StringCalculator.Add("//;\n1;2"));
}
[Fact]
public void Add_ShouldThrow_ForNegativeNumbers()
{
var ex = Assert.Throws<ArgumentException>(() => StringCalculator.Add("1,-2,3,-4"));
Assert.Contains("-2", ex.Message);
Assert.Contains("-4", ex.Message);
}
[Fact]
public void Add_ShouldIgnoreNumbersOver1000()
{
Assert.Equal(2, StringCalculator.Add("2,1001"));
}
}
// Implementation
public static class StringCalculator
{
public static int Add(string input)
{
if (string.IsNullOrEmpty(input)) return 0;
string[] delimiters = { ",", "\n" };
if (input.StartsWith("//"))
{
var delimiterEnd = input.IndexOf('\n');
var customDelimiter = input[2..delimiterEnd];
delimiters = new[] { customDelimiter };
input = input[(delimiterEnd + 1)..];
}
var numbers = input.Split(delimiters, StringSplitOptions.None)
.Select(int.Parse)
.ToList();
var negatives = numbers.Where(n => n < 0).ToList();
if (negatives.Any())
{
throw new ArgumentException(
$"Negatives not allowed: {string.Join(", ", negatives)}");
}
return numbers.Where(n => n <= 1000).Sum();
}
}
Test Output:
Passed StringCalculatorTests.Add_ShouldReturnZero_ForEmptyString
Passed StringCalculatorTests.Add_ShouldReturnSum_ForCommaOrNewlineSeparated
Passed StringCalculatorTests.Add_ShouldSupportCustomDelimiter
Passed StringCalculatorTests.Add_ShouldThrow_ForNegativeNumbers
Passed StringCalculatorTests.Add_ShouldIgnoreNumbersOver1000
Testing is a fundamental skill for professional C# developers. By mastering xUnit, TDD, and testing patterns, you build reliable software that can evolve with confidence. The .NET ecosystem provides excellent tooling for test-driven development.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro