[英]Manipulating objects with DbSet<T> and IQueryable<T> with NSubstitute returns error
[英]NSubstitute DbSet / IQueryable<T>
所以 EntityFramework 6 比以前的版本更容易測試。 互聯網上有一些不錯的框架示例,如 Moq,但實際情況是,我更喜歡使用 NSubstitute。 我已經將“非查詢”示例翻譯為使用 NSubstitute,但我無法理解“查詢測試”。
起訂量如何items.As<IQueryable<T>>().Setup(m => m.Provider).Returns(data.Provider);
翻譯成 NSubstitute? 我((IQueryable<T>) items).Provider.Returns(data.Provider);
但這沒有用。 我也嘗試過items.AsQueryable().Provider.Returns(data.Provider);
但這也沒有用。
我得到的例外是:
“System.NotImplementedException:成員‘IQueryable.Provider’尚未在
1Proxy' which inherits from 'DbSet
上實現。‘DbSet`1’的測試替身必須提供所使用的方法和屬性的實現。”
因此,讓我引用上面鏈接中的代碼示例。 此代碼示例使用 Moq 模擬 DbContext 和 DbSet。
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = new Mock<DbSet<Blog>>();
mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
var mockContext = new Mock<BloggingContext>();
mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);
// ...
}
這就是我使用 NSubstitute 的成果
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = Substitute.For<DbSet<Blog>>();
// it's the next four lines I don't get to work
((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
((IQueryable<Blog>) mockSet).Expression.Returns(data.Expression);
((IQueryable<Blog>) mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Blog>) mockSet).GetEnumerator().Returns(data.GetEnumerator());
var mockContext = Substitute.For<BloggingContext>();
mockContext.Blogs.Returns(mockSet);
// ...
}
所以問題是; 如何替換 IQueryable 的屬性(如 Provider)?
發生這種情況是因為 NSubstitute 語法特定。 例如在:
((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
NSubstitute 調用 Provider 的 getter,然后指定返回值。 這個 getter 調用不會被替代者攔截,你會得到一個異常。 這是因為在 DbQuery 類中顯式實現了 IQueryable.Provider 屬性。
您可以使用 NSub 顯式創建多個接口的替代品,並且它會創建一個涵蓋所有指定接口的代理。 然后對接口的調用將被替代者攔截。 請使用以下語法:
// Create a substitute for DbSet and IQueryable types:
var mockSet = Substitute.For<DbSet<Blog>, IQueryable<Blog>>();
// And then as you do:
((IQueryable<Blog>) mockSet).Provider.Returns(data.Provider);
((IQueryable<Blog>) mockSet).Expression.Returns(data.Expression);
((IQueryable<Blog>) mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Blog>) mockSet).GetEnumerator().Returns(data.GetEnumerator());
感謝 Kevin,我在代碼翻譯中發現了問題。
unittest 代碼示例DbSet
,但 NSubstitute 需要接口實現。 因此,相當於 NSubstitute 的 Moqs new Mock<DbSet<Blog>>()
是Substitute.For<IDbSet<Blog>>()
。 您並不總是需要提供接口,所以這就是我感到困惑的原因。 但在這個特定的案例中,它變得至關重要。
事實證明,在使用接口 IDbSet 時,我們不必強制轉換為 Queryable。
所以工作測試代碼:
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
}.AsQueryable();
var mockSet = Substitute.For<IDbSet<Blog>>();
mockSet.Provider.Returns(data.Provider);
mockSet.Expression.Returns(data.Expression);
mockSet.ElementType.Returns(data.ElementType);
mockSet.GetEnumerator().Returns(data.GetEnumerator());
var mockContext = Substitute.For<BloggingContext>();
mockContext.Blogs.Returns(mockSet);
// Act and Assert ...
}
我編寫了一個小的擴展方法來清理單元測試的排列部分。
public static class ExtentionMethods
{
public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
{
dbSet.Provider.Returns(data.Provider);
dbSet.Expression.Returns(data.Expression);
dbSet.ElementType.Returns(data.ElementType);
dbSet.GetEnumerator().Returns(data.GetEnumerator());
return dbSet;
}
}
// usage like:
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);
不是問題,但如果您還需要能夠支持異步操作:
public static IDbSet<T> Initialize<T>(this IDbSet<T> dbSet, IQueryable<T> data) where T : class
{
dbSet.Provider.Returns(data.Provider);
dbSet.Expression.Returns(data.Expression);
dbSet.ElementType.Returns(data.ElementType);
dbSet.GetEnumerator().Returns(data.GetEnumerator());
if (dbSet is IDbAsyncEnumerable)
{
((IDbAsyncEnumerable<T>) dbSet).GetAsyncEnumerator()
.Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));
dbSet.Provider.Returns(new TestDbAsyncQueryProvider<T>(data.Provider));
}
return dbSet;
}
// create substitution with async
var mockSet = Substitute.For<IDbSet<Blog>, IDbAsyncEnumerable<Blog>>().Initialize(data);
// create substitution without async
var mockSet = Substitute.For<IDbSet<Blog>>().Initialize(data);
這是我生成假 DbSet 的靜態通用靜態方法。 它可能有用。
public static class CustomTestUtils
{
public static DbSet<T> FakeDbSet<T>(List<T> data) where T : class
{
var _data = data.AsQueryable();
var fakeDbSet = Substitute.For<DbSet<T>, IQueryable<T>>();
((IQueryable<T>)fakeDbSet).Provider.Returns(_data.Provider);
((IQueryable<T>)fakeDbSet).Expression.Returns(_data.Expression);
((IQueryable<T>)fakeDbSet).ElementType.Returns(_data.ElementType);
((IQueryable<T>)fakeDbSet).GetEnumerator().Returns(_data.GetEnumerator());
fakeDbSet.AsNoTracking().Returns(fakeDbSet);
return fakeDbSet;
}
}
大約一年前,我編寫了一個包裝器,其中包含您從Testing with Your Own Test Doubles (EF6 onwards)中引用的相同代碼。 這個包裝器可以在GitHub DbContextMockForUnitTests上找到。 這個包裝器的目的是減少設置單元測試所需的重復/重復代碼的數量,這些單元測試使用 EF 來模擬DbContext
和DbSets
。 您在 OP 中擁有的大多數模擬 EF 代碼可以減少到 2 行代碼(如果您使用DbContext.Set<T>
而不是 DbSet 屬性,則只有 1行),然后在包裝器中調用模擬代碼。
要使用它, MockHelpers
文件夾中的文件復制並包含到您的測試項目中。
以下是使用上面的你的曾經,通知,現在只有2行代碼需要設置模擬的示例測試DbSet<T>
在嘲笑DbContext
。
public void GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
};
var mockContext = Substitute.For<BloggingContext>();
// Create and assign the substituted DbSet
var mockSet = data.GenerateMockDbSet();
mockContext.Blogs.Returns(mockSet);
// act
}
使這個測試調用使用 async/await 模式的.ToListAsync()
如DbSet<T>
上的DbSet<T>
.ToListAsync()
同樣.ToListAsync()
。
public async Task GetAllBlogs_orders_by_name()
{
// Arrange
var data = new List<Blog>
{
new Blog { Name = "BBB" },
new Blog { Name = "ZZZ" },
new Blog { Name = "AAA" },
};
var mockContext = Substitute.For<BloggingContext>();
// Create and assign the substituted DbSet
var mockSet = data.GenerateMockDbSetForAsync(); // only change is the ForAsync version of the method
mockContext.Blogs.Returns(mockSet);
// act
}
您不需要模擬 IQueryable 的所有部分。 當我使用 NSubstitute 來模擬 EF DbContext 時,我會這樣做:
interface IContext
{
IDbSet<Foo> Foos { get; set; }
}
var context = Substitute.For<IContext>();
context.Foos.Returns(new MockDbSet<Foo>());
使用圍繞列表或 MockDbSet() 的 IDbSet 的簡單實現。
一般來說,你應該模擬接口,而不是類型,因為 NSubstitute 只會覆蓋虛擬方法。
當你使用類似的東西時
MyDbContext.CounterpartyDbSet.AsQuariable() // or AsNoTracking()
.bla().bla().bla()
你可以通過簡單的方式走路:
var counterpartyList = new List<Counterparty>()
{
// some items here;
}
var myDbContext = Substitute.For<IMyDbContext>();
var counterpartySet = Substitute.For<DbSet<Counterparty>>();
counterpartySet.AsQueryable() // or AsNoTracking()
.Returns(counterpartyList.AsQueryable());
myDbContext.CounterpartyDbSet.Returns(counterpartySet);
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.