microsoft / PowerPlatform-DataverseServiceClient

Code Replica for Microsoft.PowerPlatform.Dataverse.Client and supporting nuget packages.
MIT License
277 stars 50 forks source link

[Feature] Mocking OrganizationServiceContext.CreateQuery not possible because the class has no interface and the method isn't virtual. #75

Open BoutemineOualid opened 3 years ago

BoutemineOualid commented 3 years ago

Hi,

I am trying to write some unit tests for my services but I wasnt able to Mock the OrganizationServiceContext class using the Moq framework. Specifically, I was trying to mock the CreateQuery method but since it is not virtual and the class has no reusable interface, the framework wasn't able to mock it.

I was wondering if this is something you could consider for the upcoming release.

Thanks.

jordimontana82 commented 3 years ago

@BoutemineOualid OrganizationServiceContext implements IOrganizationService which is the interface you could mock. Or else try FakeXrmEasy which already mocks that interface for XrmTooling and will be doing the same for CdsServiceClient in the next major version

BoutemineOualid commented 3 years ago

I am on version 0.2.17-alpha and I can't see any implementation of the IOrganizationService on OrganizationServiceContext. I will try FakeXrmEasy.

Thanks

jordimontana82 commented 3 years ago

I believe if the OrganizationServiceContext was generated by CrmSvcUtil or similar and it is not part of this lib, but it has an OrganizationServiceProxy property which implements IOrganizationService for the purposes of querying data (i.e. .CreateQuery<> would suffice), which you could mock.

CdsServiceClient also implements that interface.

The point is that for querying data you don't have to mock the OrganizationServiceContext, you could just mock the IOrganizationService and your system under test could use any wrappers around that, unless there is a property that you really need from the OrganizationServiceContext?

BoutemineOualid commented 3 years ago

I have some custom queries on the IQueryable Returned by this method and I would love to replace it with a custom IQueryable implementation containing predefined entries. Something like this :

        _crmServiceContextMock
            .Setup(crmServiceContext => crmServiceContext.CreateQuery<Address>())
            .Returns(new List<Address>()
                {
                    ...
                }
                .AsQueryable()
            );

Tried injecting a mocked CdsServiceClient to the context class but got several errors and it wasn't worth investigating. I guess the easiest would be to make this class mockable or have some kind of in memory db context that mimics the behavior of entity framework.

MattB-msft commented 3 years ago

This sort of Mocking is usually quite challenging,
as has been said, your better and safer Mocking the IOrganziationService Implementation and overriding the commands.
if you take a look at the code in the code base, you will see unit tests were we are doing exactly that.

its worth noting however that we use a special constructor to allow us to do this testing currently that is not generally accessible.

MattB-msft commented 2 years ago

While this has been open for quite a while, we have not forgotten about it :) There is an active effort now underway make mocking easier for a number of our base types in our platform.

MichaelHolmesWP commented 1 year ago

Just leaving a comment here in the good old future (Oct '2022), I assume @MattB-msft and the team fixed this at some point and the issue can finally be closed?

The underlying CreateQuery(IQueryProvider, String) API is virtual, and I've taken to implementing a partial with a static dictionary at my Test Project Level:

using System;
using System.Collections.Generic;
using System.Linq;

namespace My.Early.Bound.Model.Namespace
{
    public partial class CrmServiceContext
    {
        private static Dictionary<string, Func<IQueryable>> _testFuncs = new Dictionary<string, Func<IQueryable>>();

        internal static void AddTestProvider(string entityLogicalName, Func<IQueryable> provider)
        {
            _testFuncs[entityLogicalName] = provider;
        }

        protected override IQueryable<TEntity> CreateQuery<TEntity>(IQueryProvider provider, string entityLogicalName)
        {
            if (_testFuncs.ContainsKey(entityLogicalName) 
                && _testFuncs[entityLogicalName].Invoke() is IQueryable testProvider
            )
                return testProvider.Cast<TEntity>();
            else
                return base.CreateQuery<TEntity>(provider, entityLogicalName);
        }
    }
}

Which can then be consumed in your test projects without having the hassle of the mocking the IOrganizationService completely, especially if you just want to be testing your retrieve operations. I find this particularly useful as you can then just hijack the Query Sets at a practical level, rather than having to mock a large set of distinct entity queryables (in the case where some Early Bound CrmServiceContext retrieve methods invoke some immensely complex Query syntax with many nested joins) without having to mock the response for every permutation in the SUT.

i.e.

[SetUp]
public void Setup()
{
    // Yada Yada Unit Test Setup Stuff
    _orgSvcMock = new Mock<IOrganizationService>();
    _orgSvcMock.Setup(o => o.Create(It.IsAny<Entity>())).Returns((Entity e) => e.Id != default ? e.Id : Guid.NewGuid());
    _orgSvcMock.Setup(o => o.Update(It.IsAny<Entity>()));
    // Test Class IOrganizationService Prop
    OrgSvc = _orgSvcMock.Object;
    ...
    CrmServiceContext.AddTestProvider(Account.EntityLogicalName, () => new List<Account> { _thirdPartyAcct }.AsQueryable());
    CrmServiceContext.AddTestProvider(Contact.EntityLogicalName, () => new List<Contact> { _thirdPartyContact, _applicantContact }.AsQueryable());
}

[Test]
public void SomeTest_ChecksStuff()
{
    // Could be nested inside some SUT Service that takes IOrganizationService into its constructor or whatever, just showing cases that I know work fine with this override
    using (var svcCtx = new CrmServiceContext(OrgSvc))
            {
                var data = svcCtx.CreateQuery<Account>()
                    .FirstOrDefault(a => a.Id == _thirdPartyAcct.Id);
                // or
                var data2 = svcCtx.AccountSet.FirstOrDefault(a => a.Id == _thirdPartyAcct.Id);
                // or
                var data3 = (from a in svcCtx.AccountSet
                             select a).FirstOrDefault();
            }
}

[EDIT] - I've implemented this in both the old & new SDK (Framework + Net6) without any issues / alterations to my EB context.

MattB-msft commented 1 year ago

Let me check with the API team, this should have been close if they fixed it.