100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Unit Testing with xUnit

Learn how to write, organize, and run automated tests for C# code using xUnit, the most widely used testing framework in the modern .NET ecosystem.

.NET Ecosystem & TestingIntermediate10 min readJul 9, 2026
Analogies

Unit Testing with xUnit

Unit testing verifies that a small, isolated piece of code — typically a single method or class — behaves correctly in response to a range of inputs, including edge cases and error conditions. xUnit.net is the de facto standard testing framework for modern .NET, favored over MSTest and NUnit by the .NET team itself (ASP.NET Core and the runtime use it internally). It embraces conventions that reduce boilerplate: no [TestClass] attribute is needed, test classes are just plain classes, and a fresh instance of the test class is created for every single test method, which naturally isolates test state without requiring manual setup/teardown resets.

🏏

Cricket analogy: Testing a single batter's technique in the nets is like unit testing — isolated from the full match; and just as each net session starts with a fresh pitch, xUnit creates a fresh instance of the test class for every single test method, avoiding leftover state.

Facts, Theories, and Assertions

xUnit distinguishes between two kinds of tests. A [Fact] is a test that is always true for a fixed scenario — it takes no parameters. A [Theory] is a parameterized test that should hold true for a set of data supplied via [InlineData], [MemberData], or [ClassData] attributes, letting you exercise the same assertion logic against many inputs without duplicating code. Assertions live on the static Assert class — Assert.Equal, Assert.True, Assert.Throws<T>, Assert.Null, and so on — and a failed assertion throws an exception that the test runner catches and reports, immediately halting that test method.

🏏

Cricket analogy: A [Fact] is like checking 'does this bat meet the weight limit' — always true for one fixed bat; a [Theory] with [InlineData] is like checking that rule across a dozen different bats, and the moment one fails inspection, the umpire halts that specific check immediately.

csharp
public class DiscountCalculator
{
    public decimal ApplyDiscount(decimal price, decimal percent)
    {
        if (percent is < 0 or > 100)
            throw new ArgumentOutOfRangeException(nameof(percent));

        return price - (price * percent / 100m);
    }
}

public class DiscountCalculatorTests
{
    private readonly DiscountCalculator _sut = new();

    [Fact]
    public void ApplyDiscount_ZeroPercent_ReturnsOriginalPrice()
    {
        var result = _sut.ApplyDiscount(100m, 0m);

        Assert.Equal(100m, result);
    }

    [Theory]
    [InlineData(200, 10, 180)]
    [InlineData(50, 50, 25)]
    [InlineData(80, 25, 60)]
    public void ApplyDiscount_ValidPercent_ReturnsExpectedPrice(
        decimal price, decimal percent, decimal expected)
    {
        var result = _sut.ApplyDiscount(price, percent);

        Assert.Equal(expected, result);
    }

    [Fact]
    public void ApplyDiscount_NegativePercent_Throws()
    {
        Assert.Throws<ArgumentOutOfRangeException>(
            () => _sut.ApplyDiscount(100m, -5m));
    }
}

Sharing Setup with Fixtures

Because xUnit creates a new test class instance per test, a constructor is the natural place for per-test setup, and implementing IDisposable on the test class gives you per-test teardown. For expensive setup that should be shared *across* tests in a class — such as spinning up a database connection or an in-memory web host — use IClassFixture<T>, which xUnit instantiates once and injects into every test class constructor that requests it. For setup shared across multiple test classes, ICollectionFixture<T> combined with a [Collection] attribute achieves the same sharing at a broader scope, and also disables parallel execution between those classes so they don't race over the shared resource. A common mistake is asserting on floating-point equality with Assert.Equal(expected, actual) for double values, which can fail due to rounding error even when the values are 'logically' equal. Use the overload Assert.Equal(expected, actual, precision) or a tolerance-based comparison instead.

🏏

Cricket analogy: A fresh net session's setup (pitch marked, stumps placed) is per-test constructor work, and tearing down the nets after is like IDisposable; but sharing one expensive, pre-booked stadium across an entire tournament of matches (IClassFixture) avoids rebuilding it each time — and just as comparing exact run-rates with rounding errors misleads a table, comparing exact doubles instead of tolerant precision misleads a test.

xUnit deliberately runs test classes in parallel by default (though tests within one class run sequentially), which surfaces hidden shared-state bugs far faster than frameworks that default to serial execution. This is a philosophical difference from NUnit and MSTest, and it's why xUnit avoids [SetUp]/[TearDown]-style shared mutable fixtures in favor of constructor/dispose semantics scoped to one instance.

Mocking Dependencies

Real unit tests isolate the system under test from its collaborators. Libraries like Moq or NSubstitute let you create fake implementations of interfaces at test time, configure their return values, and verify that specific methods were called. Combined with dependency injection (constructor injection in particular), this makes classes trivially testable: pass a mock IEmailSender into a class that would otherwise send a real email, assert that Send was called with the expected arguments, and never touch a real mail server during the test run.

🏏

Cricket analogy: To test a captain's decision-making, you don't need a real opposing team — a mock 'bowler' that always delivers a scripted yorker lets you verify the captain calls the right field placement, without ever touching a real match.

  • xUnit test classes need no special attribute; a new instance is created per test method for automatic isolation.
  • [Fact] tests take no parameters; [Theory] tests are parameterized via [InlineData], [MemberData], or [ClassData].
  • Assert.Throws<T> verifies exception type and can inspect the thrown exception's properties.
  • IClassFixture<T> shares expensive setup across tests in one class; ICollectionFixture<T> shares it across classes.
  • xUnit parallelizes across test classes by default, which helps catch shared-state bugs early.
  • Combine xUnit with a mocking library (Moq, NSubstitute) and constructor injection to isolate the system under test from its dependencies.

Practice what you learned

Was this page helpful?

Topics covered

#CStudyNotes#Programming#UnitTestingWithXUnit#Unit#Testing#XUnit#Facts#StudyNotes#SkillVeris#ExamPrep