Unit Testing ASP.NET Core Web API with Moq and xUnit (Controllers + Services)

Published: (December 7, 2025 at 12:40 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

What Is Moq?

Moq allows you to replace real dependencies with lightweight test doubles so you can test logic in isolation.

Core methods

  • Setup() → define mocked behavior
  • ReturnsAsync() → return values for async methods
  • ThrowsAsync() → simulate failures
  • Verify() → assert that a dependency was called

Everything in this guide follows the pattern Mock → Execute → Validate.

Why Test Web API Controllers?

Controllers handle HTTP requests and return responses. Testing ensures:

  • ✅ Correct HTTP status codes (200, 404, 400, 201)
  • ✅ Services are called with the right parameters
  • ✅ Validation works properly
  • ✅ Errors are handled gracefully

Rule: Controllers should be thin – they orchestrate, not implement business logic.

Setup (2 minutes)

dotnet new xunit -n YourApi.Tests
cd YourApi.Tests
dotnet add package Moq --version 4.20.72
dotnet add package FluentAssertions --version 6.12.2
dotnet add reference ../YourApi/YourApi.csproj

The Pattern: Mock → Execute → Validate

Example Controller (ProductsController)

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger _logger;

    public ProductsController(IProductService productService, ILogger logger)
    {
        _productService = productService;
        _logger = logger;
    }

    [HttpGet("{id}")]
    public async Task> GetById(int id)
    {
        var product = await _productService.GetProductAsync(id);

        if (product == null)
            return NotFound(new { message = $"Product with ID {id} not found" });

        var response = new ProductResponse
        {
            Id = product.Id,
            Name = product.Name,
            SKU = product.SKU,
            Description = product.Description,
            Price = product.Price,
            IsActive = product.IsActive,
            Category = product.Category != null
                ? new CategoryResponse { Id = product.Category.Id, Name = product.Category.Name }
                : new CategoryResponse()
        };

        return Ok(response);
    }

    [HttpPost]
    public async Task> Create([FromBody] CreateProductRequest request)
    {
        try
        {
            var product = await _productService.CreateProductAsync(request);

            var response = new ProductResponse
            {
                Id = product.Id,
                Name = product.Name,
                SKU = product.SKU,
                Price = product.Price,
                IsActive = product.IsActive
            };

            return CreatedAtAction(nameof(GetById), new { id = product.Id }, response);
        }
        catch (InvalidOperationException ex)
        {
            return Conflict(new { message = ex.Message });
        }
    }
}

Testing Controllers

Controllers are tested by mocking the service, calling the action, and asserting on the ActionResult (status code + response body).

Test Class Setup

using CommonComps.Models;
using CommonComps.Models.Requests;
using CommonComps.Models.Responses;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using ProductWebAPI.Controllers;
using ProductWebAPI.Services;
using Xunit;

public class ProductsControllerTests
{
    private readonly Mock _mockService;
    private readonly Mock> _mockLogger;
    private readonly ProductsController _controller;

    public ProductsControllerTests()
    {
        _mockService = new Mock();
        _mockLogger = new Mock>();
        _controller = new ProductsController(_mockService.Object, _mockLogger.Object);
    }
}

GET Endpoint Tests

[Fact]
public async Task GetById_ReturnsOk_WhenProductExists()
{
    // Arrange
    var product = new Product
    {
        Id = 1,
        Name = "Laptop",
        SKU = "LAP-001",
        Price = 999.99m,
        IsActive = true,
        Category = new Category { Id = 1, Name = "Electronics" }
    };

    _mockService.Setup(s => s.GetProductAsync(1))
        .ReturnsAsync(product);

    // Act
    var result = await _controller.GetById(1);

    // Assert
    var okResult = result.Result.Should().BeOfType().Subject;
    var response = okResult.Value.Should().BeOfType().Subject;
    response.Id.Should().Be(1);
    response.Name.Should().Be("Laptop");
    _mockService.Verify(s => s.GetProductAsync(1), Times.Once);
}

[Fact]
public async Task GetById_ReturnsNotFound_WhenProductDoesNotExist()
{
    // Arrange
    _mockService.Setup(s => s.GetProductAsync(999))
        .ReturnsAsync((Product?)null);

    // Act
    var result = await _controller.GetById(999);

    // Assert
    result.Result.Should().BeOfType();
}

POST Endpoint Tests

[Fact]
public async Task Create_ReturnsCreated_WhenSuccessful()
{
    // Arrange
    var request = new CreateProductRequest
    {
        Name = "New Product",
        SKU = "NEW-001",
        Price = 49.99m,
        CategoryId = 1
    };

    var createdProduct = new Product
    {
        Id = 42,
        Name = "New Product",
        SKU = "NEW-001",
        Price = 49.99m,
        IsActive = true
    };

    _mockService.Setup(s => s.CreateProductAsync(It.IsAny()))
        .ReturnsAsync(createdProduct);

    // Act
    var result = await _controller.Create(request);

    // Assert
    var createdResult = result.Result.Should().BeOfType().Subject;
    createdResult.StatusCode.Should().Be(201);
    createdResult.ActionName.Should().Be(nameof(ProductsController.GetById));

    var response = createdResult.Value.Should().BeOfType().Subject;
    response.Id.Should().Be(42);
    _mockService.Verify(s => s.CreateProductAsync(It.IsAny()), Times.Once);
}

[Fact]
public async Task Create_ReturnsConflict_WhenDuplicateSKU()
{
    // Arrange
    var request = new CreateProductRequest { Name = "Test", SKU = "DUP-001" };

    _mockService.Setup(s => s.CreateProductAsync(It.IsAny()))
        .ThrowsAsync(new InvalidOperationException("Product with SKU DUP-001 already exists"));

    // Act
    var result = await _controller.Create(request);

    // Assert
    result.Result.Should().BeOfType();
}

Demo GIF

Key Moq Techniques

Setup Return Values

// Return specific value
_mockService.Setup(s => s.GetProductAsync(1)).ReturnsAsync(product);

// Return null
_mockService.Setup(s => s.GetProductAsync(999)).ReturnsAsync((Product?)null);

// Return for any argument
_mockService.Setup(s => s.GetProductAsync(It.IsAny())).ReturnsAsync(product);
Back to Blog

Related posts

Read more »

Checkbox Aria TagHelper

Introduction Learn how to initialize all checkbox inputs that reside in an ASP.NET Core page using a custom TagHelper. The TagHelper evaluates the checked attr...