Site

Testing — C# Unit Testing with xUnit

Tutorial 10.0  •  C# / Learn

10.0 What This Teaches

This tutorial covers unit testing C# code with xUnit:

10.1 Project Setup

dotnet new xunit -o MyLib.Tests
cd MyLib.Tests
dotnet add reference ../MyLib/MyLib.csproj   # reference the code under test
dotnet test                                  # build and run all tests
xUnit is the most popular .NET test framework. The project template installs the xunit and xunit.runner.visualstudio packages automatically. Tests run inside a separate process from the code under test.

10.2 [Fact] Tests

Any public method decorated with [Fact] is a test. It must be in a public class. xUnit's Assert class provides typed assertions:
using Xunit;

public class MathTests
{
    [Fact]
    public void Add_TwoPositives_ReturnsSum()
    {
        int result = Calculator.Add(3, 4);
        Assert.Equal(7, result);
    }

    [Fact]
    public void Add_NegativeNumbers_ReturnsSum()
    {
        Assert.Equal(-2, Calculator.Add(-1, -1));
    }
}
The naming convention Method_Scenario_ExpectedBehavior makes test failures self-documenting in the test runner output.

10.3 Common Assert Methods

Assert.Equal(expected, actual);          // value equality
Assert.NotEqual(unexpected, actual);
Assert.True(condition);
Assert.False(condition);
Assert.Null(obj);
Assert.NotNull(obj);
Assert.Same(expected, actual);           // reference equality
Assert.IsType<int>(obj);
Assert.Contains(item, collection);
Assert.Empty(collection);
Assert.InRange(value, low, high);
xUnit's assertion failure messages include the expected and actual values automatically. You can pass a custom message as the last parameter.

10.4 [Theory] and [InlineData]

A [Theory] is a parameterized test. Each [InlineData] attribute provides one set of arguments - the test runs once per attribute:
[Theory]
[InlineData(5,  0, 10, 5)]   // within range
[InlineData(-3, 0, 10, 0)]   // below min
[InlineData(15, 0, 10, 10)]  // above max
[InlineData(0,  0, 10, 0)]   // at min boundary
public void Clamp_ReturnsExpected(int value, int lo, int hi, int expected)
{
    Assert.Equal(expected, Calculator.Clamp(value, lo, hi));
}
Use [MemberData] or [ClassData] when test data is too large to fit inline or must be computed dynamically.

10.5 Testing Exceptions

[Fact]
public void Divide_ByZero_ThrowsArgumentException()
{
    var ex = Assert.Throws<ArgumentException>(() => Calculator.Divide(10, 0));
    Assert.Contains("zero", ex.Message);
}

[Fact]
public void Parse_InvalidInput_ThrowsFormatException()
{
    Assert.Throws<FormatException>(() => int.Parse("abc"));
}
Assert.Throws<T> returns the exception so you can inspect its message, inner exception, or other properties.

10.6 Shared Setup

xUnit creates a new test class instance for each test, so use the constructor for per-test setup. Implement IDisposable for teardown:
public class DatabaseTests : IDisposable
{
    private readonly FakeDatabase _db;

    public DatabaseTests()
    {
        _db = new FakeDatabase();
        _db.Seed();
    }

    [Fact]
    public void GetById_ExistingId_ReturnsRecord()
    {
        var record = _db.GetById(1);
        Assert.NotNull(record);
    }

    public void Dispose() => _db.Close();
}

10.7 Example - All Together

// Testing - xUnit test suite for a StringUtils class.

using Xunit;

public class StringUtilsTests
{
    [Theory]
    [InlineData("racecar", true)]
    [InlineData("hello",   false)]
    [InlineData("",        true)]
    [InlineData("a",       true)]
    public void IsPalindrome_ReturnsExpected(string input, bool expected)
    {
        Assert.Equal(expected, StringUtils.IsPalindrome(input));
    }

    [Theory]
    [InlineData("hello world", "Hello World")]
    [InlineData("",            "")]
    [InlineData("a b c",       "A B C")]
    public void TitleCase_CapitalizesEachWord(string input, string expected)
    {
        Assert.Equal(expected, StringUtils.TitleCase(input));
    }

    [Fact]
    public void WordCount_NullInput_ThrowsArgumentNullException()
    {
        Assert.Throws<ArgumentNullException>(() => StringUtils.WordCount(null!));
    }
}

10.8 Exercise

Exercise
  • Create an xunit test project and write [Fact] tests for a Temperature class that converts between Celsius and Fahrenheit.
  • Add [Theory] / [InlineData] tests for edge cases: absolute zero (-273.15°C), water freezing (0°C), boiling (100°C).
  • Test that constructing a Temperature below absolute zero throws ArgumentOutOfRangeException.

10.9 Common Mistakes

Asserting with == instead of Assert.Equal

// Wrong - silently passes even when result is wrong
bool ok = result == expected;

// Correct - xUnit reports expected vs actual on failure
Assert.Equal(expected, result);

Testing implementation details instead of behavior

Tests that assert internal field values break whenever you refactor internals, even when the external behavior is unchanged. Test the public API and observable outcomes, not how they are achieved.

Comparing floats with Assert.Equal

Assert.Equal(3.14, Math.PI);   // fails: not exactly equal
Assert.Equal(3.14, Math.PI, precision: 2);  // passes: compare to 2 decimal places

10.10 Key Terms

TermMeaning
[Fact]Attribute marking a parameterless test method
[Theory]Attribute marking a parameterized test; requires data attributes
[InlineData]Provides one set of arguments for a [Theory]
Assert.EqualFails the test if expected and actual are not equal
Assert.Throws<T>Asserts that an action throws exception T; returns the exception
xUnitOpen-source .NET unit testing framework; used by most .NET OSS projects
dotnet testCLI command that builds and runs all test projects in a solution
IDisposableInterface used for per-test teardown in xUnit test classes