← Back to list

Leveraging Moq in .NET 9 for Robust Unit Testing with NUnit

Testing modern .NET applications often requires isolating dependencies, simulating external services, and verifying interactions. Moq…

Ed Curtin · 2026-02-27 16:39 · 0 claps · 3.0 min read
#unit-testing #nunit #dotnet #moq
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval

Leveraging Moq in .NET 9 for Robust Unit Testing with NUnit

Testing modern .NET applications often requires isolating dependencies, simulating external services, and verifying interactions. Moq, combined with NUnit, is a powerful toolset that allows developers to achieve this with minimal boilerplate. In this article, we explore a real-world scenario of notifying external systems when a domain entity changes.

The Problem: Tracking Changes in Domain Entities

Consider a domain entity, Person, which supports snapshots to detect changes:

public class Person : Entity<Guid>, ISnapshotable<Person>
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public DateTime DateOfBirth { get; private set; }
    public bool IsActive { get; private set; }

    public Person(Guid id, string firstName, string lastName, DateTime dateOfBirth)
        : base(id)
    {
        SetName(firstName, lastName);
        SetDateOfBirth(dateOfBirth);
        IsActive = true;
    }

    public void SetName(string firstName, string lastName)
    {
        if (string.IsNullOrWhiteSpace(firstName)) throw new ArgumentException("First name cannot be empty.");
        if (string.IsNullOrWhiteSpace(lastName)) throw new ArgumentException("Last name cannot be empty.");
        FirstName = firstName;
        LastName = lastName;
    }

    public Person CreateSnapshot() => new Person(Id, FirstName, LastName, DateOfBirth) { IsActive = this.IsActive };
    public bool HasChanges(Person snapshot) =>
        FirstName != snapshot.FirstName || LastName != snapshot.LastName || DateOfBirth != snapshot.DateOfBirth || IsActive != snapshot.IsActive;
}

Here, Person supports snapshotting and change detection via ISnapshotable<T>. This pattern is common in domain-driven design (DDD), where entities manage their own state and behaviors.

The Service: Entity Update Notifications

We need a service that:

  1. Captures a snapshot of the entity.
  2. Detects changes.
  3. Notifies an external system if the entity has changed.
public class EntityUpdateNotificationService<T> where T : Entity<Guid>, ISnapshotable<T>
{
    private readonly IMessageService _messageService;
    private T? _snapshot;

    public EntityUpdateNotificationService(IMessageService messageService)
    {
        _messageService = messageService ?? throw new ArgumentNullException(nameof(messageService));
    }

    public void CaptureSnapshot(T entity) => _snapshot = entity.CreateSnapshot();

    public async Task NotifyIfChangedAsync(T entity, CancellationToken cancellationToken = default)
    {
        if (_snapshot != null && entity.HasChanges(_snapshot))
        {
            await _messageService.FireAndForgetAsync(new MessageEnvelope { Data = entity }, cancellationToken);
            _snapshot = entity.CreateSnapshot();
        }
    }
}

The service relies on an IMessageService interface to send notifications. In unit testing, we want to verify this behavior without actually sending messages.

Why Moq?

Moq is a mocking framework for .NET that allows developers to:

  1. Simulate external dependencies: Replace real services (like databases, APIs, message brokers) with mocks.
  2. Verify interactions: Ensure methods are called with expected parameters, exact number of times, and even with specific data.
  3. Simplify async testing: Moq supports asynchronous methods naturally, which is critical in modern .NET applications.
  4. Maintain readability: Minimal setup and expressive syntax make tests easy to read and maintain.

Instead of building a custom test double or manually tracking calls, Moq does it elegantly in just a few lines.

Testing with NUnit and Moq

[TestFixture]
public class EntityUpdateNotificationServiceTests
{
    private Mock<IMessageService> _messageServiceMock = null!;
    private EntityUpdateNotificationService<Person> _service = null!;

    [SetUp]
    public void Setup()
    {
        _messageServiceMock = new Mock<IMessageService>();
        _messageServiceMock
            .Setup(x => x.FireAndForgetAsync(It.IsAny<MessageEnvelope>(), It.IsAny<CancellationToken>()))
            .Returns(Task.CompletedTask);

        _service = new EntityUpdateNotificationService<Person>(_messageServiceMock.Object);
    }

    private Person CreatePerson(string firstName = "John") =>
        new Person(Guid.NewGuid(), firstName, "Doe", new DateTime(1990, 1, 1));

    [Test]
    public async Task NotifyIfChangedAsync_Should_Do_Nothing_When_Snapshot_Not_Captured()
    {
        var person = CreatePerson();
        await _service.NotifyIfChangedAsync(person);
        _messageServiceMock.Verify(x => x.FireAndForgetAsync(It.IsAny<MessageEnvelope>(), It.IsAny<CancellationToken>()), Times.Never);
    }

    [Test]
    public async Task NotifyIfChangedAsync_Should_Send_When_Data_Changed()
    {
        var person = CreatePerson();
        _service.CaptureSnapshot(person);
        person.SetName("Updated", person.LastName);

        await _service.NotifyIfChangedAsync(person);

        // Verify that the mock received exactly one call with the correct entity
        _messageServiceMock.Verify(
            x => x.FireAndForgetAsync(
                It.Is<MessageEnvelope>(m => m.Data == person),
                It.IsAny<CancellationToken>()),
            Times.Once);
    }
}

Moq Highlights in These Tests:

  • **Mock<IMessageService>**: No real messaging system is needed.
  • **It.IsAny<T>()**: Flexible argument matching.
  • **It.Is<T>(predicate)**: Verifies that the right data was sent.
  • **Times.Once / Times.Never**: Verifies call frequency.
  • Async-friendly: Supports Task return types without additional plumbing.

This makes the tests robust, fast, and deterministic.

Best Practices with Moq

  1. Mock behavior, not implementation: Focus on what the service should do, not how it does it.
  2. Avoid over-mocking: Only mock external dependencies, not the class under test.
  3. Use Verify to ensure interactions: Ensures correct collaboration between components.
  4. Use Setup for predictable results: Helps simulate edge cases and errors.
  5. Test cancellation tokens: Moq allows passing and verifying cancellation tokens for async operations.

Conclusion

Using Moq with NUnit in .NET 9:

  • Ensures isolated, reliable unit tests.
  • Simulates external dependencies without side effects.
  • Provides clear verification of interactions.
  • Simplifies async testing in modern applications.

This approach is essential in domain-driven designs, where entities manage their own state and services act on them. By combining snapshotting, change detection, and Moq-based testing, developers can build robust, maintainable, and testable applications.


메타데이터
post_id
e1b037c5bcfd
slug
leveraging-moq-in-net-9-for-robust-unit-testing-with-nunit-e1b037c5bcfd
url
https://medium.com/@EdwardCurtin/leveraging-moq-in-net-9-for-robust-unit-testing-with-nunit-e1b037c5bcfd
canonical_url
https://medium.com/@EdwardCurtin/leveraging-moq-in-net-9-for-robust-unit-testing-with-nunit-e1b037c5bcfd
author_url
https://medium.com/@EdwardCurtin
status
ok
fetched_at
2026-07-13 06:23:13