Skip to content

Testing in C# — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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

  1. Testing implementation details: Test behavior, not internal state. Refactoring the implementation should not break tests if behavior is unchanged.

  2. Not using parameterized tests: Copying test methods for different inputs creates maintenance overhead. Use [Theory] or [TestCase].

  3. Flaky tests: Tests that depend on timing, random data, or external resources are unreliable. Use mocks and deterministic data.

  4. Ignoring edge cases: Test empty collections, null inputs, boundary values, and error conditions.

  5. Skipping assertions: A test without assertions is worthless. Always verify expected outcomes.

Practice Questions

  1. Write a test suite for a Stack<T> implementation that covers push, pop, peek, and empty stack exceptions.

  2. Create parameterized tests for a DateParser that validates multiple date formats.

  3. Implement a test fixture that sets up an in-memory database for Integration Testing.

  4. Challenge: Build a property-based test using FsCheck that verifies the commutative property of addition across random inputs.

FAQ

Which testing framework should I use?

xUnit is the modern standard for .NET projects. It is actively maintained, extensible, and used by Microsoft internally.

What is the difference between Fact and Theory?

Fact is a plain test with no parameters. Theory is a parameterized test that receives data from InlineData, MemberData, or ClassData.

How do I run tests from the command line?

Use dotnet test. Add --filter for specific tests, --verbosity for detailed output.

Can I run tests in parallel?

Yes, test frameworks support parallel execution by default. xUnit runs tests within a class sequentially but classes in parallel.

How do I measure code coverage?

Use coverlet with dotnet test --collect:'XPlat Code Coverage'. Visual Studio and JetBrains Rider also have built-in coverage tools.

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