mehdime / DbContextScope

A simple and flexible way to manage your Entity Framework DbContext instances
http://mehdi.me/ambient-dbcontext-in-ef6/
MIT License
634 stars 271 forks source link

How to mock and unit test? #22

Closed rosdi closed 9 years ago

rosdi commented 9 years ago

Hi, this is not an 'issue' per se but I just want to know how do I mock this so that I can add unit test? I have FakeDbSet and FakeDbContext but I could not find a way to actually inject this into DbContextScope without major changes to the source code..

Any help is appreciated... thanks.

rosdi commented 9 years ago

Nevermind.. I will just do integration test instead... and this is not even a proper place to ask this question anyway. Sorry about that.

pkirillov commented 9 years ago

I had the same question last week for myself. Ended up with this kind of fake. Not fully implemented obviously, but enough for now.

Note: my real services are using AmbientDbContextLocator, but within unit tests I don't need it, that's why IDbContextCollection DbContexts is not implemented.

    /// <summary>
    /// Represents fake DbContextScope for services unit testing
    /// </summary>
    public class FakeDbContextScope : IDbContextScope
    {
        private bool _disposed;
        private int _saveChangesCount;

        public FakeDbContextScope()
        {
            _disposed = false;
            _saveChangesCount = 0;
        }

        public int SaveChanges()
        {
            _saveChangesCount++;
            return 0;
        }

        public Task<int> SaveChangesAsync()
        {
            throw new NotImplementedException("SaveChangesAsync() is not implemented within FakeDbContextScope");
        }

        public Task<int> SaveChangesAsync(CancellationToken cancelToken)
        {
            throw new NotImplementedException("SaveChangesAsync(cancelToken) is not implemented within FakeDbContextScope");
        }

        public void RefreshEntitiesInParentScope(IEnumerable entities)
        {
            throw new NotImplementedException("RefreshEntitiesInParentScope(entities) is not implemented within FakeDbContextScope");
        }

        public Task RefreshEntitiesInParentScopeAsync(IEnumerable entities)
        {
            throw new NotImplementedException("RefreshEntitiesInParentScopeAsync(entities) is not implemented within FakeDbContextScope");
        }

        public IDbContextCollection DbContexts
        {
            get
            {
                throw new NotImplementedException("IDbContextCollection() is not implemented within FakeDbContextScope");
            }
        }

        public bool Disposed
        {
            get
            {
                return _disposed;
            }
        }

        public void Dispose()
        {
            _disposed = true;
        }
    }
mehdime commented 9 years ago

Hi rosdi,

This is the right place to ask since there are no other places for these sort of questions :)

I'm assuming that you followed Microsoft's guidance to create your stub DbContext and DbSet instances.

You'd now like to get DbContextScope to return these instead of the real instances. This is actually very easy (or I think it is anyway, I haven't actually tried it yet :) )

You'll note that the DbContextScope constructor takes an optional IDbContextFactory instance. The IDbContextFactory was created to do precisely what you're trying to do: it lets you control the creation of the DbContext instances, allowing you to e.g. substitute them with a stub implementation.

So in your unit test, simply implement the IDbContextFactory interface and get it to return your stub DbContext instance. Alternatively, you can of course use a mocking library like Moq to do that for you. Then, provide this "stub DbContext factory" to your DbContextScope and you're all done.

rosdi commented 9 years ago

I got it to work... thanks to both of you.. from now on I don't see any reason NOT to use DbContextScope for my future projects... cheers..

tomtommorrow commented 9 years ago

Hi. I followed the guide you referenced above for creating stub DbContext and DbSet instances but I am still having trouble mocking a service similar(really the exact same) to the 'SendWelcomeEmail' in the UserEmailService examples. I am injecting IDbContextScopeFactory (using autofac) just like the UserEmailService example(no other dependencies) but I can't figure out how to mock the service. I am using moq. Any help would be greatly appreciated. I love the work you have done, it is making all of my projects so much easier. Thanks ahead.

         public async Task Test_vendor_async_service()
       {
        //shortened for brevity
        var data = new List<Vendor>();

        var mockSet = new Mock<DbSet<Vendor>>();
        mockSet.As<IDbAsyncEnumerable<Vendor>>()
            .Setup(m => m.GetAsyncEnumerator())
            .Returns(new TestDbAsyncEnumerator<Vendor>(data.GetEnumerator()));

        mockSet.As<IQueryable<Vendor>>()
            .Setup(m => m.Provider)
            .Returns(new TestDbAsyncQueryProvider<Vendor>(data.Provider));

        mockSet.As<IQueryable<Vendor>>().Setup(m => m.Expression).Returns(data.Expression);
        mockSet.As<IQueryable<Vendor>>().Setup(m => m.ElementType).Returns(data.ElementType);
        mockSet.As<IQueryable<Vendor>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

        //this is where I am not so sure what needs to be done.
        var mockFactory = new Mock<IDbContextScopeFactory>();
        mockFactory.As<MyContext>()
            .Setup(c => c.Vendors).Returns(mockSet.Object);

        //I can't really pass in the mockfactory because it of type Mock<T>
        //confused
        var service = new VendorsService(mockFactory);
        var vendors = await service.GetAllAsync().ConfigureAwait(false);

        Assert.AreEqual(3, vendors.Count);
        Assert.AreEqual("AAA", vendors[0].BusinessName);
        Assert.AreEqual("BBB", vendors[1].BusinessName);
        Assert.AreEqual("ZZZ", vendors[2].BusinessName);
    }
