.NET Unit Test - Mock Concrete Classes using Fody

2023/11/07 - Coenraad Stijne

Summary:

When creating unit tests, object being tested either requires virtual members, or an interface.

Through the use of the Fody package, it is possible to manipulate the intermediate language (IL) during build, and make all concrete class members virtual, so that they can be mocked without extra developer effort.

⚠️ This will not work for sealed types. There is a Unsealed.Fody project, but it is unmaintained.

Code Examples


Package Overview

Package Description
Fody Extensible tool for weaving .net assemblies
Virtuosity Change all members to virtual.
EmptyConstructor Adds an empty constructor to classes even if you have a non-empty one defined.

Fody Configuration

To enable Fody, add the following to the Test .csproj.
We configure it to only run in Debug configurations, so that Release builds remain unaffected.

<PropertyGroup>
  
  ...

  <WeaverConfiguration Condition="'$(Configuration)' == 'Debug'">
    <Weavers>
      <Virtuosity />
      <EmptyConstructor />
    </Weavers>
  </WeaverConfiguration>
</PropertyGroup>

Example of a concrete class we want to test:

public class Class1
{
    private readonly Random _random;

    public Class1(Random random)
    {
        _random = random;
    }

    public int GenerateRandomNumber() => _random.Next();
}

Manipulated class that is included in the build:

public class Class1
{
    private readonly Random _random;

    public Class1(Random random)
    {
        _random = random;
    }

    public Class1()
    {
        _random = random;
    }

    public virtual int GenerateRandomNumber() => _random.Next();
}

MSTest + Moq example:

[TestClass]
public class TestMethods
{
    [TestMethod]
    public void ConcreteClassMockTest()
    {
        var mock = new Mock<Class1>();

        mock.Setup(e => e.GenerateRandomNumber()).Returns(1);

        var result = mock.Object.GenerateRandomNumber();

        Assert.AreEqual(1, result);
    }
}