Testing in .NET with xUnit

By Syncfusion®, Inc. · Last updated

The course ideas in .NET: xUnit facts and theories, Assert.Equal and Assert.Throws, decimal for money, fakes behind interfaces, and the AI mistakes to look for in a C# diff.

The Same Ideas, in .NET

Everything you learned in this course works in C# and ASP.NET Core too. Only the tools change.

  • Claims, diffs and evidence are the same in every language.
  • The grading rule is the same: tests must pass on correct code and fail on wrong code.
  • The tools have .NET names: xUnit, Moq, Stryker.NET, FsCheck and WebApplicationFactory.

This lesson shows the .NET testing basics. The next three lessons repeat the main course ideas in .NET.

Simple lesson: new tools, same question: would this test fail if the code were wrong?

A Real-World Example

Think about learning to drive in a new city.

  • The rules are the same: stop at red, look before you turn.
  • The signs look a little different, and the roads have new names.
  • You do not learn driving again. You learn the new signs.

.NET testing is the same skill with new signs.

IdeaTypeScript (this course).NET
Test runnerVitestxUnit (dotnet test)
One testit('...', () => ...)[Fact] method
A table of casesa loop, or it.each[Theory] with [InlineData]
Check a valueexpect(x).toBe(y)Assert.Equal(y, x)
Check an errorexpect(() => f()).toThrow()Assert.Throws<T>(() => f())
Mocksvi.fn()Moq or NSubstitute
Mutation testingStrykerStryker.NET
Propertiesfast-checkFsCheck
Integrationreal http + serverWebApplicationFactory

In a real project, tests live in a separate test project, made with dotnet new xunit, and you run them with dotnet test. In this lab, each box runs in your browser on the site's .NET runner, with the real xUnit library: [Fact], [Theory], [InlineData] and Assert are the real ones. A file with test classes and no other code runs like dotnet test. The only difference: the code under test sits in the same file.

What You Will Learn

In this lesson, you will learn:

  1. How an xUnit test looks: [Fact], Assert.Equal and Assert.Throws.
  2. How [Theory] turns many cases into one test.
  3. Why money in C# should use decimal, not double.
  4. How to use a fake instead of a mock.
  5. What AI mistakes look like in a C# diff.

Your First xUnit Test

Your First Test

Your first xUnit test

using Xunit;

public class DiscountTests
{
    [Fact]
    public void TenPercentOffFiveHundred()
    {
        // Arrange, Act, Assert
        decimal result = Discount.Apply(500m, 10);
        Assert.Equal(450m, result);
    }

    [Fact]
    public void KeepsPaise() => Assert.Equal(169.99m, Discount.Apply(199.99m, 15));
}

// The code under test. The AI wrote it.
public static class Discount
{
    public static decimal Apply(decimal price, int percent) => price - percent;
}
  • [Fact] marks a method as one test.
  • Assert.Equal(expected, actual) puts the expected value first. That is the opposite order from Vitest.
  • 500m is a decimal number. The m stands for money.

Press Run.

  • The output: two ✗ lines, each with xUnit's message: "Expected: 450, Actual: 490". The last line says Failed! - Failed: 2, Passed: 0, like dotnet test.
  • Why: the AI's code takes ₹10 off, not 10%.
  • Now try: change Apply to Math.Round(price * (100 - percent) / 100, 2, MidpointRounding.AwayFromZero) and run again.

Simple lesson: [Fact], expected first, then the actual value.

Money Needs decimal

double stores numbers in binary. Many money amounts, like 0.1, cannot be stored exactly.

double vs decimal

using System;

double a = 0.1 + 0.2;
decimal b = 0.1m + 0.2m;

Console.WriteLine($"double:  0.1 + 0.2 = {a:R}");
Console.WriteLine($"decimal: 0.1 + 0.2 = {b}");
Console.WriteLine($"double equals 0.3?  {a == 0.3}");
Console.WriteLine($"decimal equals 0.3? {b == 0.3m}");

Press Run.

  • double: 0.30000000000000004. Not equal to 0.3.
  • decimal: exactly 0.3.

Review rule for AI code in .NET: prices, totals and taxes should be decimal. If an AI change uses double or float for money, flag it.

Simple lesson: use decimal for money, and round with MidpointRounding.AwayFromZero when the rule says "round half up".

[Theory]: Many Cases, One Test

[Theory] = one test method that runs once for each row of data.

A [Theory] with four rows

using Xunit;

public class ShippingTests
{
    [Theory]
    [InlineData(498, 1, 40)]
    [InlineData(499, 1, 0)]
    [InlineData(100, 5, 40)]
    [InlineData(100, 6, 60)]
    public void Fee(int total, int kg, int expected) => Assert.Equal((decimal)expected, Shipping.Fee(total, kg));
}

// The AI's code: free from ₹499, otherwise ₹40, plus ₹20 over 5 kg.
public static class Shipping
{
    public static decimal Fee(decimal total, decimal kg)
    {
        if (total > 499m) return 0m;
        return kg >= 5m ? 60m : 40m;
    }
}
  • Each [InlineData] row is one case. The rows sit on both sides of each limit.
  • [InlineData] takes simple constants, so the rows use whole numbers. For values like ₹199.99, use [MemberData].

Press Run.

  • The output: one line per row. 2 rows fail: Fee(total: 499, kg: 1, expected: 0) and the 5 kg row.
  • Why: > should be >= for the price, and >= should be > for the weight. Two off-by-one mistakes.

Simple lesson: a theory makes it cheap to test every edge.

