1

我是一位经验丰富的程序员,但对 LINQ/Moq/Ninject/MVC/MS Test/etc 不熟悉,并且遇到了一个我无法弄清楚的问题。

我从 Pro ASP.NET MVC 2 Framework 书(但使用 .NET 4.5/MVC 4)构建了 SportsStore 示例。我得到了这个工作,现在我已经开始将它转换为与我们的真实数据库一起工作。此时的主要区别在于,我们不仅有一个 Product 类,还有一个 ProductSub 类。每个产品类由 1 个或多个 ProductSub 组成,我已经用 EntitySet 关联定义了它。为了让 CartController 知道要将哪个 ProductSub 添加到购物车中,我决定将 CartController.AddToCart 更改为使用 productSubId 而不是 productId。

当我运行网站并手动单击“添加产品”时,一切似乎都正常。但是,当我运行单元测试时,我得到一个 NullReferenceException,因为 cart.Lines[0] 为空。我不认为错误出现在 CartController 中,因为当我运行网页时这似乎有效,我尝试使用 FakeProductsRepository(修改为添加 ProductSubID)来排除 Moq 导致此问题(没有帮助,所以我不'认为错误与起订量无关)。

我发现 CartController 中的这一行在单元测试中返回 null 但在我运行网页时没有:

productsRepository.ProductSubs.FirstOrDefault(ps => ps.ProductSubID == productSubId);

因此,我尝试对 CartController 进行硬编码,以查看 LINQ to Product 是否可以正常工作,它确实做到了!我认为这意味着 productsRepository 有产品,但由于某种原因,产品没有 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();
    }
}

如果您需要任何其他信息,请告诉我。

4

2 回答 2

1

即使您提供了很多代码,也缺少一些必要的信息,所以我假设IProductsRepository.ProductSubs返回IQueryable<ProductSub>。该MockProductsRepository方法创建一个模拟,IProductsRepository但不做任何设置IProductsRepository.ProductSubs。模拟框架很可能会返回一个空的IQueryable<ProductSub>.

AddToCart您尝试找到ProductSubusing productsRepository.ProductSubs.FirstOrDefault. 因为模拟返回一个空集合FirstOrDefault将返回 null,因此您调用cart.AddItem(null, 1)它解释了为什么cart.Lines[0]为 null。

在修复模拟之前,您可以考虑进行参数验证,例如

public void AddItem(ProductSub productSub, int quantity) 
{ 
    if (productSub == null)
        throw new ArgumentNullException("productSub");
    if (quantity < 1)
        throw new ArgumentOutOfRangeException("quantity");

然后,当您重新运行测试时,您的问题出在哪里会更清楚。

接下来就是为IProductsRepository.ProductSubsin创建一个设置MockProductsRepository

mockProductsRepos
  .Setup(x => x.ProductSubs)
  .Returns(products.SelectMany(p => p.ProductSubs).AsQueryable());

这只是从提供给的对象中创建所有ProductSub对象的集合。您当然可以根据需要进行修改。ProductMockProductsRepository

于 2012-09-12T15:47:27.213 回答
0

感谢 Martin Liversage,我找到了解决方案。模拟错了,但我没弄明白,因为我的 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 时,我使用了我认为更简洁的代码,然后在一行上创建整个对象(即使用新列表等):

var ps1 = new ProductSub { ProductSubID = 11 };
var p1 = new Product();
p1.ProductSubs.Add(ps1);
于 2012-09-13T08:34:57.117 回答