C# Testing Tools — xUnit, FluentAssertions, Moq and Bogus

The C# testing ecosystem has consolidated around xUnit as the test runner, FluentAssertions for readable assertions, Moq for mocking, and Bogus for test data. Each tool has a specific role: xUnit runs and discovers tests, FluentAssertions makes failures readable, Moq controls dependencies, and Bogus generates realistic data that avoids “magic strings” that make tests brittle and unreadable.

Setting Up the Test Project

// ── Create test project ────────────────────────────────────────────────────
// dotnet new xunit -n BlogApp.Tests
// dotnet add BlogApp.Tests reference BlogApp.Api
// dotnet add BlogApp.Tests package FluentAssertions
// dotnet add BlogApp.Tests package Moq
// dotnet add BlogApp.Tests package Bogus
// dotnet add BlogApp.Tests package Microsoft.AspNetCore.Mvc.Testing

// ── xUnit test lifecycle ───────────────────────────────────────────────────
public class PostsServiceTests : IDisposable
{
    // Constructor runs before each test (replaces [SetUp] in NUnit)
    private readonly Mock<IPostsRepository> _repo    = new();
    private readonly Mock<ICurrentUserService> _user = new();
    private readonly PostsService _sut;   // System Under Test

    public PostsServiceTests()
    {
        _sut = new PostsService(_repo.Object, _user.Object);
    }

    // IDisposable.Dispose runs after each test (replaces [TearDown])
    public void Dispose() { /* cleanup if needed */ }

    // ── [Fact] — single test case ─────────────────────────────────────────
    [Fact]
    public async Task CreateAsync_WithValidRequest_ReturnsPostDto()
    {
        // Arrange — set up test data and mocks
        var request = new CreatePostRequest("Getting Started", "getting-started",
                                            "Body content here.", "draft");
        _repo.Setup(r => r.SlugExistsAsync("getting-started", default))
             .ReturnsAsync(false);
        _repo.Setup(r => r.CreateAsync(It.IsAny<Post>(), default))
             .ReturnsAsync(new Post { Id = 1, Slug = "getting-started" });
        _user.Setup(u => u.UserId).Returns("user-123");

        // Act
        var result = await _sut.CreateAsync(request, default);

        // Assert — FluentAssertions
        result.Should().NotBeNull();
        result.Slug.Should().Be("getting-started");
        result.Id.Should().BePositive();
    }

    // ── [Theory] with [InlineData] — parameterised tests ─────────────────
    [Theory]
    [InlineData("",         false)]  // empty slug — invalid
    [InlineData("valid-slug", true)] // valid slug — ok
    [InlineData("has spaces", false)]  // spaces — invalid
    [InlineData("UPPERCASE", false)]   // uppercase — invalid
    public void SlugIsValid_ReturnsExpectedResult(string slug, bool expected)
    {
        var result = SlugValidator.IsValid(slug);
        result.Should().Be(expected);
    }
}

// ── Bogus — generating realistic test data ────────────────────────────────
public static class TestDataBuilder
{
    private static readonly Faker<Post> PostFaker = new Faker<Post>()
        .RuleFor(p => p.Id,          f => f.IndexFaker + 1)
        .RuleFor(p => p.Title,       f => f.Lorem.Sentence(5))
        .RuleFor(p => p.Slug,        (f, p) => p.Title.ToLowerInvariant()
                                                       .Replace(" ", "-"))
        .RuleFor(p => p.Body,        f => f.Lorem.Paragraphs(3))
        .RuleFor(p => p.Status,      f => "draft")
        .RuleFor(p => p.ViewCount,   f => f.Random.Int(0, 10000))
        .RuleFor(p => p.PublishedAt, f => f.Date.Past());

    public static Post  Post()      => PostFaker.Generate();
    public static Post[]  Posts(int n) => PostFaker.Generate(n).ToArray();
}
Note: xUnit creates a new instance of the test class for every test method — this enforces test isolation. Unlike NUnit’s [SetUp] attribute (which runs before each test on the same instance), xUnit’s constructor runs fresh. This means shared state between tests is impossible by accident — each test starts with a clean mock setup. Use IClassFixture<T> for expensive shared resources (like a test database) that should be created once per test class, not per test method.
Tip: Name tests using the convention MethodName_Scenario_ExpectedBehaviour: CreateAsync_WithDuplicateSlug_ThrowsConflictException. This makes the test report self-documenting — a failing test immediately tells you what failed, under what condition, and what was expected. Avoid test names like Test1, CreateTest, or WhenPostCreated — they require reading the test body to understand the intent.
Warning: Bogus uses a seeded random number generator by default — the same seed produces the same data across runs, making tests deterministic. If you need truly random data per run (to catch edge cases), use new Faker().Random.Int() directly. However, for unit tests, deterministic data is usually preferable — a failing test should be reproducible. Reserve true randomness for property-based testing (FsCheck, AutoFixture) where the framework tracks seeds for you.

Common Mistakes

Mistake 1 — Hardcoded magic strings in tests (brittle, unreadable)

❌ Wrong — var post = new Post { Title = "test", Slug = "test-slug", Body = "test body" }; unclear intent; breaks when validation changes minimum length.

✅ Correct — var post = TestDataBuilder.Post(); realistic data; validation passes naturally.

Mistake 2 — Asserting too many things in one test (hard to diagnose failures)

❌ Wrong — one [Fact] that asserts 10 different properties; one failure masks the others.

✅ Correct — one assertion per [Fact] for the primary behaviour; use [Theory] for multiple input variations of the same assertion.

🧠 Test Yourself

A xUnit test class has 5 test methods. Each test sets _count = 0 at the start and increments it. After running all 5 tests, what is _count?