.NET Academy

Unit testing with xUnit

Write fast, focused automated tests with xUnit — Arrange, Act, Assert.

13 min read

You will learn

  • Explain what a unit test is and why it pays off
  • Write tests with [Fact] and [Theory] using Arrange-Act-Assert
  • Choose clear test names and assertions
  • Run tests with dotnet test

Why automated tests?

Every change risks breaking something that used to work. Clicking through the app after each change is slow and you'll skip it eventually. Automated tests check behavior in seconds, every time — on your machine and in CI on every push. They also document how code is supposed to behave, and they make refactoring safe.

A unit test checks a small piece of logic in isolation: one method or class, with no real database, network or file system. Unit tests run in milliseconds, so you can have thousands.

Setting up xUnit

xUnit is the most popular .NET test framework (NUnit and MSTest are alternatives with similar ideas):

dotnet new xunit -o Shop.Tests
dotnet add Shop.Tests reference Shop.Core      # the project being tested
dotnet test                                    # build and run all tests

Your first tests

public class PasswordValidatorTests
{
    [Fact]
    public void Validate_ValidPassword_ReturnsNoErrors()
    {
        // Arrange
        var password = "Password1";
 
        // Act
        var errors = PasswordValidator.Validate(password);
 
        // Assert
        Assert.Empty(errors);
    }
 
    [Fact]
    public void Validate_ShortPassword_ReportsTooShort()
    {
        var errors = PasswordValidator.Validate("Ab1");
 
        Assert.Contains("Too short", errors);
    }
 
    [Theory]
    [InlineData("password1", "Needs an uppercase letter")]
    [InlineData("Passwords", "Needs a digit")]
    public void Validate_MissingRule_ReportsIt(string password, string expectedError)
    {
        var errors = PasswordValidator.Validate(password);
 
        Assert.Equal([expectedError], errors);
    }
}
  • [Fact] — a single test case.
  • [Theory] + [InlineData] — the same test with several inputs.
  • Arrange, Act, Assert — set up, do one thing, check the outcome.
  • Names describe what, when and expected result: Method_Scenario_ExpectedBehavior.

These are exactly the checks this lesson's exercise runs. Make them pass!

Assertions

AssertionChecks
Assert.Equal(expected, actual)Values are equal (works with collections too)
Assert.True(cond) / Assert.False(cond)A condition
Assert.Null(x) / Assert.NotNull(x)Nullness
Assert.Contains(item, collection) / Assert.Empty(c)Collections
Assert.Throws<ArgumentException>(() => ...)An exception is thrown
await Assert.ThrowsAsync<T>(async () => ...)Same, for async code

Put expected first, actual second — failure messages read "Expected: X, Actual: Y". Libraries like FluentAssertions or Shouldly offer a more readable style: errors.Should().BeEmpty();.

What makes a good unit test

  • Fast — milliseconds. No sleeps, no real I/O.
  • Isolated — tests don't depend on each other or on execution order.
  • Repeatable — same result every run. Inject the clock (TimeProvider) and seed randomness; flaky tests are worse than none.
  • One reason to fail — test one behavior; many small tests beat one giant test.
  • Test behavior, not implementation — assert on results, not on private details, so refactoring doesn't break tests.

What to test

Focus on code with logic: calculations, validation, business rules, edge cases (empty lists, nulls, boundaries like exactly 8 characters). Don't bother unit-testing trivial getters or framework code — and test infrastructure (databases, HTTP) with integration tests instead (later in this track).

Tests in this course

Every exercise you've solved was checked by hidden tests — the same idea as xUnit, running in your browser. Writing the tests yourself is the next step.

Key takeaways

  • Unit tests check small pieces of logic in isolation, quickly and automatically.
  • xUnit: [Fact] for one case, [Theory] + [InlineData] for many; run with dotnet test.
  • Structure tests as Arrange-Act-Assert with descriptive names.
  • Keep tests fast, isolated and deterministic; test behavior, not implementation details.

Exercise

A teammate wrote the xUnit tests shown in the lesson for a password validator; your job is to make them pass. Validate returns a list of error messages, in this order: "Too short" (fewer than 8 characters), "Needs a digit", "Needs an uppercase letter". A valid password returns an empty list.

Program.cs
Loading editor…

C# runtime not loaded

Hint 1

Check each rule with an if and errors.Add(...) — in the order listed.

Hint 2

password.Any(char.IsDigit) and password.Any(char.IsUpper) test the character rules.

Show solution
foreach (var error in PasswordValidator.Validate("abc"))
    Console.WriteLine(error);

static class PasswordValidator
{
    public static List<string> Validate(string password)
    {
        var errors = new List<string>();
        if (password.Length < 8) errors.Add("Too short");
        if (!password.Any(char.IsDigit)) errors.Add("Needs a digit");
        if (!password.Any(char.IsUpper)) errors.Add("Needs an uppercase letter");
        return errors;
    }
}

Quiz

4 questions · 3 correct to pass
  1. 1.What makes a test a "unit" test?

  2. 2.What's the difference between [Fact] and [Theory] in xUnit?

  3. 3.What are the three parts of the Arrange-Act-Assert pattern?

  4. 4.A test passes on your machine and fails randomly in CI. What's the most likely cause?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)