我对EntitySet的嘲弄错了吗?

时间:2012-09-12 15:31:18

标签: asp.net-mvc linq moq mstest entityset

我是一名经验丰富的程序员,但是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();
    }
}

如果您需要任何其他信息,请与我们联系。

2 个答案:

答案 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);