Updating Rhino Mocks

In the .NET Core era, more-and-more existing .NET applications are getting updated to the new technology stack. Using netstandard libraries the upgrade is relatively simple. On the other hand, using non standard libraries may cause issues. One of the libraries that is not netstandard is Rhino Mocks. Rhino Mocks is used for mocking test dependency behavior. Unfortunately it is not .NET Core compatible, so using Rhino Mocks in tests is blocker to moving along with the .net core stack.

We can do 2 things to overcome such an issue:

  • Update Rhino Mocks by making it .NET Core compatible

  • Move away from Rhino mocks to other mocking libraries

The first point should be an obvious choice, but as it turns out, there are multiple repos for Rhino Mocks, and none of them seems to be the "true" source and none of them seems to be maintained either. That leaves with the second choice.

Updating a couple of tests, or usually a couple hundreds of tests is easy. One can do it manually, or it can be party automated using Regex and Find and Replace command. When there are thousands or multiple thousands of tests, this task also gets a lot more involved. With that many tests, usually formatting is off, or there are certain edge cases, that one cannot think of upon getting started.

Analyzers

This post is introducing a C# Analyzer which helps to move away from Rhino Mocks. The analyzer helps to convert Rhino Mocks to substitutes of NSubstitute. Why NSubstitute? There are a couple of popular mocking libraries as of today, such as Moq or NSubstitute among many others. NSubstitute provides a convenient way to configure our mocks, hence it has been chosen for the analyzer conversion.

An analyzer can analyze actual type information, and with that it may re-write code more safely. It can also transform larger chunk of blocks, move lambdas or replace statements with multiple statements. Analyzers can be also shared as nuget packages, so distribution is easy, and Visual Studio provides the user interface for the developers. Using analyzers seems a good choice for converting code.

One such C# analyzer is TestUpdaterAnalyzers. Here is a quick look at what it can do:

analyzer-sample

Let's walk through a more complex example, one Rhino Mocks sample:

[Fact]
public void CalculateId_Returns_NumberEs_Multiplied_Age_Height()
{
    // Arrange
    var validatorStub = MockRepository.GenerateStub<IValidator>();
    validatorStub.Stub(x => x.TryValidate(Arg<Request>.Matches(y => y.Name == "test"), out Arg<bool>.Out(true).Dummy)).Return(true);

    var nameProviderMock = MockRepository.GenerateMock<INameProvider>();
    nameProviderMock.Expect(x => x.GetFullName("test")).IgnoreArguments().Return("eee").Repeat.Any();
    nameProviderMock.Stub(x => x.Initialized).Return(true);

    var loggerMock = MockRepository.GenerateMock<ILogger<ComplexBusinessLogic>>();
    loggerMock.Stub(x => x.IsEnabled).PropertyBehavior();

    var sut = new ComplexBusinessLogic(validatorStub, nameProviderMock, loggerMock);
    
    // Act
    var result = sut.CalculateId(new Request() { Age = 1, Height = 1, Name = "test" });

    // Assert
    Assert.Equal(3, result);
    nameProviderMock.VerifyAllExpectations();
    loggerMock.AssertWasCalled(x => x.Log(Arg<string>.Is.NotNull));
    Assert.True(loggerMock.IsEnabled);
}

3 mocks are created, 2 mocks and 1 stub:

  • validatorStub has one behavior set for TryValidate method call, when the request arguments's name property equals to test. Note, that this method also has an out argument.

  • nameProviderMock has a stub for accessing the Initialized property, and an expectation for GetFullName method call, where arguments are ignored, and the call may be repeated any times.

  • loggerMock sets property behavior for the IsEnabled property.

  • Finally, we have asserts on the Log method being called on loggerMock and verifying all expectations set for nameProviderMock.

The NSubstitute converted test method:

[Fact]
public void CalculateId_Returns_NumberEs_Multiplied_Age_Height()
{
    // Arrange
    var validatorStub = Substitute.For<IValidator>();
    validatorStub.TryValidate(NSubstitute.Arg.Is<Request>(y => y.Name == "test"), out NSubstitute.Arg.Any<bool>()).Returns(a0 => { a0[1] = true; return true; });

    var nameProviderMock = Substitute.For<INameProvider>();
    nameProviderMock.GetFullName("test").ReturnsForAnyArgs("eee");
    nameProviderMock.Initialized.Returns(true);

    var loggerMock = Substitute.For<ILogger<ComplexBusinessLogic>>();

    var sut = new ComplexBusinessLogic(validatorStub, nameProviderMock, loggerMock);
    
    // Act
    var result = sut.CalculateId(new Request() { Age = 1, Height = 1, Name = "test" });

    // Assert
    Assert.Equal(3, result);
    nameProviderMock.Received().GetFullName("test");
    loggerMock.Received().Log(NSubstitute.Arg.Is<string>(a0 => a0 != null));
    Assert.True(loggerMock.IsEnabled);
}

In the converted method, we still have 3 substituted objects. validatorStub (notice, that the name of the variable has not changed) is configuring a behavior while respecting argument condition and the out parameters from the original stub. Note, that the out argument is set in the Returns lambda compared to the original code.

For nameProviderMock the IgnoreArguments has been converted to ReturnsForAnyArgs, and substitution of the property looks also simpler with the NSubstitute syntax.

PropertyBehavior is the default in NSubsitute, there is no need to set it up explicitly for loggerMock.

Finally, notice how AssertWasCalled and VerifyAllExpectations has been converted to Received calls for NSubstitute.

Conclusion

The goal of the analyzer is to rewrite the original method with NSubstitute, while keeping the new behavior as close as possible to the original one. Not all operations may be converted directly, there is no one-to-one mapping between the two libraries. The GitHub home of the test Analyzer details on the supported features.