Unit Testing in C#
Unit Testing in C#
Section titled “Unit Testing in C#”The .NET ecosystem has three mature test frameworks. xUnit is the most popular for new projects.
Frameworks Comparison
Section titled “Frameworks Comparison”| Framework | NuGet Package | Attribute |
|---|---|---|
| xUnit | xunit | [Fact], [Theory] |
| NUnit | NUnit | [Test], [TestCase] |
| MSTest | MSTest.TestFramework | [TestMethod], [DataRow] |
Setting Up xUnit
Section titled “Setting Up xUnit”dotnet new classlib -n MyAppdotnet new xunit -n MyApp.Testscd MyApp.Testsdotnet add reference ../MyApp/MyApp.csprojdotnet add package Moqdotnet add package FluentAssertionsWriting Tests with xUnit
Section titled “Writing Tests with xUnit”using Xunit;
public class CalculatorTests{ private readonly Calculator _sut; // system under test
public CalculatorTests() { _sut = new Calculator(); }
[Fact] public void Add_TwoPositiveNumbers_ReturnsCorrectSum() { // Arrange var a = 5; var b = 3;
// Act var result = _sut.Add(a, b);
// Assert Assert.Equal(8, result); }
[Theory] [InlineData(0, 5, 5)] [InlineData(-1, 1, 0)] [InlineData(100, -50, 50)] public void Add_VariousInputs_ReturnsExpectedResult(int a, int b, int expected) { var result = _sut.Add(a, b); Assert.Equal(expected, result); }}FluentAssertions
Section titled “FluentAssertions”More readable assertions:
using FluentAssertions;
result.Should().Be(8);result.Should().BeGreaterThan(0);items.Should().HaveCount(3);items.Should().Contain("apple");items.Should().BeInAscendingOrder();action.Should().Throw<ArgumentException>() .WithMessage("*cannot be null*");Mocking with Moq
Section titled “Mocking with Moq”Mock dependencies so unit tests don’t touch databases or external APIs:
using Moq;
public class OrderServiceTests{ [Fact] public async Task PlaceOrder_WhenProductExists_ReturnsOrderId() { // Arrange var productRepo = new Mock<IProductRepository>(); var orderRepo = new Mock<IOrderRepository>();
productRepo .Setup(r => r.GetByIdAsync(42)) .ReturnsAsync(new Product { Id = 42, Name = "Widget", Price = 9.99m });
orderRepo .Setup(r => r.SaveAsync(It.IsAny<Order>())) .ReturnsAsync(Guid.NewGuid());
var service = new OrderService(productRepo.Object, orderRepo.Object);
// Act var orderId = await service.PlaceOrderAsync(userId: 1, productId: 42, quantity: 2);
// Assert orderId.Should().NotBeEmpty(); orderRepo.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Once); }
[Fact] public async Task PlaceOrder_WhenProductNotFound_ThrowsNotFoundException() { var productRepo = new Mock<IProductRepository>(); productRepo .Setup(r => r.GetByIdAsync(It.IsAny<int>())) .ReturnsAsync((Product?)null);
var service = new OrderService(productRepo.Object, new Mock<IOrderRepository>().Object);
var act = () => service.PlaceOrderAsync(1, 999, 1);
await act.Should().ThrowAsync<NotFoundException>() .WithMessage("*Product 999 not found*"); }}Testing Exceptions
Section titled “Testing Exceptions”// xUnitAssert.Throws<ArgumentNullException>(() => new UserService(null));
// FluentAssertionsAction act = () => new UserService(null);act.Should().Throw<ArgumentNullException>() .WithParameterName("repository");
// Async exceptionsawait act.Should().ThrowAsync<HttpRequestException>();Setup and Teardown
Section titled “Setup and Teardown”public class DatabaseTests : IAsyncLifetime{ private TestDatabase _db;
public async Task InitializeAsync() { _db = await TestDatabase.CreateAsync(); }
public async Task DisposeAsync() { await _db.DisposeAsync(); }
[Fact] public async Task CanInsertUser() { await _db.Users.AddAsync(new User { Name = "Alice" }); var count = await _db.Users.CountAsync(); count.Should().Be(1); }}Running Tests
Section titled “Running Tests”dotnet testdotnet test --filter "FullyQualifiedName~OrderService" # filter by namedotnet test --logger "console;verbosity=detailed"dotnet test /p:CollectCoverage=true # with coverage