1

私は経験豊富なプログラマーですが、LINQ / Moq / Ninject / MVC / MS Test / etcに不慣れで、理解できない問題に遭遇しました。

私はProASP.NETMVC 2 FrameworkブックからSportsStoreサンプルを作成しました(ただし、.NET 4.5 / MVC 4を使用)。私はそれを機能させ、今では実際のデータベースで機能するように変換し始めました。この時点での主な違いは、Productクラスだけでなく、ProductSubクラスもあることです。各Productクラスは1つ以上のProductSubで構成されており、これをEntitySetアソシエーションで定義しました。CartControllerにカートに追加するProductSubを認識させるために、CartController.AddToCartを変更してproductIdではなくproductSubIdを取得することにしました。

Webサイトを実行し、手動で[製品の追加]をクリックすると、すべてが正常に機能しているようです。ただし、単体テストを実行すると、cart.Lines [0]がnullであるため、NullReferenceExceptionが発生します。CartControllerにエラーがあるとは思わないので、Webページを実行すると機能するようです。また、FakeProductsRepository(ProductSubIDを追加するように変更)を使用して、これを引き起こすMoqを除外しようとしました(これは役に立たなかったので、私はしませんエラーがMoqと関係があるとは思わない)。

CartControllerのこの行は、単体テストではnullを返しますが、Webページを実行するとnullを返すことがわかりました。

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

そこで、CartControllerをハードコーディングして、代わりにLINQ totheProductが機能するかどうかを確認しました。つまり、productsRepositoryにはProductがありますが、何らかの理由でProductにはProductSubがありません。私は今のところ正しいですか?

私の最善の推測は、単体テストのこのコードに何か問題があることです。

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

しかし、私は何を理解することはできません。リストを使用するのは間違っていますか?代わりに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コード(Webページで機能するので問題ありません):

    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を見つけようとします。モックは空のコレクションを返すため、 nullを返します。したがって、nullである理由を説明する呼び出しを行います。ProductSubproductsRepository.ProductSubs.FirstOrDefaultFirstOrDefaultcart.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たオブジェクトからすべてのオブジェクトのコレクションを作成するだけです。もちろん、必要に応じてこれを変更できます。ProductMockProductsRepository

于 2012-09-12T15:47:27.213 に答える
0

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);
于 2012-09-13T08:34:57.117 に答える