Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mockObject.Verify does not record args passed in multiple calls as separate cloned objects (not pointing to the same reference) #1333

Closed
aqib-bhat opened this issue Feb 20, 2023 · 5 comments

Comments

@aqib-bhat
Copy link

aqib-bhat commented Feb 20, 2023

Background

  • I was writing a method to revert the original value for a field if some operation on an external dependency failed.
  • After fetching the record with the original value, the new value is first stored in the DB and when the external dependency fails, the original value is again saved for that field.

What I expected

  • When writing the unit tests for the service, I expected mockRepositoryObject.Verify to prove that the following 3 operations:
    • Get method first gets called (which is setup to return the entity record with the original value in the field.
    • SaveOrUpdate / Upsert method first gets called with the new value in the field.
    • SaveOrUpdate / Upsert method then gets called with the original value in the field.

What actually happens with Moq4 (Seen with version 4.18.4)

  • mockRepositoryObject.Verify shows that SaveOrUpdate method gets called Exactly 2 times but both times with the original value in the field.

How I verified that this was indeed a bug

  • By adding breakpoints and debugging the unit test to make sure that indeed the SaveOrUpdate method gets called twice, first with the new value in the field/property of interest and the original value.
  • I also added a Callback to the Setup for the SaveOrUpdate method to save the arguments passed into a List. The List did end up with 2 elements but both had the original value in the field/property of interest.

Workaround

  • Using the MemberwiseClone method added a ShallowCopy method to the entity class: https://learn.microsoft.com/en-us/dotnet/api/system.object.memberwiseclone?view=net-7.0
  • Then in the Callback added to the Setup for the SaveOrUpdate method, I saved a shallow copy of the argument into a List.
  • Then, when I do the verification on the members in args List, they show the first member ([0]) has the new value for the field/property of interest and the second member ([1]) has the original value.

Example to demonstrate the bug

using Moq;
					
public class Program
{
	public static void Main()
	{
		var myRepositoryMock = new Mock<IMyRepository>();
		MyEntity myEntity = new()
			{
				Id = 3,
				Name = "Tom",
				Age = 10
			};
		MyService myService = new MyService(myRepositoryMock.Object);
		myRepositoryMock.Setup(m => m.GetMyEntity(3)).Returns(myEntity);
		myService.UpdateAgeTwice(3);
		myRepositoryMock.Verify(m => m.UpsertMyEntity(It.IsAny<MyEntity>()), Times.Exactly(2));
		//myRepositoryMock.Verify(m => m.UpsertMyEntity(It.Is<MyEntity>(u => u.Age == 11)), Times.Once()); // This should work but does NOT!
		//myRepositoryMock.Verify(m => m.UpsertMyEntity(It.Is<MyEntity>(u => u.Age == 12)), Times.Once()); // This should work but does NOT!
		myRepositoryMock.Verify(m => m.UpsertMyEntity(It.Is<MyEntity>(u => u.Age == 12)), Times.Exactly(2)); // This should NOT work but does!
	}
	
	public class MyEntity
	{
		public int Id { get; set; }
		public string Name { get; set; }
		public int Age { get; set; }
	}
	
	public interface IMyRepository
	{
		public MyEntity GetMyEntity(int id);

		public void UpsertMyEntity(MyEntity myEntity);
	}
	
	public class MyService
	{
		private IMyRepository _myRepository;
		
		public MyService(IMyRepository myRepository)
		{
			_myRepository = myRepository;
		}
		
		public void UpdateAgeTwice(int id)
		{
			MyEntity myEntity =_myRepository.GetMyEntity(id);
			myEntity.Age++;
			_myRepository.UpsertMyEntity(myEntity);
			myEntity.Age++;
			_myRepository.UpsertMyEntity(myEntity);
		}
	}
}
@stakx
Copy link
Contributor

stakx commented Feb 20, 2023

Not a bug. Check the closed issues, we get this about once every two weeks.

@aqib-bhat
Copy link
Author

@stakx Thank you for the quick response!
So, this will be available in 4.19.0 and as per your example in #1319 one has to use multiple Setups with the different expected args and .Verifiable(Times.Once); at the end of each setup for a single .Verify() call for this to work.
I usually do prefer to have .Verifiable in setups and a single .Verify() in the // Assert section, however, in case of multiple invocations with different/modified args, it seems better to have a single .Setup and multiple .Verify calls.
Anyway, as I have mentioned, a Callback in the Setup that saves the shallow/deep copy of the arg(s) rather than the reference, did the trick for me.

@rishisolutionsbysms
Copy link

Any idea when this is going to release?

@stakx
Copy link
Contributor

stakx commented Apr 30, 2023

I'll do a release as soon as #1340 has been taken care of.

@rishisolutionsbysms
Copy link

@stakx Any idea when this is going to release?

@devlooped devlooped locked and limited conversation to collaborators Sep 4, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants