DEV Community

Cover image for EasyTdd 0.5.0: Streamlining Mocking with Incremental FluentMock
Kazys
Kazys

Posted on • Originally published at easytdd.dev

EasyTdd 0.5.0: Streamlining Mocking with Incremental FluentMock

A few days ago, I released EasyTdd version 0.5.0. In this post, I'll walk through the updates and new features introduced in this version. The highlight of the release is the introduction of the incremental code generator for FluentMocks—a powerful feature implemented with the IIncrementalGenerator technology. FluentMock wraps around the popular Moq.Mock class and automatically generates setup and verify methods for every property and method of the target class, making the testing process more intuitive and readable. This implementation leverages the fluent interface design pattern, enabling method chaining that closely resembles natural language, improving code readability and maintainability. In addition to FluentMock, this update includes several key improvements and bug fixes, such as template enhancements, better file naming, and refined attribute handling, further solidifying EasyTdd’s position as a must-have tool for .NET developers focused on test-driven development. Read on to explore what’s new and how these updates can elevate your TDD experience.

Incremental FluentMock

An Overview

Let’s start by creating a FluentMock. First, install or update to EasyTdd 0.5.0. If you need help, you can follow the installation guide here. After installing, the 'Generate Incremental FluentMock' option will appear in the Quick Action menu when you right-click on an interface or abstract class declaration.:

Generate Incremental FluentMock

This action, in the above case, will generate a class SomeInterfaceMock. The code looks as following:

[EasyTdd.Generators.FluentMockFor(typeof(ISomeInterface))]
public partial class SomeInterfaceMock
{
    public static SomeInterfaceMock Create()
    {
        return new SomeInterfaceMock();
    }
}
Enter fullscreen mode Exit fullscreen mode

When to Use FluentMock

FluentMock is designed to simplify the process of creating, setting up, and managing mocks in your testing suite. But when is it most beneficial to use FluentMock over traditional mocking frameworks? Here are a few scenarios where FluentMock shines:

  • Centralized Setup Across the Solution: FluentMock enables you to define mock setups in a single place, making it easier to maintain and reuse configurations across multiple tests. This consistency ensures that any change to a mock’s behavior is automatically reflected throughout your entire test suite.

  • Reduced Boilerplate Code: Unlike traditional mocks, FluentMock significantly reduces repetitive setup code. This allows you to focus more on the test logic rather than the configuration details, resulting in cleaner and more maintainable tests.

  • Improved Readability: By using a fluent interface design pattern, FluentMock provides a more intuitive and readable syntax, resembling natural language. This makes it easier for developers to understand test scenarios at a glance, especially in complex setups.

  • Less Code, Fewer Mistakes: With less code required to configure and verify mocks, there’s a lower chance of introducing errors. FluentMock minimizes the verbosity of traditional mock setups, reducing the cognitive load on developers.

  • Foundation for Future Features: FluentMock’s streamlined structure lays the groundwork for upcoming features in EasyTdd, such as more automated code generation for other TDD scenarios. This means adopting FluentMock today will ensure a smoother transition to new capabilities as they are released.

Overall, FluentMock offers an ideal solution for developers looking to implement test-driven development (TDD) more efficiently by eliminating unnecessary boilerplate and making tests easier to manage and scale.

It is still the same old Moq.Mock

The FluentMock class is derived from the existing Moq.Mock class, meaning all the standard Moq functionality is still accessible. FluentMock streamlines the most common scenarios, but whenever you need to use the full power of Moq, you can still fall back to the regular Setup, Returns, and Verify methods as needed. Here’s an example:

// create a fluentMock with a basic setup
var mock = SomeInterfaceMock
    .Create()
    .SetupResolve(() => expectedResult0);

// use Moq.Mock regular techniquest for further setup and configuration:
mock
    .Setup(x => x.Resolve(2, It.IsAny<string>(), It.IsAny<SomeOtherResultType>()))
    .Returns(() => expectedResult2)
    .Callback(
        (int id, string value, SomeOtherResultType result) =>
        {
            // callback functionality
        }
    );

// user Moq.Mock regular Verify
mock.Verify();
mock.VerifyNoOtherCalls();
Enter fullscreen mode Exit fullscreen mode

This flexibility ensures that you can leverage the power of FluentMock for simpler scenarios while retaining full control over more complex test configurations using traditional Moq methods.

FluentMock vs. Traditional Moq: Setup Comparison

FluentMock Setup: Compact and Readable

The FluentMock code is automatically generated in the background and is immediately ready to use:

