【问题标题】:Is there a way to Mock (using Moq ) a service within a service and execute the code?有没有办法在服务中模拟(使用 Moq )服务并执行代码?
【发布时间】:2021-02-15 17:02:35
【问题描述】:

我有一个使用 Moq 的测试问题。我到处都看了,但我不太明白到底发生了什么。

我想测试一个名为 Basket 的类(服务),在该类中我作为 DI 传递另一个类(服务)DiscountService。

我面临的问题是,每当我模拟 Basket 类时,它都会很好地触发,但是当它到达在 DiscountService 中实际执行另一个方法的代码时,它只是跳过该方法而不是进入。我做错了吗?

我认为可能是我没有正确进行设置?

这是我目前的实际测试代码:

 [Test]
        public async System.Threading.Tasks.Task Test1Async()
        {
            //Arrange
            var basketServiceMock = new Mock<IBasket>();
            var discountServiceMock = new Mock<IDiscountService>();

            List<BasketProductModel> productsModel = 
                new List<BasketProductModel> 
                { 
                    new BasketProductModel()
                    {
                        ProductName = "Milk",
                        ProductPrice = 10,
                        Quantity = 3
                    }
                };
            BasketModel basket = new BasketModel { Products = productsModel };
            basketServiceMock.Setup(x => x.Baskett)
                .Returns(new BasketModel { Products = productsModel });

            var product = new BasketProductModel()
            {
                ProductName = "Milk",
                ProductPrice = 10,
                Quantity = 1
            };
            var product1 = new BasketProductModel()
            {
                ProductName = "Milk",
                ProductPrice = 10,
                Quantity = 1
            };
            var product2 = new BasketProductModel()
            {
                ProductName = "Milk",
                ProductPrice = 10,
                Quantity = 1
            };
            var product3 = new BasketProductModel()
            {
                ProductName = "Milk",
                ProductPrice = 10,
                Quantity = 1
            };

            discountServiceMock.Setup(x => x.ApplyDiscount(ref basket, product));
            var basketService = new Basket.Basket(discountServiceMock.Object);


            //Act
            await basketService.AddProductToBasket(product);
            await basketService.AddProductToBasket(product1);
            await basketService.AddProductToBasket(product2);
            await basketService.AddProductToBasket(product3);

            //Assert

        }

这是我的篮子课:

       public class Basket : IBasket
    {
        private BasketModel _basketProducts;
        private double _totalCost;
        public BasketModel Baskett { 
            get {
                if (_basketProducts == null)
                    throw new ArgumentNullException("xxxxxxxxxx");

                return _basketProducts;
            } 
        }

        public double TotalCost
        {
            get
            {
                if (_totalCost == null)
                {
                    throw new ArgumentNullException("xxxxxxxxxx");
                }

                return _totalCost;
            }
        }

        private readonly IDiscountService _discountService;

        public Basket(IDiscountService discountService)
        {
            _discountService = discountService;

            _basketProducts = new BasketModel();
            _basketProducts.Products = new List<BasketProductModel>();
        }

        public async Task AddProductToBasket(BasketProductModel product)
        {
            if (product == null)
                throw new ArgumentNullException();


            var obj = _basketProducts.Products.FirstOrDefault(x => x.ProductName == product.ProductName);
            if (obj == null)
                _basketProducts.Products.Add(product);
            else
            {
                obj.Quantity += product.Quantity;
            }
            _discountService.ApplyDiscount(ref _basketProducts, product); <---This part is being skipped, do I miss something in the SETUP part?!

        }


    }

这是我的未触发的 DiscountService 类(ApplyDiscount 方法)

       public class DiscountService : IDiscountService
    {

        public static readonly ReadOnlyCollection<string> discountedProducts = new List<String> {
                                                                                            "Milk",
                                                                                            "Butter",
                                                                                            "Bread",
        }.AsReadOnly();

        public void ApplyDiscount(ref BasketModel basket, BasketProductModel product)
        {
            if (discountedProducts.Contains(product.ProductName) && basket.Products.Count > 0)
                product.CalculateDiscount(basket);
        }

    }

这是 BasketProductModel(篮子里的产品)

    public class BasketProductModel : DiscountProcess
    {
        public BasketProductModel()
        {
            Init(this);
        }

        public string ProductName { get; set; }
        public double ProductPrice { get; set; }
        public int Quantity;
        public int Freebies { get; set; }
        public double Total { get; set; }
    }

这是上面类中使用的抽象类,用于使用一些预先配置的实现


    public abstract class DiscountProcess
    {
        private BasketProductModel _obj { get; set; }
        public BasketProductModel Obj
        {
            get
            {    // check _obj is inited:
                if (_obj == null) throw new Exception();
                return _obj;
            }
        }

        protected void Init(BasketProductModel bPModel)
        {
            _obj = bPModel;
        }

        public void CalculateDiscount(BasketModel basket)
        {
            var productContained = NumberOfProductsInBasket(ref basket, _obj);
            var obj = basket.Products.FirstOrDefault(x => x.ProductName == _obj.ProductName);

            var freeItems = 0;
            switch (_obj.ProductName)
            {
                case "Milk":
                    if (productContained % 2 != 0)
                    {
                        obj.Freebies = numberOfFreeItems(obj.Quantity);
                        //AddQuantity(ref obj);
                    }
                    break;
                case "Butter":

                    break;
                case "Bread":
                    productContained = NumberOfProductsInBasket(ref basket, productName: "Butter");
                    if (productContained % 2 == 0)
                    {
                        CalculateDiscountedPrice(50, ref obj);
                    }
                    break;
                default:
                    break;
            }

            CalculateTotal(ref obj);
        }

        //...Other Methods...
    }

