Domanda

I'm an experienced programmer, but new to LINQ/Moq/Ninject/MVC/MS Test/etc and have run into an issue I haven't been able to figure out.

I have built the SportsStore sample from the Pro ASP.NET MVC 2 Framework book (but with .NET 4.5/MVC 4). I got that working and now I've begun to convert it to work with our real database. The main difference at this point is that we do not only have a Product class, but also a ProductSub class. Each Product class consists of 1 or more ProductSub's and I have defined this with an EntitySet Association. To make the CartController to know which ProductSub to add to the Cart I decided to change CartController.AddToCart to take a productSubId instead of a productId.

Everything seems to work fine when I run the website and manually click "add product". However, when I run my unit tests I get a NullReferenceException because cart.Lines[0] is null. I don't think the error is in CartController since that seems to work when I run the webpage, and I tried to use the FakeProductsRepository (modified to add ProductSubID's) to rule out Moq causing this (which didn't help, so I don't think the error has anything to do with Moq).

I've figured out that this line in CartController returns null in the unit test but not when I run the webpage:

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

So I tried to hard code the CartController to see if LINQ to the Product instead would work, which it did! I think that means that the productsRepository have Product's, but that for some reason the Product's doesn't have a ProductSub's. I'm I right so far?

My best guess is that there's something wrong with this code in the unit test:

new Product { ProductID = 2, ProductSubs = new List<ProductSub> { new ProductSub { ProductSubID = 456} } }

But I can't figure out what. Is it wrong to use List? I tried using EntitySet instead but it made got the same error.

Unit test code:

    [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);
    }

Cart class:

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; }
}

Product class:

[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 code (which should be fine since I tried the 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 code (which should be fine since it works on the webpage):

    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 });
    }

Code for 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();
    }
}

Please let me know if you need any other information.

È stato utile?

Soluzione 2

I figured out the solution thanks to Martin Liversage. The mock WAS wrong, but I didn't figure it out because my FakeProductsRepository was ALSO wrong. Due to the dependency between Products and ProductSubs I don't think his suggested change to the mock would work though (but please correct me if I'm wrong).

The issue in FakeProductsRepository was that the constructor overwrote the initial fakeProducts collection with an empty collection. Once I changed that to only overwrite the initial collection if a new collection was supplied as parameter the unit tests worked using the FakeProductsRepository.

    public FakeProductsRepository(params Product[] products)
    {
        if (products != null)
            fakeProducts = new List<Product>(products).AsQueryable();
    }

Thus there was an issue with the mock since that still didn't work. To solve it all I needed to do was to remove the ProductSubs function from IProductsRepository (which I had intended as a shortcut, but which I realized messed up the mocking). Once I did that and accessed the ProductSubs through the Products in CartController everything worked again.

    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 });
    }

That was all I needed, but to simplify the test code I also decided to use pure ProductSub objects where that was enough instead of accessing them through a Product. Where I needed the whole Product (ie when the IProductsRepository was involved I used this code which I think is cleaner then creating the whole object on one line (ie with new List etc):

var ps1 = new ProductSub { ProductSubID = 11 };
var p1 = new Product();
p1.ProductSubs.Add(ps1);

Altri suggerimenti

Even though you have provided a lot of code some necessary information is missing so I'm assuming that IProductsRepository.ProductSubs returns IQueryable<ProductSub>. The MockProductsRepository method creates a mock for IProductsRepository but does not do any setup for IProductsRepository.ProductSubs. The mocking framework will most likely return an empty IQueryable<ProductSub>.

In the AddToCart you try to find the ProductSub using productsRepository.ProductSubs.FirstOrDefault. Because the mock returns an empty collection FirstOrDefault will return null thus you call cart.AddItem(null, 1) which explains why cart.Lines[0] is null.

Before fixing the mock you could consider doing parameter validation, e.g.

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

Then when you rerun your test it will be much clearer where your problem is.

Next thing will then be to create a setup for IProductsRepository.ProductSubs in MockProductsRepository:

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

This simply creates a collection of all the ProductSub objects from the Product objects provided to MockProductsRepository. You can of course modify this as you see fit.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top