public class UsageTest
{
    [Test]
    public void Test()
    {
        var expectedResult0 = new SomeResultType();
        var expectedResult1 = new SomeResultType();
        var expectedResult2 = new SomeResultType();

        var mock = SomeInterfaceMock
            .Create()
            .SetupResolve(
                () => expectedResult0
            )
            .SetupResolve(
                () => expectedResult1,
                isId: x => x == 1
            )
            .SetupResolve(
                () => expectedResult2,
                isId: x => x == 2
            );

        mock
            .Object
            .Resolve(
                1,
                "Jon",
                new SomeOtherResultType()
            )
            .Should()
            .Be(expectedResult1);

        mock
            .Object
            .Resolve(
                2,
                "Tom",
                new SomeOtherResultType()
            )
            .Should()
            .Be(expectedResult2);

        mock
            .Object
            .Resolve(
                8,
                "Jennifer",
                new SomeOtherResultType()
            )
            .Should()
            .Be(expectedResult0);
    }
}
Enter fullscreen mode Exit fullscreen mode

In the code example above the mock is set up to return expectedResult1 when provided id is 1, return expectedResult2 when provided id is 2, and expectedResult0 in all other cases.

Traditional Moq Setup: Verbose and Detailed

Let's take a look at how the setup would look with a pure Moq.Mock:

var expectedResult0 = new SomeResultType();
var expectedResult1 = new SomeResultType();
var expectedResult2 = new SomeResultType();

var mock = new Mock<ISomeInterface>(MockBehavior.Strict);

mock
    .Setup(
        x =>
            x.Resolve(
                It.IsAny<int>(),
                It.IsAny<string>(),
                It.IsAny<SomeOtherResultType>()
            )
    )
    .Returns(() => expectedResult0);


mock
    .Setup(
        x =>
            x.Resolve(
                1,
                It.IsAny<string>(),
                It.IsAny<SomeOtherResultType>()
            )
    )
    .Returns(() => expectedResult1);

mock
    .Setup(
        x =>
            x.Resolve(
                2,
                It.IsAny<string>(),
                It.IsAny<SomeOtherResultType>()
            )
    )
    .Returns(() => expectedResult2);
Enter fullscreen mode Exit fullscreen mode

I prefer short lines so to look fare I will compare setup in one-liners:

// FluentMock Setup Example:
var mock = SomeInterfaceMock
    .Create()
    // Sets up Resolve to return expectedResult0 for all inputs by default
    .SetupResolve(() => expectedResult0)
    // Sets up Resolve to return expectedResult1 when the 'id' parameter is 1
    .SetupResolve(() => expectedResult1, isId: x => x == 1)
    // Sets up Resolve to return expectedResult2 when the 'id' parameter is 2
    .SetupResolve(() => expectedResult2, isId: x => x == 2);

//vs

// Traditional Moq Setup Example:
var mock = new Mock<ISomeInterface>(MockBehavior.Strict);

mock
    // Sets up Resolve to return expectedResult0 for any combination of parameters
    .Setup(x => x.Resolve(It.IsAny<int>(), It.IsAny<string>(), It.IsAny<SomeOtherResultType>()))
    .Returns(() => expectedResult0);

mock
    // Sets up Resolve to return expectedResult1 when 'id' is 1 and the other parameters are any values
    .Setup(x => x.Resolve(1, It.IsAny<string>(), It.IsAny<SomeOtherResultType>()))
    .Returns(() => expectedResult1);

mock
    // Sets up Resolve to return expectedResult2 when 'id' is 2 and the other parameters are any values
    .Setup(x => x.Resolve(2, It.IsAny<string>(), It.IsAny<SomeOtherResultType>()))
    .Returns(() => expectedResult2);
Enter fullscreen mode Exit fullscreen mode

FluentMock pros: Easy to read, compact, and utilizes method chaining, which closely resembles natural language.

Verification Example: FluentMock vs. Traditional Moq

The same applies to verification. The example below checks that the mock was called with an id value of 1, a name value of Jon, and a SomeOtherResult with an id value of 100:

mock
    .VerifyResolve(
        x => x == 1,
        x => x == "Jon",
        x => x.Id == 100
    );

//vs

mock
    .Verify(
        x => x.Resolve(
            1,
            "Jon",
            It.Is<SomeOtherResultType>(
                x => x.Id == 100
            )
        )
    );
Enter fullscreen mode Exit fullscreen mode

Reusable FluentMock Setups

The FluentMock approach also offers a convenient place to have setups for a specific interface in one place. For example if I have an interface INameLookup:

public interface INameLookup
{
    public string Lookup(int id);
}
Enter fullscreen mode Exit fullscreen mode

In many testing scenarios, it is enough that the service returns something relevant to the provided input. I might want to create a lightweight fake implementation with the help of FluentMock:

