我正在使用单元测试来覆盖我们的一些服务类,并且我已经设法使用NSubstitute隔离/伪造dbcontext(遵循this指南)。我做了一些测试并且正在工作,事情似乎没问题,但现在我找不到我添加到上下文中的实体。
测试代码非常简单:
[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);
}
FakeContextBuilder
是我为设置上下文(其他实体的类似方法)所做的辅助类:
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;
}
}
服务方式:
public void CreateStore(int accountID, int someProperty, string someOtherProperty)
{
var account = _context.Accounts.Find(accountID);
account.Stores.Add(new Store(someProperty, someOtherProperty));
}
在Accounts.Find()
行,我得到null
而不是预期的帐户实例。如果我添加一个断点并查看上下文,我看到在“帐户”上“枚举结果”没有产生结果,但我可以看到提供者和枚举器等在非公共成员中正确设置。伪上下文构建器在其他测试中也可以正常工作,所以我的猜测是这与Find()方法有关。
编辑:我现在已经确认Find()方法是罪魁祸首,因为测试通过了这样做:
var account = _context.Accounts.Single(act => act.Id == 10);
我仍然希望将Find()用于缓存目的等等。这可以在测试代码中以某种方式配置吗?不愿意为此搞乱生产代码,因为这真的是一个简单的操作。
答案 0 :(得分:0)
我已经解决了这个问题。它可能不是有史以来最巧妙的解决方案,但似乎可以做到这一点,而且我现在看不到(至少目前)这将是一种维护滋扰。
我通过创建一个DbSet<T>
的子类来完成它,我想象的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());
}
}
然后我只是修改了Substitute.For()调用以使用子类,包含我的自定义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;
}