我是一名经验丰富的程序员,但是LINQ / Moq / Ninject / MVC / MS Test /等的新手,遇到了一个我无法弄清楚的问题。
我已经从Pro ASP.NET MVC 2 Framework一书(但使用.NET 4.5 / MVC 4)构建了SportsStore示例。我完成了工作,现在我已经开始将其转换为使用我们的真实数据库。此时的主要区别在于我们不仅拥有Product类,还拥有ProductSub类。每个Product类由一个或多个ProductSub组成,我已经使用EntitySet Association定义了它。为了使CartController知道要添加到Cart的ProductSub,我决定更改CartController.AddToCart以获取productSubId而不是productId。
当我运行网站并手动点击“添加产品”时,一切似乎都能正常工作。但是,当我运行单元测试时,我得到一个NullReferenceException,因为cart.Lines [0]为null。我不认为错误是在CartController中,因为这似乎在我运行网页时起作用,我试图使用FakeProductsRepository(修改为添加ProductSubID)来排除Moq导致这个(这没有帮助,所以我不喜欢我认为这个错误与Moq有什么关系。)
我已经发现CartController中的这一行在单元测试中返回null,但在运行网页时却没有:
productsRepository.ProductSubs.FirstOrDefault(ps => ps.ProductSubID == productSubId);
所以我尝试对CartController进行硬编码,以确定产品的LINQ是否可以正常工作,它确实如此!我认为这意味着productsRepository有Product,但由于某种原因,Product没有ProductSub。到目前为止我是对的吗?
我最好的猜测是单元测试中的代码有问题:
new Product { ProductID = 2, ProductSubs = new List<ProductSub> { new ProductSub { ProductSubID = 456} } }
但我无法弄清楚是什么。使用List是不对的?我尝试使用EntitySet,但它得到了相同的错误。
单元测试代码:
[TestMethod]
public void Can_Add_Product_To_Cart()
{
// Arrange: Give a repository with some products...
var mockProductsRepository = UnitTestHelpers.MockProductsRepository(
new Product { ProductID = 1, ProductSubs = new List<ProductSub> { new ProductSub { ProductSubID = 123 } } },
new Product { ProductID = 2, ProductSubs = new List<ProductSub> { new ProductSub { ProductSubID = 456 } } }
);
var cartController = new CartController(mockProductsRepository, null);
var cart = new Cart();
// Act: When a user adds a product to their cart...
cartController.AddToCart(cart, 456, null);
// Assert: Then the product is in their cart
Assert.AreEqual(1, cart.Lines.Count);
Assert.AreEqual(456, cart.Lines[0].ProductSub.ProductSubID);
}
购物车类:
public class Cart
{
private List<CartLine> lines = new List<CartLine>();
public IList<CartLine> Lines { get { return lines.AsReadOnly(); } }
public void AddItem(ProductSub productSub, int quantity)
{
var line = lines.FirstOrDefault(x => x.ProductSub.ProductSubID == productSub.ProductSubID);
if (line == null)
lines.Add(new CartLine { ProductSub = productSub, Quantity = quantity });
else
line.Quantity += quantity;
}
public decimal ComputeTotalValue()
{
return lines.Sum(l => (decimal)l.ProductSub.Price * l.Quantity);
}
public void Clear()
{
lines.Clear();
}
public void RemoveLine(ProductSub productSub)
{
lines.RemoveAll(l => l.ProductSub.ProductSubID == productSub.ProductSubID);
}
}
public class CartLine
{
public ProductSub ProductSub { get; set; }
public int Quantity { get; set; }
}
产品类别:
[Table]
public class Product
{
[HiddenInput(DisplayValue = false)]
[Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
public int ProductID { get; set; }
[Required(ErrorMessage = "Please enter a product name")]
[Column]
public string Name { get; set; }
[Required(ErrorMessage = "Please enter a description")]
[DataType(DataType.MultilineText)]
[Column(Name = "info")]
public string Description { get; set; }
public float LowestPrice
{
get { return (from product in ProductSubs select product.Price).Min(); }
}
private EntitySet<ProductSub> _ProductSubs = new EntitySet<ProductSub>();
[System.Data.Linq.Mapping.Association(Storage = "_ProductSubs", OtherKey = "ProductID")]
public ICollection<ProductSub> ProductSubs
{
get { return _ProductSubs; }
set { _ProductSubs.Assign(value); }
}
[Required(ErrorMessage = "Please specify a category")]
[Column]
public string Category { get; set; }
}
[Table]
public class ProductSub
{
[HiddenInput(DisplayValue = false)]
[Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
public int ProductSubID { get; set; }
[Column(Name = "products_id")]
private int ProductID;
private EntityRef<Product> _Product = new EntityRef<Product>();
[System.Data.Linq.Mapping.Association(Storage = "_Product", ThisKey = "ProductID")]
public Product Product
{
get { return _Product.Entity; }
set { _Product.Entity = value; }
}
[Column]
public string Name { get; set; }
[Required]
[Range(0.00, double.MaxValue, ErrorMessage = "Please enter a positive price")]
[Column]
public float Price { get; set; }
}
UnitTestHelpers代码(自从我尝试了FakeProductsRepository以来应该没问题):
public static IProductsRepository MockProductsRepository(params Product[] products)
{
var mockProductsRepos = new Mock<IProductsRepository>();
mockProductsRepos.Setup(x => x.Products).Returns(products.AsQueryable());
return mockProductsRepos.Object;
}
CartController代码(因为它适用于网页,应该没问题):
public RedirectToRouteResult AddToCart(Cart cart, int productSubId, string returnUrl)
{
//Product product = productsRepository.Products.FirstOrDefault(p => p.ProductID == 2);
//cart.AddItem(product.ProductSubs.FirstOrDefault(), 1);
ProductSub productSub = productsRepository.ProductSubs.FirstOrDefault(ps => ps.ProductSubID == productSubId);
cart.AddItem(productSub, 1);
return RedirectToAction("Index", new { returnUrl });
}
FakeProductsRepository的代码:
public class FakeProductsRepository : IProductsRepository
{
private static IQueryable<Product> fakeProducts = new List<Product> {
new Product { Name = "Football", ProductSubs = new List<ProductSub> { new ProductSub { ProductSubID = 123, Price = 25 } } },
new Product { Name = "Surf board", ProductSubs = new List<ProductSub> { new ProductSub { ProductSubID = 456, Price = 179 } } },
new Product { Name = "Running shoes", ProductSubs = new List<ProductSub> { new ProductSub { ProductSubID = 789, Price = 95 } } }
}.AsQueryable();
public FakeProductsRepository(params Product[] prods)
{
fakeProducts = new List<Product>(prods).AsQueryable();
}
public IQueryable<Product> Products
{
get { return fakeProducts; }
}
public IQueryable<ProductSub> ProductSubs
{
get { return fakeProducts.SelectMany(ps => ps.ProductSubs); }
}
public void SaveProduct(Product product)
{
throw new NotImplementedException();
}
public void DeleteProduct(Product product)
{
throw new NotImplementedException();
}
}
如果您需要任何其他信息,请与我们联系。
答案 0 :(得分:1)
即使您提供了大量代码,但缺少一些必要的信息,所以我假设IProductsRepository.ProductSubs
返回IQueryable<ProductSub>
。 MockProductsRepository
方法为IProductsRepository
创建了一个模拟,但没有对IProductsRepository.ProductSubs
进行任何设置。模拟框架很可能会返回一个空的IQueryable<ProductSub>
。
在AddToCart
中,您尝试使用ProductSub
找到productsRepository.ProductSubs.FirstOrDefault
。因为mock返回一个空集合FirstOrDefault
将返回null,因此你调用cart.AddItem(null, 1)
来解释为什么cart.Lines[0]
为空。
在修复模拟之前,您可以考虑进行参数验证,例如
public void AddItem(ProductSub productSub, int quantity)
{
if (productSub == null)
throw new ArgumentNullException("productSub");
if (quantity < 1)
throw new ArgumentOutOfRangeException("quantity");
然后,当您重新运行测试时,问题就会更清楚了。
接下来将在IProductsRepository.ProductSubs
中为MockProductsRepository
创建一个设置:
mockProductsRepos
.Setup(x => x.ProductSubs)
.Returns(products.SelectMany(p => p.ProductSubs).AsQueryable());
这只是创建了ProductSub
对象提供给Product
的所有MockProductsRepository
个对象的集合。你当然可以根据需要修改它。
答案 1 :(得分:0)
感谢Martin Liversage,我找到了解决方案。模拟WAS错了,但我没弄明白,因为我的FakeProductsRepository也错了。由于Products和ProductSubs之间的依赖关系,我认为他对模拟的建议改变不会起作用(但如果我错了,请纠正我)。
FakeProductsRepository中的问题是构造函数用空集合覆盖了最初的fakeProducts集合。一旦我将其更改为仅覆盖初始集合,如果提供了一个新集合作为参数,则单元测试使用FakeProductsRepository工作。
public FakeProductsRepository(params Product[] products)
{
if (products != null)
fakeProducts = new List<Product>(products).AsQueryable();
}
因此,模拟存在问题,因为它仍然无效。要解决这个问题,我需要做的就是从IProductsRepository中移除ProductSubs函数(我原本打算将其作为一种快捷方式,但我意识到它搞砸了模拟)。一旦我这样做并通过CartController中的Products访问了ProductSubs,一切都恢复了。
public RedirectToRouteResult AddToCart(Cart cart, int productSubId, string returnUrl)
{
ProductSub productSub = productsRepository.Products.SelectMany(p => p.ProductSubs).FirstOrDefault(ps => ps.ProductSubID == productSubId);
cart.AddItem(productSub, 1);
return RedirectToAction("Index", new { returnUrl });
}
这就是我所需要的,但为了简化测试代码,我还决定使用纯ProductSub对象,而不是通过Product访问它们。我需要整个产品的地方(即当涉及到IProductsRepository时,我使用了这个代码,我认为它更干净,然后在一行上创建整个对象(即使用新的List等):
var ps1 = new ProductSub { ProductSubID = 11 };
var p1 = new Product();
p1.ProductSubs.Add(ps1);