0

I'm in the middle of covering some of our service classes with unit tests and I have managed to isolate/fake the dbcontext using NSubstitute (following this guide). I have some tests done and working, and things seemed to be alright, but now I can't find an entity I added to the context.

The test code is pretty straightforward:

[Fact]
public void CreateStore_GivenAccount_AccountIsAssignedTheStore()
{
    const int accountId = 10;
    var account = new Account {Id = accountId};
    var fakeContext = new FakeContextBuilder()
        .WithAccounts(account)
        .Build();
    var service = new Service(fakeContext);
    const int someProperty = 0;
    const string someOtherProperty = "blabla";

    service.CreateStore(accountId, someProperty, someOtherProperty);

    var storeWasAdded = account.Stores
        .Any(store =>
            store.SomeProperty == someProperty &&
            store.SomeOtherProperty == someOtherProperty);
    Assert.True(storeWasAdded);
}

The FakeContextBuilder is a helper class I made for setting up the context (similar methods for other entities):

public class FakeContextBuilder
{
    private DbSet<Account> _accountTable;

    private static DbSet<TEntity> SetUpFakeTable<TEntity>(params TEntity[] entities) where TEntity : class
    {
        var fakeTable = Substitute.For<DbSet<TEntity>, IQueryable<TEntity>>() as IQueryable<TEntity>;
        var table = entities.AsQueryable();
        fakeTable.Provider.Returns(table.Provider);
        fakeTable.Expression.Returns(table.Expression);
        fakeTable.ElementType.Returns(table.ElementType);
        fakeTable.GetEnumerator().Returns(table.GetEnumerator());
        return (DbSet<TEntity>) fakeTable;
    }

    public Context Build()
    {
        var context = Substitute.For<Context>();
        context.Accounts.Returns(_accountTable);
        return context;
    }

    public FakeContextBuilder WithAccounts(params Account[] accounts)
    {
        _accountTable = SetUpFakeTable(accounts);
        return this;
    }
}

Service method:

public void CreateStore(int accountID, int someProperty, string someOtherProperty)
{
    var account = _context.Accounts.Find(accountID);
    account.Stores.Add(new Store(someProperty, someOtherProperty));
}

On the Accounts.Find() row I get null instead of the expected account instance. If I add a breakpoint and look at the context I see that "enumerate results" on Accounts yields no results, but I can see that the provider and enumerator etc are set correctly in non-public members. The fake context builder also works fine in other tests, so my guess is that this is related to the Find() method.

EDIT: I have now confirmed that the Find() method is the culprit since the test passes when doing this instead:

var account = _context.Accounts.Single(act => act.Id == 10);

I still want to use Find() for caching purposes and so on. Can this be configured in the test code somehow? Would hate to mess up the production code for this, since it's really a simple operation.

sara
  • 3,521
  • 14
  • 34
  • It seems like the problem is stemming from the fact that DbSet.Find() is `virtual`, and so NSubstitute is overriding it with a null implementation. Will investigate further. – sara Dec 22 '15 at 12:55

1 Answers1

0

I have solved the problem. It might not be the most neat solution ever, but it seems to do the trick, and I can't see (at the moment at least) that it would be a maintenance nuisance later on.

I pulled it off by creating a sub-class of DbSet<T> which I imaginatively enough named DbSetWithFind<T>

public class DbSetWithFind<TEntity> : DbSet<TEntity> where TEntity : class
{
    private readonly IQueryable<TEntity> _dataSource;

    public DbSetWithFind(IQueryable<TEntity> dataSource)
    {
        _dataSource = dataSource;
    }

    public sealed override TEntity Find(params object[] keyValues) // sealed override prevents EF from "ruining" it.
    {
        var keyProperties = typeof (TEntity).GetProperties()
            .Where(property => property.IsDefined(typeof (KeyAttribute), true));
        return _dataSource.SingleOrDefault(entity =>
            keyProperties
                .Select(property => property.GetValue(entity))
                .Intersect(keyValues)
                .Any());
    }
}

Then I just modified the Substitute.For() call to use the sub-class, containing my custom implementation of Find().

private static DbSet<TEntity> SetUpFakeTable<TEntity>(params TEntity[] entities) where TEntity : class
{
    var dataSource = entities.AsQueryable();
    var fakeDbSet = Substitute.For<DbSetWithFind<TEntity>, IQueryable<TEntity>>(dataSource); // changed type and added constructor params
    var fakeTable = (IQueryable<TEntity>) fakeDbSet;
    fakeTable.Provider.Returns(dataSource.Provider);
    fakeTable.Expression.Returns(dataSource.Expression);
    fakeTable.ElementType.Returns(dataSource.ElementType);
    fakeTable.GetEnumerator().Returns(dataSource.GetEnumerator());

    return (DbSet<TEntity>) fakeTable;
}
sara
  • 3,521
  • 14
  • 34