Elfocrash / Cosmonaut

🌐 A supercharged Azure CosmosDB .NET SDK with ORM support
https://cosmonaut.readthedocs.io
MIT License
341 stars 44 forks source link

Mocking ICosmosStore with Moq #43

Closed reddy6ue closed 5 years ago

reddy6ue commented 5 years ago

I am facing the following problems with Moq. I am mocking an ICosmosStore repository to write unit tests in the service layer.

var fakeDataRepo = new Mock<ICosmosStore<FakeData>>();
var fakeData = Factory.CreateFakeData90;

fakeDataRepo.Setup(p => 
                        p.AddAsync(It.IsAny<FakeData>()))
                        .Returns<FakeData>(a => 
                                Task.FromResult(
                                    new CosmosResponse<FakeData>(fakeData, null)));

I get the error Error CS0854 An expression tree may not contain a call or invocation that uses optional arguments

The root cause is that AddAsync has two other optional parameters RequestOptions and CancellationToken which trips up Moq. When I add the other two parameters, it obviously fails because I didn't setup the method that takes a single parameter. The problem is not with Cosmonaut per se, but it'll really help with a very popular mocking framework to also provide a version of AddAsync in the interface without the two optional parameter that just delegates to the method with the two optional parameter not being used.

Elfocrash commented 5 years ago

Hello @reddy6ue,

All you need to do is provide the default values for this call when you set it up. It's how Moq always worked and I have to do the same for my own tests as well.

Simply do:

activityRepo.Setup(p => p.AddAsync(It.IsAny<FakeData>(), null, default(CancellationToken)))
                        .Returns<FakeData>(a => 
                                Task.FromResult(
                                    new CosmosResponse<FakeData>(fakeData, null)));
reddy6ue commented 5 years ago

When I do this, I get a System.ArgumentException with the error Invalid callback. Setup on method with 3 parameter(s) cannot invoke callback with different number of parameters (1) error.

Elfocrash commented 5 years ago

That's weird. Can you change them with It.IsAny?

activityRepo.Setup(p => p.AddAsync(It.IsAny<FakeData>(), It.IsAny<RequestOptions>(), It.IsAny<CancellationToken>))
                        .Returns<FakeData>(a => 
                                Task.FromResult(
                                    new CosmosResponse<FakeData>(fakeData, null)));
reddy6ue commented 5 years ago

Tried that too. Didn't work. I think it has a problem with the fact that my method call sends only one parameter.

Elfocrash commented 5 years ago

That doesn't really make sense because calling a method with optional parameters means that the optional parameters are defaulted. They are still invoked though. Are you sure you are setting it up correctly? Did you try checking how i did it on my tests? I think it's the way you do Returns.

reddy6ue commented 5 years ago

I know it doesn't make sense. That's why I'm struggling with it. I changed my AddAsync methods to send the two additional parameters as null, and default(CancellationToken) and the error is still occurring. Baffling.

Elfocrash commented 5 years ago

Can you please paste the full test code so I can debug it locally?

Elfocrash commented 5 years ago

Btw, looking at the code you submitted initially, you are creating a fakeDataRepo but you are setting up an activityRepo. Is that intended?

reddy6ue commented 5 years ago

Meant to type in fakeDataRepo.

Elfocrash commented 5 years ago

Yeah, paste the test including any setup if possible please. I can’t reproduce it.

reddy6ue commented 5 years ago

Here's the only thing I've done differently. I created a root entity class called SharedCosmosEntity which looks like this.

    public interface ISharedCosmosEntity
    {
        string CosmosEntityName { get; set; }
    }

All these contortions in the code are to make sure that my collection name is the class name.

    public class SharedCosmosEntity : ISharedCosmosEntity
    {
        [JsonProperty("id")]
        public string Id { get; set; }

        // todo: This is not working. Need to figure out why
        [JsonProperty("CosmosEntityName")]
        public string CosmosEntityName
        {
            get => this.GetType().Name;
            set => value = this.GetType().Name;
        }
    }

And each entity looks like this.

    public class Activity: SharedCosmosEntity
    {
         public string SomeProp {get; set;}
}
reddy6ue commented 5 years ago

Here's my test.

    public class ActivityManagementServiceTest
    {
        private ActivityManagementService activityManagementService = null;

        public ActivityManagementServiceTest()
        {
            var activityRepo = new Mock<ICosmosStore<Activity>>();

            var fakeActivity = ActivityManagementServiceData.CreateActivityOne();

            var document = fakeActivity.ConvertObjectToDocument();
            var resourceResponse = document.ToResourceResponse(HttpStatusCode.OK);

            activityRepo.Setup(p =>
                     p.AddAsync(It.IsAny<Activity>(),
                                 null, default(CancellationToken)))
                     .Returns<Activity>(a =>
                             Task.FromResult(
                                 new CosmosResponse<Activity>(fakeActivity, resourceResponse)));

            activityManagementService = new ActivityManagementService(activityRepo.Object);
        }

        [Fact]
        public void StoreUniqueActivity_ValidInput_ReturnActivityObject()
        {
            var fakeActivity = ActivityManagementServiceData.CreateActivityOne();

            var storeActivity = pActivityManagementService.StoreUniqueActivityAsync(fakeActivity);

            Assert.True(true);
        }
}

The failure occurs in the Test Constructor before it ever hits the test.

Elfocrash commented 5 years ago

You don not need to deal with the CosmosEntityName property. Cosmonaut will do that for you.

Can you change this: .Returns<Activity>(a => Task.FromResult(new CosmosResponse<Activity>(fakeActivity, resourceResponse)));

to this:

.ReturnsAsync(new CosmosResponse<Activity>(fakeActivity, resourceResponse))

reddy6ue commented 5 years ago

That did the trick! Thanks for helping me out.