Errors: Assert.Throws

Checking an error

using Xunit;

public class CartTests
{
    [Fact]
    public void NegativeQuantityThrows() =>
        Assert.Throws<ArgumentException>(() => Cart.Total(new[] { (10m, -1) }));

    [Fact]
    public void ThreePensCostThirty() => Assert.Equal(30m, Cart.Total(new[] { (10m, 3) }));
}

public static class Cart
{
    public static decimal Total((decimal Price, int Qty)[] items)
    {
        decimal total = 0m;
        foreach (var (price, qty) in items)
            total += price * qty;
        return total;
    }
}

Press Run.

  • The output: the first test fails. xUnit says no exception was thrown.
  • Now try: add if (qty < 0) throw new ArgumentException("Quantity cannot be negative"); inside the loop.

Assert.Throws<T> is strict about the type. If the code throws a different exception, the test fails. That catches an AI change that throws Exception or InvalidOperationException where the rule says ArgumentException.

Simple lesson: test the unhappy path, with the exact exception type.

Fakes and Mocks in .NET

You met mocks in Auditing AI-Written Tests. In .NET, code usually takes its parts through an interface, so a test can pass in a fake.

  • Moq and NSubstitute are popular mock libraries.
  • A hand-written fake class is often simpler and clearer.

A fake clock

using Xunit;

public class SaleTests
{
    [Fact]
    public void SaleIsOnBeforeMidnight() =>
        Assert.True(Sale.IsOn(new FakeClock(new DateTime(2026, 11, 13, 23, 59, 0))));

    [Fact]
    public void SaleIsOverAfterMidnight() =>
        Assert.False(Sale.IsOn(new FakeClock(new DateTime(2026, 11, 14, 0, 1, 0))));
}

public interface IClock { DateTime Now { get; } }

// The fake: always returns the time the test chose.
public class FakeClock : IClock
{
    public FakeClock(DateTime now) => Now = now;
    public DateTime Now { get; }
}

public static class Sale
{
    public static bool IsOn(IClock clock) => clock.Now < new DateTime(2026, 11, 14);
}

Press Run. Both pass.

  • Assert.False is the real xUnit opposite of Assert.True.
  • The clock is an edge: it changes on its own. Faking it makes the test repeatable.
  • The sale rule is your own code. It runs for real.

Simple lesson: fake the edges (clock, network, email). Let your own code run.

AI Mistakes in a C# Diff

The same review checklist works. Some mistakes look a little different in C#.

Made-Up Methods

A method that does not exist

using System;

string name = "asha";
Console.WriteLine(name.Capitalize());

Press Run.

  • The output: a compile error. string has no Capitalize method.
  • In a real project, the build fails before any test runs. That is rung 2 of the evidence ladder.
  • Fix it: char.ToUpper(name[0]) + name[1..].

Skipped Tests

In xUnit, a test is skipped with Skip =. Run this and read the summary line.

A skipped test

using Xunit;

public class CouponTests
{
    [Fact]
    public void TenPercentOffOneThousand() => Assert.Equal(900m, Coupon.Apply(1000m));

    // The agent's last edit: this test was red, so it added Skip.
    [Fact(Skip = "flaky, fix later")]
    public void NeverMoreThanTwoHundredOff() => Assert.Equal(4800m, Coupon.Apply(5000m));
}

public static class Coupon
{
    public static decimal Apply(decimal total) => total - Math.Round(total * 10 / 100, 2);
}
  • The summary: Passed! - Failed: 0, Passed: 1, Skipped: 1. Green.
  • Now try: remove (Skip = "flaky, fix later"). Run again. Red: 4500, not 4800.
  • Treat Skip = in an AI diff exactly like .skip in TypeScript: a red flag.

Other C# warning signs in an AI diff:

  • double or float for money.
  • #pragma warning disable or ! (the null-forgiving operator) added to hide warnings.
  • catch (Exception) { } that swallows every error.
  • A new NuGet package you have never heard of.

Simple lesson: the build and the review checklist catch most C# AI mistakes before the tests do.

TL;DR

  1. xUnit = the most common .NET test framework. Run it with dotnet test.
  2. [Fact] = one test. [Theory] + [InlineData] = one test, many rows.
  3. Assert.Equal(expected, actual) = expected comes first.
  4. Assert.Throws<T> = checks the exact exception type.
  5. decimal = the type for money. double cannot store 0.1 exactly.
  6. Fakes = simple classes behind an interface, for the edges.
  7. C# red flags: Skip =, made-up methods, double for money, swallowed exceptions.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Swapped argumentsAssert.Equal(actual, expected)Expected first: Assert.Equal(450m, result).Failure messages say the opposite of the truth.
double for money"It is just a number."Use decimal.Totals are off by tiny amounts, and tests fail randomly.
One [Fact] per case"Copy the test ten times."Use a [Theory] with rows.Edges get left out because each one costs a new method.
Assert.Throws<Exception>"Any error is fine."Name the exact type.The wrong exception type passes the test.
Mocking your own code"Mock everything with Moq."Fake the edges only.The real logic never runs in the test.

Check Your Understanding

Exercises

What to Remember

  • Same ideas, new tools: xUnit, Moq, Stryker.NET, FsCheck, WebApplicationFactory.
  • [Fact] for one case, [Theory] for a table of cases.
  • Expected first in Assert.Equal. Exact types in Assert.Throws.
  • Money is decimal.
  • Fake the edges, run your own code.

Further Reading

Next: Spec First and Mutation Testing in .NET. You will make C# tests fail first, then measure them with mutants.