# Testing in .NET with xUnit

Course: [Testing & Verification](https://www.devtutorials.ai/learn/testing-verification) · Section: Testing in .NET
Source: https://www.devtutorials.ai/learn/testing-verification/dotnet/testing-in-dotnet-xunit

> 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.

| Idea | TypeScript (this course) | .NET |
|---|---|---|
| Test runner | Vitest | xUnit (`dotnet test`) |
| One test | `it('...', () => ...)` | `[Fact]` method |
| A table of cases | a loop, or `it.each` | `[Theory]` with `[InlineData]` |
| Check a value | `expect(x).toBe(y)` | `Assert.Equal(y, x)` |
| Check an error | `expect(() => f()).toThrow()` | `Assert.Throws<T>(() => f())` |
| Mocks | `vi.fn()` | Moq or NSubstitute |
| Mutation testing | Stryker | Stryker.NET |
| Properties | fast-check | FsCheck |
| Integration | real http + server | WebApplicationFactory |

**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

```csharp runnable title="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.

```csharp runnable title="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.

```csharp runnable title="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

```csharp runnable title="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.

```csharp runnable title="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

```csharp runnable title="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.

```csharp runnable title="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

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Swapped arguments | `Assert.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

- [Unit testing C# with xUnit, Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-csharp-with-xunit): set up a real test project.
- [xUnit.net documentation](https://xunit.net/): facts, theories and asserts.
- The **C# Fundamentals** course on this site covers the language itself.

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