Unit Testing with xUnit & Moq

লেসন ৪১ / ৫০

xUnit Setup

dotnet new xunit -o MyApp.Tests
dotnet add MyApp.Tests reference MyApp.Application
dotnet add package Moq
dotnet add package FluentAssertions

Basic Test (AAA Pattern)

public class CalculatorTests {
    [Fact]
    public void Add_TwoNumbers_ReturnsSum() {
        // Arrange
        var calc = new Calculator();

        // Act
        var result = calc.Add(2, 3);

        // Assert
        Assert.Equal(5, result);
    }

    [Theory]
    [InlineData(1, 2, 3)]
    [InlineData(0, 0, 0)]
    [InlineData(-1, 1, 0)]
    public void Add_VariousInputs_ReturnsExpected(int a, int b, int expected) {
        var calc = new Calculator();
        var result = calc.Add(a, b);
        Assert.Equal(expected, result);
    }
}

Moq — Mocking Dependencies ⭐

public class ProductServiceTests {
    [Fact]
    public async Task GetById_WhenExists_ReturnsProduct() {
        // Arrange
        var mockRepo = new Mock<IProductRepository>();
        var product = new Product { Id = 1, Name = "Laptop", Price = 50000 };
        mockRepo.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(product);

        var service = new ProductService(mockRepo.Object);

        // Act
        var result = await service.GetByIdAsync(1);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Laptop", result.Name);
        mockRepo.Verify(r => r.GetByIdAsync(1), Times.Once);
    }

    [Fact]
    public async Task Create_ValidProduct_CallsRepo() {
        var mockRepo = new Mock<IProductRepository>();
        var service = new ProductService(mockRepo.Object);

        await service.CreateAsync(new ProductDto("Phone", 30000));

        mockRepo.Verify(r => r.AddAsync(It.IsAny<Product>()), Times.Once);
        mockRepo.Verify(r => r.SaveChangesAsync(), Times.Once);
    }
}

FluentAssertions — Readable Asserts

using FluentAssertions;

result.Should().NotBeNull();
result.Name.Should().Be("Laptop");
result.Price.Should().BeGreaterThan(0);
list.Should().HaveCount(3);
list.Should().Contain(x => x.Id == 1);
action.Should().Throw<ArgumentException>().WithMessage("*invalid*");

EF Core — In-Memory Database

dotnet add package Microsoft.EntityFrameworkCore.InMemory

[Fact]
public async Task GetAll_ReturnsAllProducts() {
    var opt = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("TestDb_" + Guid.NewGuid())
        .Options;

    using var ctx = new AppDbContext(opt);
    ctx.Products.AddRange(
        new Product { Name = "P1", Price = 100 },
        new Product { Name = "P2", Price = 200 });
    await ctx.SaveChangesAsync();

    var repo = new ProductRepository(ctx);
    var all = await repo.GetAllAsync();

    all.Should().HaveCount(2);
}

Test Lifecycle

public class MyTests : IClassFixture<DbFixture>, IDisposable {
    private readonly DbFixture _fixture;
    public MyTests(DbFixture f) => _fixture = f;   // shared
    public void Dispose() { /* per-test cleanup */ }
}

Run Tests

dotnet test
dotnet test --filter "Category=Unit"
dotnet test --collect:"XPlat Code Coverage"