Updating Rhino Mocks
06/01/2020
5 minutes
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:
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 forTryValidate
method call, when the request arguments's name property equals totest
. Note, that this method also has an out argument.nameProviderMock
has a stub for accessing theInitialized
property, and an expectation forGetFullName
method call, where arguments are ignored, and the call may be repeated any times.loggerMock
sets property behavior for theIsEnabled
property.Finally, we have asserts on the
Log
method being called onloggerMock
and verifying all expectations set fornameProviderMock
.
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.