Unit Testing with xUnit
xUnit is the dominant .NET test framework — [Fact] for single tests, [Theory] with [InlineData] for parameterized cases.
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.
flowchart LRTest[xUnit Test] --> Arrange[Arrange]Arrange --> Act[Act]Act --> Assert[Assert]Assert --> Report[Test Runner Report]
Step-by-step explanation
- dotnet new xunit creates test project.
- ProjectReference to system under test.
- dotnet test runs all; CI gate on pass.
- AAA pattern structures readability.
- Deterministic tests — no DateTime.Now without inject IClock.
- Parallel test execution default in xUnit.
Practical code example
xUnit tests with Moq for async service and FluentAssertions:
namespace TechLearningPro.Tests;public sealed class WithdrawalServiceTests{[Fact]public async Task Withdraw_SufficientFunds_ReturnsTrue(){// Arrangevar 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);// Actvar result = await svc.TryWithdrawAsync(Guid.NewGuid(), 100m, CancellationToken.None);// Assertresult.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?
Q2BeginnerMock vs fake?
Q3IntermediateTest async method?
Q4IntermediateIntegration vs unit test?
Q5AdvancedTest minimal API POST returns 201.
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.