moq / Moq.AutoMocker

An auto-mocking IoC container for Moq
MIT License
387 stars 53 forks source link

Register mock/service as scoped #188

Open archigo opened 1 year ago

archigo commented 1 year ago

We have a Parallel.ForEach that uses IServiceScopeFactory to make sure certain services are different instances.

I am having a hard time figure out how to make this work when resolving through automocker.

I can mock the scope resolution to resolve to an automocker, but I can not seem to find a way to make that scoped automocker have the mocks that are set on the original automocker.

Our setup is that we have a base class that all our tests inherit from. The base class will set up an automocker with default configs and we add to those configs in each test.

Is there a convenient way to work with scoped services in automocker, or a way to copy the setup in an existing automocker to a new mocker?

            // the base mocker that all tests use
            autoMocker = CreateDefaultAutoMocker();

            var scopeMock = autoMocker.GetMock<IServiceScope>();
            scopeMock.Setup(x => x.ServiceProvider).Returns(() =>
            {
                var scopedMocker = CreateDefaultAutoMocker();
                // copy all setup from base mocker here and overwrite the scoped service?
                scopedMocker.Use<IScopedService>(new ScopedService());
                return scopedMocker; 
            });

            var scopeFacMock = autoMocker.GetMock<IServiceScopeFactory>();
            scopeFacMock.Setup(x => x.CreateScope()).Returns(scopeMock.Object);

In the above code (from our base setup class) the scopedMocker is missing the mock setup from individual tests

Keboo commented 1 year ago

@archigo I think the issue that you are running into with your code is that your IServiceScopeFactory is always going to return the same IServiceScope instance. So even though you may create multiple scope, all of the scopes are the same instance and will be then using the same AutoMocker instance.

I think something like this is closer to what you want.

[TestMethod]
public void ScopedItemsAreResolvedWithinScopes()
{
    AutoMocker mocker = new();

    var scopeFacMock = mocker.GetMock<IServiceScopeFactory>();
    scopeFacMock.Setup(x => x.CreateScope()).Returns(() => {
        AutoMocker scopedMocker = new();

        //Regsiter items that should be scoped
        scopedMocker.With<IScopedService, ScopedService>();

        Mock<IServiceScope> scope = new();
        scope.Setup(x => x.ServiceProvider).Returns(() => scopedMocker);
        return scope.Object;
    });

    using var scope1 = mocker.CreateScope();
    using var scope2 = mocker.CreateScope();

    IScopedService instance1 = scope1.ServiceProvider.GetRequiredService<IScopedService>();
    IScopedService instance2 = scope2.ServiceProvider.GetRequiredService<IScopedService>();

    Assert.AreNotSame(instance1, instance2);
}

It is also worth noting that AutoMocker is not intended to be a full DI container (the fact it implements IServiceProvider is more of a convenience). In a full DI implementation, the scoped AutoMocker instance would reach back up to the parent AutoMocker instance for things like singletons. This sort of behavior could be achieved with a custom resolver, but in most cases where a full DI container is needed, I would recommend just creating the real DI container, and registering mock instances where appropriate.

archigo commented 1 year ago

The issue is that there are mocks set up that needs to be available to on the scoped mocks as well.

I am resolving a service which is not concurrency safe, so it is important that a new service is resolved in each scope, but I still need to have the mocks that are returning external data in each scope as the service depends on them.

public class BaseTestClass {
    protected baseAutoMocker;

    public BaseTestClass(){
        baseAutoMocker= new AutoMocker();
        // setup mocks for all the things that always needs to be setup, like auth services and other stuff.
        ....

       var scopeFacMock = baseAutoMocker.GetMock<IServiceScopeFactory>();
        scopeFacMock.Setup(x => x.CreateScope()).Returns(() => {
            AutoMocker scopedMocker = new(); // this somehow needs to copy the mock that each unit test adds to baseAutoMocker

            //Register items that should be scoped
            scopedMocker.With<IScopedService, ScopedService>();

            Mock<IServiceScope> scope = new();
            scope.Setup(x => x.ServiceProvider).Returns(() => scopedMocker);
            return scope.Object;
        });
    }
}
[TestClass]
public class Tests : BaseTestClass {

    [TestMethod]
    public void SomeTest()
    {    
        var someServiceMock = baseAutoMocker.GetMock<SomeService>();
        someServiceMock.Setup(...) // this needs to be setup on all of the scoped mocks as well!

        var sut = baseAutoMocker.CreateInstance<ServiceToBeTested>();

        sut.Run()

       Assert...
    }
}
// code to be tested
Parallel.ForEach(operations, operation => 
{
    var sp = _serviceScopeFactory.CreateScope().ServiceProvider;
    var dbcontext = sp.GetRequiredService<DbContext>();
    var someEntity = dbcontext.SomeEntities.First(x => x.Id == operation.Id);
    var someService = sp.GetRequiredService<SomeService>();
    // get some mocked data from someService to create some database chages
    var data = someService.GetData();
    someEntity.Something = data.Something;
    ...

    dbcontext .SaveChanges();
});