Sina Iravanian

I blog about software development

Mock Entity Framework DbSet With NSubstitute

| Comments

The source code for this post is on GitHub.

When it comes to mocking a DbSet to test various read and write operations things may get a little bit tricky. Mocking a DbSet for a write operation (Add, Update, or Remove) is quite straightforward. But testing a synchronous read operation requires mocking the whole IQueryable interface. Even more trickier than that is testing an asynchronous read operation which requires mocking an IDbAsyncQueryProvider interface. Here is a very good article that demonstrates how to work around these difficulties using Moq. This blog post shows how to achieve the same thing using NSubstitute.

Mocking a DbSet for a write operation is quite straightforward:

1
2
3
4
5
6
7
8
9
10
11
var mockSet = Substitute.For<DbSet<Person>>();
var mockContext = Substitute.For<IPeopleDbContext>();
mockContext.People.Returns(mockSet);
var service = new PeopleService(mockContext);

// Act
service.AddPersonAsync(new Person { FirstName = "John", LastName = "Doe" });

// Assert
mockSet.Received(1).Add(Arg.Any<Person>());
mockContext.Received(1).SaveChangesAsync();

As seen above mocking the DbSet is as easy as one single call to Substitute.For<DbSet<T>>().

However mocking a DbSet for a synchronous read operation needs a little bit more work. It needs to mock the whole IQueryable interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var data = new List<Person>
{
    new Person { Id = 1, FirstName = "BBB" },
    new Person { Id = 2, FirstName = "ZZZ" },
    new Person { Id = 3, FirstName = "AAA" },
}.AsQueryable();

// create a mock DbSet exposing both DbSet and IQueryable interfaces for setup
var mockSet = Substitute.For<DbSet<Person>, IQueryable<Person>>();

// setup all IQueryable methods using what you have from "data"
((IQueryable<Person>)mockSet).Provider.Returns(data.Provider);
((IQueryable<Person>)mockSet).Expression.Returns(data.Expression);
((IQueryable<Person>)mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Person>)mockSet).GetEnumerator().Returns(data.GetEnumerator());

// do the wiring between DbContext and DbSet
var mockContext = Substitute.For<IPeopleDbContext>();
mockContext.People.Returns(mockSet);
var service = new PeopleService(mockContext);

// Act
var people = service.GetAllPeople();

// Assert
Assert.That(people.Length, Is.EqualTo(3));
Assert.That(people[0].FirstName, Is.EqualTo("BBB"));
Assert.That(people[1].FirstName, Is.EqualTo("ZZZ"));
Assert.That(people[2].FirstName, Is.EqualTo("AAA"));

Mocking a DbSet for an asynchronous read operation requires much more work and a bunch of extra classes. This article lists the required extra classes. To name add TestDbAsyncEnumerable, TestDbAsyncEnumerator, and TestDbAsyncQueryProvider classes to your test project. This is how you’d be able to mock DbSet for asynchronous read operations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var data = new List<Person>
{
    new Person { Id = 1, FirstName = "BBB" },
    new Person { Id = 2, FirstName = "ZZZ" },
    new Person { Id = 3, FirstName = "AAA" },
}.AsQueryable();

// create a mock DbSet exposing both DbSet, IQueryable, and IDbAsyncEnumerable interfaces for setup
var mockSet = Substitute.For<DbSet<Person>, IQueryable<Person>, IDbAsyncEnumerable<Person>>();

// setup all IQueryable and IDbAsyncEnumerable methods using what you have from "data"
// the setup below is a bit different from the test above
((IDbAsyncEnumerable<Person>)mockSet).GetAsyncEnumerator()
    .Returns(new TestDbAsyncEnumerator<Person>(data.GetEnumerator()));
((IQueryable<Person>)mockSet).Provider.Returns(new TestDbAsyncQueryProvider<Person>(data.Provider));
((IQueryable<Person>)mockSet).Expression.Returns(data.Expression);
((IQueryable<Person>)mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Person>)mockSet).GetEnumerator().Returns(data.GetEnumerator());

// do the wiring between DbContext and DbSet
var mockContext = Substitute.For<IPeopleDbContext>();
mockContext.People.Returns(mockSet);
var service = new PeopleService(mockContext);

// Act
var people = await service.GetAllPeopleAsync();

// Assert
Assert.That(people.Length, Is.EqualTo(3));

The amount of code per unit test seems a little bit too much. So I created a utility method that creates the mock DbSet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class NSubstituteUtils
{
    public static DbSet<T> CreateMockDbSet<T>(IEnumerable<T> data = null)
        where T: class
    {
        var mockSet = Substitute.For<DbSet<T>, IQueryable<T>, IDbAsyncEnumerable<T>>();

        if (data != null)
        {
            var queryable = data.AsQueryable();

            // setup all IQueryable and IDbAsyncEnumerable methods using what you have from "data"
            // the setup below is a bit different from the test above
            ((IDbAsyncEnumerable<T>) mockSet).GetAsyncEnumerator()
                .Returns(new TestDbAsyncEnumerator<T>(queryable.GetEnumerator()));
            ((IQueryable<T>) mockSet).Provider.Returns(new TestDbAsyncQueryProvider<T>(queryable.Provider));
            ((IQueryable<T>) mockSet).Expression.Returns(queryable.Expression);
            ((IQueryable<T>) mockSet).ElementType.Returns(queryable.ElementType);
            ((IQueryable<T>) mockSet).GetEnumerator().Returns(queryable.GetEnumerator());
        }

        return mockSet;
    }
}

Exploiting this, the unit test code would become shorter and more readable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var data = new List<Person>
{
    new Person { Id = 1, FirstName = "BBB" },
    new Person { Id = 2, FirstName = "ZZZ" },
    new Person { Id = 3, FirstName = "AAA" },
};

var mockSet = NSubstituteUtils.CreateMockDbSet(data);
var mockContext = Substitute.For<IPeopleDbContext>();
mockContext.People.Returns(mockSet);
var service = new PeopleService(mockContext);

// Act
var secondPerson = await service.GetPersonAsync(2);

// Assert
Assert.That(secondPerson.Id, Is.EqualTo(2));

The source code for this post is accessible from here.

Comments