MarcosF8
MarcosF8

Reputation: 2008

Mocking EF core dbcontext and dbset

I am using ASP.NET Core 2.2, EF Core and MOQ. When I run the test I am getting this error:

Message: System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member: x => x.Movies

What I am doing wrong?

public class MovieRepositoryTest
{
    private readonly MovieRepository _sut;

    public MovieRepositoryTest()
    {
        var moviesMock = CreateDbSetMock(GetFakeListOfMovies());
        var mockDbContext = new Mock<MovieDbContext>();
        mockDbContext.Setup(x => x.Movies).Returns(moviesMock.Object);
        _sut = new MovieRepository(mockDbContext.Object);
    }

    [Fact]
    public void GetAll_WhenCalled_ReturnsAllItems()
    {
        //Act
        var items = _sut.GetAll();

        //Assert
        Assert.Equal(3, items.Count());
    }

    private IEnumerable<Movie> GetFakeListOfMovies()
    {
        var movies = new List<Movie>
        {
            new Movie {Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action"},
            new Movie {Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action"},
            new Movie {Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action"}
        };

        return movies;
    }

    private static Mock<DbSet<T>> CreateDbSetMock<T>(IEnumerable<T> elements) where T : class
    {
        var elementsAsQueryable = elements.AsQueryable();
        var dbSetMock = new Mock<DbSet<T>>();

        dbSetMock.As<IQueryable<T>>().Setup(m => m.Provider).Returns(elementsAsQueryable.Provider);
        dbSetMock.As<IQueryable<T>>().Setup(m => m.Expression).Returns(elementsAsQueryable.Expression);
        dbSetMock.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(elementsAsQueryable.ElementType);
        dbSetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(elementsAsQueryable.GetEnumerator());

        return dbSetMock;
    }
  }

And this is my DB Context, with the Movie dbSet:

public class MovieDbContext: DbContext
{
    public MovieDbContext(DbContextOptions<MovieDbContext> options) : base(options)
    {

    }

    public DbSet<Movie> Movies { get; set; }
}

And the Repository with the method GetAll to be tested:

 public class MovieRepository: IMovieRepository
{
    private readonly MovieDbContext _moviesDbContext;
    public MovieRepository(MovieDbContext moviesDbContext)
    {
        _moviesDbContext = moviesDbContext;
    }

    public IEnumerable<Movie> GetAll()
    {
        return _moviesDbContext.Movies;
    }
}

Upvotes: 101

Views: 125102

Answers (7)

Patrick Knott
Patrick Knott

Reputation: 1817

I'm using .Net6 Core I found this answer on the internet and incorporated it into my test code: https://jason-ge.medium.com/mock-async-data-repository-in-asp-net-core-3-1-634cb19a3013

To add a DbContext Mock:

    public static Mock<T> GetDbContextMock<T>() where T : DbContext
    {
        var optionsBuilder = new DbContextOptionsBuilder<T>();
        return new Mock<T>(optionsBuilder.Options);
    }

To add a functional DBSet Mock:

    public static DbSet<T> GetQueryableMockDbSet<T>(List<T> source)
        where T : class
    {
        var queryable = source.AsQueryable<T>();

        var dbSet = new Mock<DbSet<T>>();
        dbSet.As<IAsyncEnumerable<T>>().Setup(x => x.GetAsyncEnumerator(default)).Returns(new TestAsyncEnumerator<T>(queryable.GetEnumerator()));
        dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new TestAsyncQueryProvider<T>(queryable.Provider));
        dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
        dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
        dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>((s) => source.Add(s));

        return dbSet.Object;
    }

And now the use:

var entityList = new List<entity>() { myEntity };
var dbSet = GetQueryableMockDbSet(entityList);

var context = GetDbContextMock<myDbContext>().Object;
context.MyEntityName = dbSet;
var wrapperRepo = new WrapperRepo(context.Object);

NOTE: You will need the classes (TestAsyncEnumerator, TestAsyncQueryProvider, TestAsyncEnumerable) from the link above: https://jason-ge.medium.com/mock-async-data-repository-in-asp-net-core-3-1-634cb19a3013. If someone wants to add those here, go for it, but it will add about 80 lines to this answer.

If you have a wrapper DbContext around your DbContext, you may need to assign the DbContext that mock, then inject into your repo. In my case I did not have virtual on my entity collections and didn't want to add it.

Upvotes: 1

Kolazomai
Kolazomai

Reputation: 1161

Use the Moq.EntityFrameworkCore package.

It is as easy as:

using Moq.EntityFrameworkCore;

var myDbContextMock = new Mock<MyDbContext>();
var entities = new List<Entity>() { new Entity(), new Entity() };
myDbContextMock.Setup(x => x.Entities).ReturnsDbSet(entities);

Upvotes: 45

setup your dependency injection for the unit test project (dot.net core 5 and xunit 2.4)

1. add a startup.cs file with a class Startup 
2.  public void ConfigureServices(IServiceCollection services)
    {
        var configuration = new ConfigurationBuilder()
      .SetBasePath(System.IO.Directory.GetCurrentDirectory())
      .AddJsonFile("appsettings.Development.json", false, true)
      .Build();
        //setups nlog dependency injection

        services.AddControllers();

        var connectionstring = configuration.GetConnectionString("DbCoreConnectionString");

        services.AddDbContext<ViewpointContext>(options1 => options1.UseSqlServer(connectionString));

        services.AddScoped<IRepositoryDB, RepositoryDB>();

        services.ConfigureLoggerService();

    }

 3. in your xunit class add your dependency injection
      IRepositoryDB _db;
      public TestSuite(ITestOutputHelper output,IRepositoryDB db)
    {
         _db=db;
    }

Upvotes: -1

TanvirArjel
TanvirArjel

Reputation: 32059

I see you are using EF core DbContext in your MovieRepository. So instead of using mock, Using EF Core InMemory database will be a great option for you. This will also reduce the complexity.

Write your GetAllTest() method as follows:

[Fact]
public void GetAllTest()
{
        var options = new DbContextOptionsBuilder<MovieDbContext>()
            .UseInMemoryDatabase(databaseName: "MovieListDatabase")
            .Options;

        // Insert seed data into the database using one instance of the context
        using (var context = new MovieDbContext(options))
        {
            context.Movies.Add(new Movie {Id = 1, Title = "Movie 1", YearOfRelease = 2018, Genre = "Action"});
            context.Movies.Add(new Movie {Id = 2, Title = "Movie 2", YearOfRelease = 2018, Genre = "Action"});
            context.Movies.Add(new Movie {Id = 3, Title = "Movie 3", YearOfRelease = 2019, Genre = "Action"});
            context.SaveChanges();
        }

        // Use a clean instance of the context to run the test
        using (var context = new MovieDbContext(options))
        {
            MovieRepository movieRepository = new MovieRepository(context);
            List<Movies> movies == movieRepository.GetAll();

            Assert.Equal(3, movies.Count);
        }
}

Note: Don't forget to install Microsoft.EntityFrameworkCore.InMemory nuget package as follows:

Install-Package Microsoft.EntityFrameworkCore.InMemory

For more details: Testing with InMemory

Upvotes: 198

Alexei - check Codidact
Alexei - check Codidact

Reputation: 23078

This is a development of R.Titovs answer done in ASP.NET Core 3.1:

Constructing the Moq (generic method)

The data is cloned to allow for tests to run in parallel and prevent a test to access data changed by another.

public static Mock<DbSet<TEnt>> SetDbSetData<TEnt>(this Mock<IApplicationDbContext> dbMock,
        IList<TEnt> list, bool clone = true) 
    where TEnt : class
{
    var clonedList = clone ? list.DeepClone().ToList() : list.ToList();
    var mockDbSet = clonedList.AsQueryable().BuildMockDbSet();

    dbMock.Setup(m => m.Set<TEnt>()).Returns(mockDbSet.Object);
    dbMock.Setup(m => m.ReadSet<TEnt>()).Returns(mockDbSet.Object.AsQueryable());

    return mockDbSet;
}

Using some test data

_appUserDbSetMock = _dbMock.SetDbSetData(ApplicationUserTestData.ApplicationUserData);

Example test

[Fact]
private async Task Handle_ShouldAddANewUser()
{
    var command = new CreateApplicationUserCommand
    {
        // ...
    };

    await _handler.Handle(command, default);

    _appUserDbSetMock.Verify(m => m.AddAsync(It.IsAny<ApplicationUser>(), default), Times.Once);
}

One advantage of using MoqQueryable is that there is no need for a generic repository since DbSet acts like one and the mocking is very easy.

Upvotes: 3

R.Titov
R.Titov

Reputation: 3293

To save your time, try to use my Moq/NSubstitute extension MockQueryable: https://github.com/romantitov/MockQueryable supported all Sync/Async operations

//1 - create a List<T> with test items
var users = new List<UserEntity>()
{
 new UserEntity,
 ...
};

//2 - build mock by extension
var mock = users.AsQueryable().BuildMock();

//3 - setup the mock as Queryable for Moq
_userRepository.Setup(x => x.GetQueryable()).Returns(mock.Object);

//3 - setup the mock as Queryable for NSubstitute
_userRepository.GetQueryable().Returns(mock);

DbSet also supported

//2 - build mock by extension
var mock = users.AsQueryable().BuildMockDbSet();

//3 - setup DbSet for Moq
var userRepository = new TestDbSetRepository(mock.Object);

//3 - setup DbSet for NSubstitute
var userRepository = new TestDbSetRepository(mock);

Note:

  • AutoMapper supported from 1.0.4 ver
  • DbQuery supported from 1.1.0 ver
  • EF Core 3.0 supported from 3.0.0 ver

Upvotes: 30

Michael Brown
Michael Brown

Reputation: 9143

The error you're receiving is because you need to declare the Movies property on your dbcontext as Virtual.

As someone pointed out in the comments, you should use EF's built in memory provider for testing.

Upvotes: 0

Related Questions