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 is based on 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 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<ProductsController> _logger;
public ProductsController(IProductService productService, ILogger<ProductsController> logger)
{
_productService = productService;
_logger = logger;
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductResponse>> 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<ActionResult<ProductResponse>> 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<T> (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<IProductService> _mockService;
private readonly Mock<ILogger<ProductsController>> _mockLogger;
private readonly ProductsController _controller;
public ProductsControllerTests()
{
_mockService = new Mock<IProductService>();
_mockLogger = new Mock<ILogger<ProductsController>>();
_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<OkObjectResult>().Subject;
var response = okResult.Value.Should().BeOfType<ProductResponse>().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<NotFoundObjectResult>();
}
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<CreateProductRequest>()))
.ReturnsAsync(createdProduct);
// Act
var result = await _controller.Create(request);
// Assert
var createdResult = result.Result.Should().BeOfType<CreatedAtActionResult>().Subject;
createdResult.StatusCode.Should().Be(201);
createdResult.ActionName.Should().Be(nameof(ProductsController.GetById));
var response = createdResult.Value.Should().BeOfType<ProductResponse>().Subject;
response.Id.Should().Be(42);
_mockService.Verify(s => s.CreateProductAsync(It.IsAny<CreateProductRequest>()), 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<CreateProductRequest>()))
.ThrowsAsync(new InvalidOperationException("Product with SKU DUP-001 already exists"));
// Act
var result = await _controller.Create(request);
// Assert
result.Result.Should().BeOfType<ConflictObjectResult>();
}
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.CreateProductAsync(It.IsAny<CreateProductRequest>())).ReturnsAsync(product);
// Throw exception
_mockService.Setup(s => s.CreateProductAsync(It.IsAny<CreateProductRequest>()))
.ThrowsAsync(new InvalidOperationException("Product already exists"));
Verify Method Calls
// Verify called once
_mockService.Verify(s => s.GetProductAsync(1), Times.Once);
// Verify never called
_mockService.Verify(s => s.DeleteProductAsync(It.IsAny<int>()), Times.Never);
// Verify with specific argument check
_mockService.Verify(s => s.CreateProductAsync(It.Is<CreateProductRequest>(r =>
r.SKU == "LAP-001")), Times.Once);
Testing Services (Business Logic Layer)
Services contain business logic and depend on repositories. Mock the repository to test service logic in isolation.
Example Service (ProductService)
public class ProductService : IProductService
{
private readonly IProductRepository _productRepository;
private readonly ILogger<ProductService> _logger;
public ProductService(IProductRepository productRepository, ILogger<ProductService> logger)
{
_productRepository = productRepository;
_logger = logger;
}
public async Task<Product?> GetProductAsync(int id)
{
return await _productRepository.GetByIdAsync(id);
}
public async Task<Product> CreateProductAsync(CreateProductRequest request)
{
// Business logic: Check for duplicate SKU
if (await _productRepository.ExistsAsync(request.SKU))
throw new InvalidOperationException($"Product with SKU {request.SKU} already exists");
var product = new Product
{
Name = request.Name,
SKU = request.SKU,
Description = request.Description,
Price = request.Price,
CategoryId = request.CategoryId,
IsActive = true
};
product.Id = await _productRepository.CreateAsync(product);
return product;
}
}
Unit Testing Services with Mocked Repository
using CommonComps.Models;
using CommonComps.Models.Requests;
using CommonComps.Repositories;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using ProductWebAPI.Services;
using Xunit;
public class ProductServiceTests
{
private readonly Mock<IProductRepository> _productRepositoryMock;
private readonly Mock<ILogger<ProductService>> _loggerMock;
private readonly ProductService _productService;
public ProductServiceTests()
{
_productRepositoryMock = new Mock<IProductRepository>();
_loggerMock = new Mock<ILogger<ProductService>>();
_productService = new ProductService(_productRepositoryMock.Object, _loggerMock.Object);
}
[Fact]
public async Task GetProductAsync_WithValidId_ReturnsProduct()
{
// Arrange
var expectedProduct = new Product
{
Id = 1,
Name = "Test Product",
SKU = "TEST-001",
Price = 99.99m,
IsActive = true
};
_productRepositoryMock.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(expectedProduct);
// Act
var result = await _productService.GetProductAsync(1);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(1);
result.Name.Should().Be("Test Product");
_productRepositoryMock.Verify(r => r.GetByIdAsync(1), Times.Once);
}
[Fact]
public async Task CreateProductAsync_WithDuplicateSKU_ThrowsException()
{
// Arrange
var request = new CreateProductRequest
{
Name = "Duplicate Product",
SKU = "EXISTING-001",
Price = 29.99m,
CategoryId = 1
};
_productRepositoryMock.Setup(r => r.ExistsAsync("EXISTING-001"))
.ReturnsAsync(true);
// Act & Assert
var action = () => _productService.CreateProductAsync(request);
await action.Should().ThrowAsync<InvalidOperationException>()
.WithMessage("*EXISTING-001*already exists*");
}
}
Best Practices
- Arrange-Act-Assert (AAA) - Clear test structure
-
Test naming:
MethodName_Scenario_ExpectedResult - Mock only dependencies - Not the thing you're testing
- Verify important interactions - Did service get called?
- One test = one scenario - Keep tests focused
- Repository tests - Use integration tests for critical queries
Quick Reference
// 1. Create mocks
var mockService = new Mock<IProductService>();
var mockLogger = new Mock<ILogger<ProductsController>>();
// 2. Setup behavior
mockService.Setup(s => s.GetProductAsync(1)).ReturnsAsync(product);
// 3. Inject into controller
var controller = new ProductsController(mockService.Object, mockLogger.Object);
// 4. Execute
var result = await controller.GetById(1);
// 5. Validate (note: use .Result for ActionResult<T>)
result.Result.Should().BeOfType<OkObjectResult>();
mockService.Verify(s => s.GetProductAsync(1), Times.Once);
Running Tests
# Run all tests
dotnet test
# Run with details
dotnet test --logger "console;verbosity=detailed"
# Run specific test
dotnet test --filter "FullyQualifiedName~GetById_ReturnsOk"
Summary
Testing controllers:
- Mock service
- Call controller
- Verify HTTP result + service calls
Testing services:
- Mock repository
- Execute service logic
- Validate business rules
Next: Learn integration testing with Testcontainers in Quick Guide #2: Repository + Database Testing!


Top comments (0)