[EasyTdd.Generators.FluentMockFor(typeof(INameLookup))]
public partial class NameLookupMock
{
    public static NameLookupMock Create()
    {
        var mock = new NameLookupMock();

        return mock.SetupLookup((int id) => $"name_of_{id}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Whenever INameLookup is needed in a test, the NameLookupMock.Create() can be used, which produces the expected results.

One more example could be with a repository:

public interface IOrderRepository
{
    Task<Order> Get(int id);
}
Enter fullscreen mode Exit fullscreen mode

And the mock:

[EasyTdd.Generators.FluentMockFor(typeof(IOrderRepository))]
public partial class OrderRepositoryMock
{
    public static OrderRepositoryMock Create()
    {
        var mock = new OrderRepositoryMock();

        return mock.SetupGet((int id) => OrderBuilder.Random().WithId(id));
    }
}
Enter fullscreen mode Exit fullscreen mode

Pretty cheap fake, when specific values are not required but calling code needs a repository which returns something on each call.

Different ways to set up

I will explore every possible use case in detail in a separate blog series, but for now, I'll quickly highlight the added value. Below I will provide a list of setups with descriptions of them:

 SomeInterfaceMock
    .Create()
    // setup to return expectedResult0 on every Resolve call
    .SetupResolve(() => expectedResult0)
    // setup to return expectedResult1 when id parameter value is 1
    .SetupResolve(() => expectedResult1, isId: x => x == 1)
    // setup to return expectedResult2 when Id value of someOtherResult parameter is 100
    .SetupResolve(() => expectedResult2, isSomeOtherResult: x => x.Id == 100)
    // setup to return dynamic value depending on the input
    .SetupResolve(
        (int id, string name, SomeOtherResultType someOtherResultType) =>
            new SomeResultType
            {
                Id = id, 
                Name = $"name of {id}"
            }
    )
    // setup to throw an exception when input name is "Ted"    
    .SetupResolve(() => throw new Exception("Something went wrong"), isName: x => x == "Ted");
Enter fullscreen mode Exit fullscreen mode

Similar goes for verification:

// verifies if Resolve was called with specific parameters
mock
    .VerifyResolve(
        x => x == 1,
        x => x == "Jon",
        x => x.Id == 100
    );

// verifies if Resolve was called with value "John"
mock
    .VerifyResolve(
        isValue: x => x == "John"
    );

// verifies if Resolve was called with value "John" exactly twice
mock
    .VerifyResolve(
        isValue: x => x == "John",
        times: () => Times.Exactly(2)
    );

// verifies if Resolve was called at least once
mock
    .VerifyResolve();
Enter fullscreen mode Exit fullscreen mode

Configuration

The default placement, naming, and location of the generated FluentMock may not suit everyone's preferences and requirements. Fortunately, all of these aspects can be easily customized to better align with your needs and tastes. Now, I will provide the settings one by one. Settings for the incremental FluentMock, as well as for all other tools, are located in the settings.json file under the IncrementalTestDoubleForAbstract section. Here are the descriptions:

  • ClassSuffix: The default value is Mock. This value determines the suffix added to the target class's corresponding fluent mock class name. In the previous example, for ISomeInterface, the fluentMock class name was SomeInterfaceMock.

  • NameInMenu: The default value is Generate Incremental FluentMock. This setting allows you to modify the name displayed in the Quick Actions menu. By default, it is Generate Incremental FluentMock, but you can rename it to whatever you prefer.

  • AssemblySpecificTargetProjectSuffix: The default value is TestDoubles. This setting instructs EasyTdd to place the generated builder in a project corresponding to the project of the target class. It searches for a project with the same name as the target class’s project, but with the predefined suffix appended. For example, if ISomeInterface is in the project EasyTdd.CodeSource.CodeProject1, the corresponding project for the fluent mock would be EasyTdd.CodeSource.CodeProject1.TestDoubles. This can be customized to suit individual preferences and requirements, such as using a different suffix for the test doubles project. Within the test doubles project, EasyTdd maintains the original folder hierarchy.

  • TargetProjectNameForAssembliesWithoutSpecificTargetProject: The default value is null. This setting specifies a project name where a test double will be placed when the project of a target class doesn't have a corresponding test double project. For example, if you want to place all test doubles from all projects into a single test doubles project called EasyTdd.CodeSource.TestDoubles, you would set this setting to EasyTdd.CodeSource.TestDoubles. In this case, you can leave AssemblySpecificTargetProjectSuffix with its default value. EasyTdd will first try to find a project with the predefined suffix and, if it doesn’t find one, it will fall back to the value in TargetProjectNameForAssembliesWithoutSpecificTargetProject. If EasyTdd doesn't locate the project specified in TargetProjectNameForAssembliesWithoutSpecificTargetProject, it will place the test double class next to the target class.

  • Folder: The default value is Mocks. This setting allows you to organize test doubles, such as builders and mocks, into corresponding folders. For example, if you choose to have a single project for all test doubles, set TargetProjectNameForAssembliesWithoutSpecificTargetProject to EasyTdd.CodeSource.TestDoubles and Folder to Mocks. In this case, the fluent mock for EasyTdd.CodeSource.CodeProject1\Services\ISomeInterface.cs will be placed in EasyTdd.CodeSource.TestDoubles\Mocks\CodeProject1\Services\SomeInterfaceMock.cs.

  • FileTemplates: This section contains settings for the specific files that will be generated.

    • NameTemplate: The default value is {{className}}.cs for the EasyTdd-generated part, and {{className}}.g.cs for the EasyTdd.Generators-generated part. Here, className is a template variable that refers to the name of the FluentMock class.
    • ContentTemplateFile: The default value for the EasyTdd-generated part is DefaultTemplates\incremental.fluentmock.tpl. The default value for the EasyTdd.Generators part is DefaultTemplates\moq.incremental.fluentmock.g.tpl.
    • Recreate: The Recreate setting determines how files are generated. If set to true, the files are generated by the incremental generator (EasyTdd.Generators). If set to false, they are generated by the EasyTdd Visual Studio extension. The files generated by EasyTdd are intended for modifications, such as fluent mock setups. In contrast, the files generated by EasyTdd.Generators will be regenerated whenever changes are made to the target class.
  • ToolingNamespaces: By default, this contains a list of namespaces to support the tooling used in the default fluent mock template. EasyTdd automatically adds target class-related namespaces to the generated class, but it is not aware of the namespaces required by the tooling in the template. This setting allows you to specify those additional namespaces manually. By default, the Moq and System namespaces are added to support functionality provided in the templates.

Feel free to update the templates to match your taste and needs. Remember to copy and paste them outside of the DefaultTemplates folder, as the templates in this folder are overridden with each new release. It is even possible to use some other mocking library in the background like FakeItEasy or some other.

Other updates and fixes in the 0.5.0

Nullable Support

With the latest release templates now are aware if nullable is enabled for the project. Templates are updated accordingly and now generated code reflects that:

Nullable

Unified generated file naming

Now, the file names are consistent whether a builder or a FluentMock is generated by the incremental generator or created as a regular class. Now in both cases the file which is a subject for regeneration is suffixed with .g. In above case it will be SomeInterfaceMock.g.cs and SomeInterfaceMock.cs.

Added links to a menu to all generated files

When non incremental build or fluentMock was created two files were generated and Quick Actions menu showed a link to only one. Now both files are shown:

Links to generated files

Added Build(int count) to the builder template to build multiple objects

Now, each builder has a Build(int count, Func<int, TBuilder, TBuilder>? adjust = null) method, and multiple objects can be built by adjusting each with the adjust method:

var values = WithGenericBuilder<int>
    .Default()
    .Build(
        10, 
        (index, builder) => builder
            .WithSomeInt(index)
            .WithSomeString($"name of {index}")
    )
    .ToList();
Enter fullscreen mode Exit fullscreen mode

Updated test case generation to not require test attributes on the test

Now test attribute is not required for a test to generate test cases. It is enough for a method to be public and EasyTdd adds all required attributes:

Test case generation

And the result:

[TestCaseSource(typeof(SampleTestCases))]
public void SampleTest(int a, int b, int c)
{
    Assert.That(c, Is.EqualTo(a + b));
}
Enter fullscreen mode Exit fullscreen mode

Awareness of methods and properties of the base class

Now, the test generator, builder, and FluentMock generators recognize the properties and methods of the base class and generate the corresponding members accordingly.

Summary

With the release of EasyTdd version 0.5.0, FluentMock is poised to make unit testing even more powerful and accessible for .NET developers. Its intuitive syntax, reduced boilerplate, and the ability to handle complex setups and verifications with ease make it a great addition to any test-driven development toolkit. This release also introduces several other important enhancements, including improved support for nullable reference types, a unified naming convention, and refined templates, ensuring a smoother experience for both new and existing users.

If you’re already using EasyTdd, give FluentMock a try and let me know your experience in the comments below or reach out to me on LinkedIn. For new users, now is a great time to explore EasyTdd! Try it out with FluentMock, Incremental Builder, and other test-driven development-enhancing features that make creating test doubles and managing test cases a breeze.

Stay tuned for future posts where I’ll dive deeper into specific scenarios and share tips on getting the most out of EasyTdd. Make sure to subscribe or follow for updates so you don’t miss out on the next article. Until then, happy testing!

Top comments (0)