100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Unit Testing Controllers and Services

Learn how to isolate ASP.NET Core controllers and services with xUnit and Moq to verify business logic without running the full HTTP pipeline.

Testing & DeploymentIntermediate9 min readJul 10, 2026
Analogies

Why Unit Test Controllers and Services

Unit testing a controller means testing its action methods in isolation from the ASP.NET Core hosting pipeline, routing, filters, and model binding. You instantiate the controller directly in a test project, inject mocked versions of its dependencies (such as IProductService), and call an action method like a plain C# method. This is fast — no Kestrel server, no HTTP round-trip — and lets you pinpoint exactly which line of business logic broke when a test fails, rather than debugging through middleware.

🏏

Cricket analogy: Testing a controller in isolation is like a batsman practicing straight drives in the nets against a bowling machine rather than in a full T20 match — you isolate one skill (like Virat Kohli drilling cover drives) without worrying about fielders, crowd noise, or match pressure.

Mocking Dependencies with Moq

Moq lets you create a mock of an interface like IOrderService and configure its behavior with mock.Setup(s => s.GetOrderAsync(id)).ReturnsAsync(order) so that when the controller under test calls that method, it receives a predictable fake result instead of hitting a real database. You then inject mock.Object into the controller's constructor. After the action runs, mock.Verify(s => s.GetOrderAsync(id), Times.Once) confirms the controller actually called the dependency the expected number of times, which catches bugs where logic is accidentally skipped or duplicated.

🏏

Cricket analogy: Setting up a mock's Returns value is like a coach at nets programming a bowling machine to always deliver a yorker at leg stump, so the batsman (the controller) faces a predictable, repeatable scenario every time.

csharp
public class ProductsControllerTests
{
    [Fact]
    public async Task GetById_ReturnsOkWithProduct_WhenProductExists()
    {
        // Arrange
        var mockService = new Mock<IProductService>();
        var product = new ProductDto { Id = 1, Name = "Widget" };
        mockService.Setup(s => s.GetByIdAsync(1)).ReturnsAsync(product);
        var controller = new ProductsController(mockService.Object);

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

        // Assert
        var okResult = Assert.IsType<OkObjectResult>(result.Result);
        var returned = Assert.IsType<ProductDto>(okResult.Value);
        Assert.Equal("Widget", returned.Name);
        mockService.Verify(s => s.GetByIdAsync(1), Times.Once);
    }
}

Asserting Action Results and Status Codes

Controller action methods often return IActionResult or ActionResult<T>, and a good unit test asserts both the concrete result type and its payload — for example Assert.IsType<OkObjectResult>(result) followed by casting Value to the expected DTO and checking its properties. When testing validation failures, you can manually add an error to ModelState before calling the action and assert that a BadRequestObjectResult is returned, verifying the controller respects [ApiController]'s automatic model validation behavior or your own explicit check.

🏏

Cricket analogy: Asserting the correct ActionResult type is like a third umpire in the DRS system checking not just whether the ball hit the stumps but confirming exactly how — bowled, LBW, or run out — before confirming the decision.

The [ApiController] attribute automatically returns a 400 Bad Request with validation error details when ModelState is invalid, before your action method body even runs — so you don't need to manually check ModelState.IsValid in most cases, but your unit tests should still verify this behavior.

Testing Services and Business Logic

Service classes that contain business logic — calculating a discount, validating a shipping address, orchestrating a multi-step order process — should be tested directly, independent of any controller. Following the Arrange-Act-Assert pattern, you construct the service with mocked repositories or HTTP clients, call the method under test, and assert both the return value and any side effects, such as verifying a repository's SaveAsync was called exactly once. Testing services this way keeps controllers thin and pushes the bulk of your test coverage onto the layer where the actual logic lives.

🏏

Cricket analogy: Testing the service layer directly is like evaluating a bowler's raw pace and swing in isolated fitness testing at the National Cricket Academy, separate from how a captain might deploy them tactically in a match.

Avoid mocking types you don't own deeply, like EF Core's DbContext — mocking DbSet<T> and LINQ queries is brittle and often doesn't reflect real query behavior. Prefer wrapping data access behind a repository interface, or use an in-memory/SQLite provider in integration tests instead.

  • Mock dependencies with Moq to isolate controller logic from the HTTP pipeline
  • Use Assert.IsType<T> to verify both the ActionResult type and its payload
  • Verify mock calls with Times.Once to catch skipped or duplicated logic
  • Test ModelState validation failures by manually adding errors before invoking the action
  • Test service classes directly and independently from controllers
  • Follow Arrange-Act-Assert for consistent, readable tests
  • Avoid deeply mocking EF Core's DbContext; use a repository abstraction instead

Practice what you learned

Was this page helpful?

Topics covered

#ASPNETCoreStudyNotes#MicrosoftTechnologies#UnitTestingControllersAndServices#Unit#Testing#Controllers#Services#StudyNotes#SkillVeris#ExamPrep