Strengthening Moq based unit tests with PosInformatique.Moq.Analyzers

Strengthening Moq based unit tests with PosInformatique.Moq.Analyzers

As a passionate advocate for robust unit testing and code quality, I've invested heavily in leveraging the Moq library within my .NET projects. To ensure reliability and catch potential errors early, I developed a .NET analyzer tool specifically tailored for Moq based unit tests. This analyzer is called PosInformatique.Moq.Analyzers and available on nuget.org web site.

Origin and Purpose

This analyzer was born from the need to fortify unit tests by detecting discrepancies during the coding phase. I wanted to preemptively identify errors in my Moq based tests to ensure their coherence and correctness.

For that I used the .NET Compiler Platform SDK to create an analyzer packaged inside a NuGet package.

What's new in the version 1.3.0?

The recent release, version 1.3.0, brings a significant advancement: the PosInfoMoq2003 rule. This rule inspects method signature within Callback() to validate their alignment with Setup() method.

For exemple, in the following mocked method UpdateAsync(), the analyzer check that the Callback() method take a lambda expression with exactly the same signature.

var repository = new Mock<ITaskRepository>(MockBehavior.Strict);
repository.Setup(r => r.UpdateAsync(task, It.IsAny<TaskChangeTracking>()))
    .Callback((TaskUpdate _, TaskChangeTracking updated) =>
    {
        updated.DateTime.Should().Be(new DateTime(2023, 1, 2, 3, 4, 5, 6)).And.BeIn(DateTimeKind.Utc);
        updated.UserId.Should().Be("55555555-5555-5555-5555-555555555555");
    })
    .Returns(Task.CompletedTask);

This enhancement significantly boosts the analyzer's ability to ensure method coherence within Moq setups.

A quick recap of the previous versions

Let's recall the enhancements of the previous versions. They introduced rules like PosInfoMoq1000 and PosInfoMoq1001, emphasizing the importance of invoking Verify() or VerifyAll() methods and defining Mock<T> instances to Strict mode.

Also, other additional rules was added, to perform immediate compilation validation, aiming to catch errors before runtime execution, ensuring higher code quality and mitigating potential issues during the testing phase.

  • PosInfoMoq2000: Requires the use of Returns() or ReturnsAsync() methods for Strict mocks. This rule ensures that when defining a Mock<T> with Strict behavior, these methods are invoked when setting up a method to mock returning a value.

  • PosInfoMoq2001: Check the usage of the Setup() method to be use on overridable members (not extension method, not sealed).

  • PosInfoMoq2002: Specifies that the Mock<T> class can only be used to mock non-sealed classes (interfaces of abstract classes).

For instance, this is a simple unit test which matches four of the previous rules of the PosInformatique.Moq.Analyzers analyzer.

[Fact]
public async Task GetAsync()
{
    var visits = new[]
    {
        new Visit(default, default, default),
    };

    var manager = new Mock<IVisitManager>(MockBehavior.Strict);  // PosInfoMoq1001 : Check that mock are defined to Strict mode.
    manager.Setup(m => m.FindAsync(It.IsAny<VisitQuery>()))
        .Callback((VisitQuery q) =>  // PosInfoMoq2003: Check the signature to match with the Setup() mocked method.
        {
            q.Should().BeEquivalentTo(new VisitQuery()
            {
                Id = new Guid("11111111-1111-1111-1111-111111111111"),
            });
        })
        .ReturnsAsync(visits);  // PosInfoMoq2000: Returns() or ReturnsAsync() is required.

    var result = await manager.Object.GetAsync(new Guid("11111111-1111-1111-1111-111111111111"));

    result.Should().BeSameAs(visits[0]);

    manager.VerifyAll();  // PosInfoMoq1000 : Check that the Verify() / VerifyAll() method is called in the unit test.
}

For detailed installation instructions and a comprehensive list of rules, refer to the official documentation available on the github.io website.