C# Programming Tutorial 0/45 lessons ~6 min read Lesson 43

    Unit Testing with xUnit

    xUnit is the dominant .NET test framework — [Fact] for single tests, [Theory] with [InlineData] for parameterized cases.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    xUnit is the dominant .NET test framework — [Fact] for single tests, [Theory] with [InlineData] for parameterized cases. Combine with FluentAssertions, Moq/NSubstitute, and WebApplicationFactory for integration tests.

    Interviewers ask Arrange-Act-Assert, mocking vs fakes, testing async code, and test pyramid balance.

    The story

    Before deploying a withdrawal feature to production, the team runs automated tests that mock the account repository — verifying that valid withdrawals return true and invalid amounts throw ArgumentOutOfRangeException. CI blocks the merge if any test fails, catching regressions that manual QA might miss under deadline pressure.

    Understanding the topic

    Key concepts

    • Fact single test; Theory data-driven.
    • IClassFixture shared setup; ICollectionFixture collection.
    • Assert.ThrowsAsync for exception tests.
    • Mock IRepository with Moq ReturnsAsync.
    • WebApplicationFactory spins test host.
    • Testcontainers for real PostgreSQL in integration tests.
    text
    flowchart LR
    Test[xUnit Test] --> Arrange[Arrange]
    Arrange --> Act[Act]
    Act --> Assert[Assert]
    Assert --> Report[Test Runner Report]

    Step-by-step explanation

    1. dotnet new xunit creates test project.
    2. ProjectReference to system under test.
    3. dotnet test runs all; CI gate on pass.
    4. AAA pattern structures readability.
    5. Deterministic tests — no DateTime.Now without inject IClock.
    6. Parallel test execution default in xUnit.

    Practical code example

    xUnit tests with Moq for async service and FluentAssertions:

    csharp
    namespace TechLearningPro.Tests;
    public sealed class WithdrawalServiceTests
    {
    [Fact]
    public async Task Withdraw_SufficientFunds_ReturnsTrue()
    {
    // Arrange
    var repo = new Mock<IAccountRepository>();
    repo.Setup(r => r.GetBalanceAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
    .ReturnsAsync(500m);
    repo.Setup(r => r.DebitAsync(It.IsAny<Guid>(), 100m, It.IsAny<CancellationToken>()))
    .Returns(Task.CompletedTask);
    var svc = new WithdrawalService(repo.Object);
    // Act
    var result = await svc.TryWithdrawAsync(Guid.NewGuid(), 100m, CancellationToken.None);
    // Assert
    result.Should().BeTrue();
    repo.Verify(r => r.DebitAsync(It.IsAny<Guid>(), 100m, It.IsAny<CancellationToken>()), Times.Once);
    }
    [Theory]
    [InlineData(0)]
    [InlineData(-10)]
    public async Task Withdraw_InvalidAmount_Throws(decimal amount)
    {
    var svc = new WithdrawalService(Mock.Of<IAccountRepository>());
    var act = () => svc.TryWithdrawAsync(Guid.NewGuid(), amount, CancellationToken.None);
    await act.Should().ThrowAsync<ArgumentOutOfRangeException>();
    }
    }

    Line-by-line code explanation

    • [Fact] marks a single test method with no parameter variations.
    • public async Task Withdraw_SufficientFunds_ReturnsTrue() names the scenario using Method_Condition_Expected convention.
    • var repo = new Mock<IAccountRepository>() creates a Moq mock of the repository interface.
    • repo.Setup(r => r.GetBalanceAsync(...)).ReturnsAsync(500m) configures the mock to return a balance.
    • repo.Setup(r => r.DebitAsync(...)).Returns(Task.CompletedTask) stubs the debit call as successful.
    • var svc = new WithdrawalService(repo.Object) passes the mock into the system under test.
    • var result = await svc.TryWithdrawAsync(...) executes the method being tested.
    • result.Should().BeTrue() uses FluentAssertions for readable failure messages.
    • repo.Verify(..., Times.Once) asserts the debit was called exactly once — interaction testing.
    • [Theory] [InlineData(0)] [InlineData(-10)] runs the same test logic with multiple invalid inputs.

    Key takeaway: Mock verifies interaction. Theory covers multiple invalid inputs. FluentAssertions readable assertions. Test async with async Task test method.

    Real-world use

    Where you'll use this in production

    • CI pipeline blocking merge on test failure.
    • TDD for domain logic WithdrawalService.
    • API integration tests with WebApplicationFactory.
    • Regression suite before EF migration deploy.

    Best practices

    • One logical assert focus per test.
    • Name tests Method_Scenario_Expected.
    • Inject abstractions — mock interfaces.
    • Avoid testing framework or trivial getters.
    • Use Testcontainers for DB integration not mocks.

    Common mistakes

    • Testing private methods via reflection.
    • Shared mutable static state between tests.
    • async void test methods.
    • Over-mocking — test nothing real.

    Advanced interview questions

    Q1BeginnerFact vs Theory?
    Fact single; Theory repeated with different InlineData/MemberData.
    Q2BeginnerMock vs fake?
    Mock verifies interactions; fake has working simplified implementation.
    Q3IntermediateTest async method?
    public async Task Test() { await act; Assert... }
    Q4IntermediateIntegration vs unit test?
    Unit isolates class with mocks; integration hits real DB/API with test host.
    Q5AdvancedTest minimal API POST returns 201.
    WebApplicationFactory custom factory; HttpClient PostAsJsonAsync; Assert StatusCode Created; scope seed DB.

    Summary

    xUnit Fact/Theory structure automated tests. Moq and FluentAssertions accelerate productive testing. WebApplicationFactory enables API integration tests. Keep tests deterministic and independent. Next: performance best practices.

    Ready to mark this lesson complete?Track your journey across the entire course.