bmosca commented 6 years ago

Hi, I've been working around the DbContextScope which is working fine however when it comes to Unit Testing I have to say I am a bit confused about how I should use the libraries in order to unit test a MVC application. I am trying to call the method GetScanDefinitions() and assert the objects retrieved with my predefined mockset.

This is my Unit Test class:

public class UnitScanDefinitionsControllerTest
    {   
    private CrawlerContext _dbEntities;
        private readonly ICrawlerRepository _crawlerRepository;
        private IScanDefinitionService _scanDefinitionService;

        public UnitScanDefinitionsControllerTest()
        {
            IAmbientDbContextLocator ambientDbContextLocator = new AmbientDbContextLocator();
            _crawlerRepository = new CrawlerRepository(ambientDbContextLocator);
        }

        [Fact]
        public async Task ScanDefinitionView()
        {
            var data = new List<ScanDefinition>
            {
                new ScanDefinition { Name = "AAA" },
                new ScanDefinition { Name = "BBB" },
                new ScanDefinition { Name = "CCC" },

            }.AsQueryable();

            var mockSet = new Mock<DbSet<ScanDefinition>>();
            mockSet.As<IDbAsyncEnumerable<ScanDefinition>>()
                .Setup(m => m.GetAsyncEnumerator())
                .Returns(new DbAsyncEnumerator<ScanDefinition>(data.GetEnumerator()));

            mockSet.As<IQueryable<ScanDefinition>>()
                .Setup(m => m.Provider)
                .Returns(new DbAsyncQueryProvider<ScanDefinition>(data.Provider));

            mockSet.As<IQueryable<ScanDefinition>>().Setup(m => m.Expression).Returns(data.Expression);
            mockSet.As<IQueryable<ScanDefinition>>().Setup(m => m.ElementType).Returns(data.ElementType);
            mockSet.As<IQueryable<ScanDefinition>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

            var mockFactory = new Mock<IDbContextFactory>();
            mockFactory.Setup(c => c.CreateDbContext<CrawlerContext>()).Returns(_dbEntities);

            _dbContextFactory = mockFactory.Object;

            // Setup DbContextScopeFactory to use mocked context
            _mockContextScope = new DbContextScopeFactory(_dbContextFactory);
            _scanDefinitionService = new ScanDefinitionService(_mockContextScope, _crawlerRepository);

            var scanDefinitions = await _scanDefinitionService.GetScanDefinitions().ToListAsync();

            Assert.Equal(3, scanDefinitions.Count);
            Assert.Equal("AAA", scanDefinitions[0].Name);
            Assert.Equal("BBB", scanDefinitions[1].Name);
            Assert.Equal("ZZZ", scanDefinitions[2].Name);
        }
    }

The Service Layer looks like this:

public class ScanDefinitionService : IScanDefinitionService
    {
        private readonly IDbContextScopeFactory _dbContextScopeFactory;
        private readonly ICrawlerRepository _crawlerRepository;

        public ScanDefinitionService(IDbContextScopeFactory dbContextScopeFactory, ICrawlerRepository context)
        {
            _dbContextScopeFactory = dbContextScopeFactory;
            _crawlerRepository = context;
        }

        public IQueryable<ScanDefinition> GetScanDefinitions()
        {
            using (_dbContextScopeFactory.CreateReadOnly())
            {
                return _crawlerRepository.GetScanDefinitions();
            }
        }
    }

And finally the Repository Layer:

public class CrawlerRepository : ICrawlerRepository

    {
        private readonly IAmbientDbContextLocator _ambientDbContextLocator;

        private CrawlerContext DbContext
        {
            get
            {
                var dbContext = _ambientDbContextLocator.Get<CrawlerContext>();

                if (dbContext == null)
                    throw new InvalidOperationException("Db Context Error message");

                return dbContext;
            }
        }

        public CrawlerRepository(IAmbientDbContextLocator ambientDbContextLocator)
        {
            if (ambientDbContextLocator == null) throw new ArgumentNullException("ambientDbContextLocator");
            _ambientDbContextLocator = ambientDbContextLocator;
        }

        public IQueryable<ScanDefinition> GetScanDefinitions()
        {
            return DbContext.ScanDefinitions  **<--- TEST FAILS HERE; Message: System.NullReferenceException : Object reference not set to an instance of an object.**
                .Include(sd => sd.ScanBases)
                .Include(sd => sd.ScanDefinitionFileTypes)
                .Include(sd => sd.ScanDefinitionRuleDefinitions);
        }
    }

Any help would be much appreciated.