所以基本上我希望能够访问服务内的代码并执行所有代码,以便我可以断言篮子:)

知道如何实现这一点吗?

最终目标:

我希望Baskett 能够根据 DiscountService 对 ref 传递的 basked 所做的事情来获取数据,以便我可以在最后进行断言。

【问题讨论】:

  • 为什么两个类要相互注入? (循环依赖)。哪一个被认为是被测对象?
  • @Nkosi 好点,我ve spotted it as well...and Im 打算重构它,但是,被测对象应该是 basked,我们在里面注入了 discountService...(忽略里面的依赖DiscountService 是篮子)
  • @Nkosi 我已经用一些新信息更新了这个问题 + 我重构了循环依赖:) 你介意随时看看吗?
  • 你在嘲笑DiscountService...为什么你会期望调用实际服务的方法?
  • @IanKemp 好问题,这就是为什么我想我在这里,从我的错误中吸取教训。我想我不明白发生了什么,所以我需要一些帮助:)

标签: c# unit-testing integration-testing moq


【解决方案1】:

如果Basket 是被测对象,则不应嘲笑它。

//...

//Arrange
var basketServiceMock = new Mock<IBasket>(); //<-- Should not be mocked
var discountServiceMock = new Mock<IDiscountService>();

//...

改为使用实际实例,

var discountServiceMock = new Mock<IDiscountService>();
Basket basketServiceMock = new Basket(discountServiceMock .Object);

//...

并模拟完成测试用例所需的依赖项。

在这种情况下,主题会在您尝试在测试中设置的构造函数中初始化模型。由于模型正在由主题类初始化,因此无需尝试在测试中创建一个。只需根据测试需要填充它即可。

查看以下内容

[Test]
public async System.Threading.Tasks.Task Test1Async() {
    //Arrange
    
    var discountServiceMock = new Mock<IDiscountService>();

    Basket subject = new Basket(discountServiceMock.Object);

    List<BasketProductModel> productsModel = new List<BasketProductModel> { 
        new BasketProductModel() {
            ProductName = "Milk",
            ProductPrice = 10,
            Quantity = 3
        }
    };
    subject.Baskett.Products =  productsModel; 
    
    // Only matches if the ref argument to the invocation is the same instance
    discountServiceMock
        .Setup(x => x.ApplyDiscount(ref subject.Baskett, It.IsAny<BasketProductModel>()));
        //You need to decide what you want the mocked member to do.
    
    var product = new BasketProductModel() {
        ProductName = "Milk",
        ProductPrice = 10,
        Quantity = 1
    };
    var product1 = new BasketProductModel() {
        ProductName = "Milk",
        ProductPrice = 10,
        Quantity = 1
    };
    var product2 = new BasketProductModel() {
        ProductName = "Milk",
        ProductPrice = 10,
        Quantity = 1
    };
    var product3 = new BasketProductModel() {
        ProductName = "Milk",
        ProductPrice = 10,
        Quantity = 1
    };

    //Act
    await subject.AddProductToBasket(product);
    await subject.AddProductToBasket(product1);
    await subject.AddProductToBasket(product2);
    await subject.AddProductToBasket(product3);

    //Assert
    //...You need to decide what it is you are actually testing
    //and want to assert

}

观察:

被测成员定义为async,但该成员实际上没有等待任何内容。重新检查该成员的设计。

【讨论】:

  • 谢谢你,我也修改了异步的东西,我等待它。但是,我仍然有同样的问题,在 Basket 类中,我有一个名为 AddProductToBasket 的方法,它进入了,但问题是如果我尝试进入它并查看那里发生了什么,它是不进入,而是跳过它(转到下一行而不是在该方法中调试)ApplyDiscount。有什么想法可以让我们进入调试方法吗? :)
  • @CsibiNorbert 在AddProductToBasket 方法中设置了一个断点。当您尝试介入时,它应该会到达断点
  • 我想你错过了我所说的,断点在AddProductToBasket 内命中。但是,当它到达这个方法_discountService.ApplyDiscount 并且我尝试获取它时,它不会进入内部,这导致Baskett 不会被ApplyDiscount 方法所做的任何更新。 :)
  • @CsibiNorbert 哦,好的,我明白了。没有什么可进入的。 ApplyDiscount 是一个被嘲笑的成员。这不是实际的实现。依赖项被模拟并注入到被测对象中。
  • @CsibiNorbert 是的,在这种情况下是正确的。
猜你喜欢
  • 2021-11-01
  • 2020-12-07
  • 1970-01-01
  • 1970-01-01
  • 2019-11-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-03
  • 1970-01-01
相关资源
最近更新 更多