使用Moq实现单元测试

先开个头,当我们对A进行单元测试时,可能会发现A的实现必须要依赖B。这时,我们在写单元测试时,就必须先创建B的实例,然后把B传给A再建立A的实例进行测试。

这样就会出现一些问题:

1、我们的单元测试会变得复杂而且脆弱。复杂是因为我们必须要花费精力去弄清楚B的逻辑。脆弱是因为如果B的逻辑更改了,我们对A的单元测试也可能会面临失败。

2、更严重的是,当我们测试失败时,我们无法很快定位到究竟是A除了问题还是B出了问题。

 

所以我们使用Moq这种技术来Mock “伪造” 一个B的实例,这样我们就能专注于对A的单元测试。

接下来开始记录一下使用Moq的案例。

 

首先了解一下不使用Moq的情况下我们怎么测试一个跟其他类有依赖关系的方法。

 

1、这里先声明了一个产品实体。其中有产品的名称、种类、和价格。

  public class Product
    {
        public string Name { set; get; }
        public string Category { get; set; }
        public decimal Price { set; get; }
    }

 

2、我们有一个接口IValueCalculator,声明了一个方法来计算产品价格。

 public interface IValueCalculator
    {
        decimal ValueProducts(IEnumerable<Product> products);
    }

 

3、还需要定义一个接口IDiscountHelper来给产品的价格打折。

 public interface IDiscountHelper
    {
        decimal GetDiscount(decimal price);
    }

  有一个实现这个接口的MinDiscountHelper 类,根据不同的价格范围进行打折。

 public class MinDiscountHelper : IDiscountHelper
    {

        public decimal GetDiscount(decimal price)
        {
            if (price < 0)
            {
                throw new ArgumentOutOfRangeException();
            }
            else if (price > 10 && price <= 100)
            {
                return price - 5;
            }
            else if (price > 100)
            {
                return price * 0.9M;
            }
            else
            {
                return price;
            }
        }
    }

 

4、接下来定义一个LinqValueCalculator 类来实现接口IValueCalculator。

  我们可以发现这个类要依赖于IDiscountHelper接口的实现来计算打折后的价格,然后实现IValueCalculator的ValueProducts()方法返回最终的产品价格。

 public class LinqValueCalculator : IValueCalculator
    {
        private IDiscountHelper discounter;

        public LinqValueCalculator(IDiscountHelper discountPara)
        {
            this.discounter = discountPara;
        }

        public decimal ValueProducts(IEnumerable<Product> products)
        {
            return this.discounter.GetDiscount(products.Sum(p => p.Price));
        }
    }

 

5、如此一来,我们要测试LinqValueCalculator的方法时,就不得不先定义一个IDiscountHelper的实例。

这就会出现我们一开始所说的问题。

[TestClass]
 public class UnitTest2 {
     private Product[] products = {
       new Product {Name = "AAA", Price = 275M},
       new Product {Name = "BBB", Price = 48.95M},
       new Product {Name = "CCC", Price = 19.50M},
       new Product {Name = "DDD", Price = 34.95M}
     };
 
   [TestMethod]
   public void Sum_Products_Correctly() {
     // arrange
     var discounter = new MinimumDiscountHelper();
     var target = new LinqValueCalculator(discounter);
     var goalTotal = products.Sum(e => e.Price);
     // act
     var result = target.ValueProducts(products);
     // assert
     Assert.AreEqual(goalTotal, result);
   }
 }

 

接下来我们使用Moq来解决这种问题,让我们可以专注于我们想要测试的模块。

1、在单元测试项目中打开NuGet程序包管理。

 

2、在右侧联机搜索Moq然后安装识别码为Moq的程序包即可。

 

3、可以看到Moq被引用到了单元测试项目里。

 

4、在测试类中引用命名空间。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

 

5、在测试方法中使用Moq。

 

 Mock<IDiscountHelper> mocker = new Mock<IDiscountHelper>();     // 创建Mock对象,伪造一个IDiscountHelper的实现

  先定义一个实现IDiscountHelper的Mock,这个Mock是一个实现了IDiscountHelper的杜撰实例。

 

 mocker.Setup(m => m.GetDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);     // 装载方法
 mocker.Setup(m => m.GetDiscount(It.Is<decimal>(v => v == 0))).Throws<ArgumentOutOfRangeException>();     // 参数等于0时,抛出异常
 mocker.Setup(m => m.GetDiscount(It.Is<decimal>(v => v > 100))).Returns<decimal>(total => total * 0.9M);     // 参数大于100时,返回
 mocker.Setup(m => m.GetDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive))).Returns<decimal>(total => total - 5);      // 参数在10与100之间,包括10和100,返回-5

  使用Setup()来装载依赖的方法,用Returns<T>来返回任意类型的结果。

  在Setup()中使用lambda表达式,指定相应方法。用It对象来控制传入的参数,下面是It对象的一些常用方法:

  

  使用Returns()方法来控制返回值,同样支持lambda表达式。

    注意:Moq是以倒序的方式装载Setup()的,因此我们要最先写最基础的场景,往下写其他特殊的场景,确保所有场景都能够被覆盖。在这里,我们首先写了一个It.IsAny<decimal>来确保无论如何最终总能传入decimal参数,后面再根据不同的测试场景传入decimal参数。

  其实这个时候,我们已经跟之前定义的MinDiscountHelper类没什么关系了,我们直接使用Moq来做这个接口实现,返回数据给之后的测试。

 

  接着来我们只需要把实现了IDiscountHelper接口的Mock实例传给我们要测试的行为即可:

var test = new LinqValueCalculator(mocker.Object);

 

  整合起来如下:

        private Product[] InitProducts(decimal price)
        {
            return new Product[] { new Product { Price = price } };
        }

        /// <summary>
        /// 使用Moq辅助,单独测试跟其他模块有依赖关系的方法。
        /// </summary>
        [TestMethod]
        [ExpectedException(typeof(ArgumentOutOfRangeException))]        // 指定计划抛出的异常
        public void TestMethod1()
        {
            Mock<IDiscountHelper> mocker = new Mock<IDiscountHelper>();     // 创建Mock对象,伪造一个IDiscountHelper的实现
            /* 装载实现的GetDiscount方法。
             * Mock的装载方式是倒序,因此要最先写最基础的场景,往下装载特殊的场景。
             */
            mocker.Setup(m => m.GetDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);     // 装载方法
            mocker.Setup(m => m.GetDiscount(It.Is<decimal>(v => v == 0))).Throws<ArgumentOutOfRangeException>();     // 参数等于0时,抛出异常
            mocker.Setup(m => m.GetDiscount(It.Is<decimal>(v => v > 100))).Returns<decimal>(total => total * 0.9M);     // 参数大于100时,返回九折
            mocker.Setup(m => m.GetDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive))).Returns<decimal>(total => total - 5);      // 参数在10与100之间,包括10和100,返回-5

            var test = new LinqValueCalculator(mocker.Object);

            //decimal zero = test.ValueProducts(InitProducts(0M));
            decimal five = test.ValueProducts(InitProducts(5M));
            decimal ten = test.ValueProducts(InitProducts(10M));
            decimal fifty = test.ValueProducts(InitProducts(50M));
            decimal hundred = test.ValueProducts(InitProducts(100M));
            decimal twoHundred = test.ValueProducts(InitProducts(200M));

            Assert.AreEqual(5M, five, "Test Five failed");
            Assert.AreEqual(5M, ten, "Test Ten failed");
            Assert.AreEqual(45M, fifty, "Test Fifty failed");
            Assert.AreEqual(95M, hundred, "Test Hundred failed");
            Assert.AreEqual(200 * 0.9M, twoHundred, "Test TwoHundred failed");
            test.ValueProducts(InitProducts(0M));
        }

  注意:我们还使用了 [ExpectedException(typeof(ArgumentOutOfRangeException))]  来捕获我们希望测试抛出的异常。

 

自此,Moq就解决了我们在开篇提到的问题,我们不用再关心所依赖的其他模块的具体实现,也不用担心它们是更改了。我们使用Moq杜撰那些依赖项,回传想要的数据给测试目标。这样我们就能心无旁骛地达到我们的测试目标。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C#单元测试中的模拟(mock)是一种常用的技术,用于在测试中创建虚拟的对象,以替代真实的依赖项,以确保测试的可预测性和可重复性。 在C#中,常用的单元测试模拟框架包括Moq、NSubstitute和FakeItEasy等。这些框架提供了一些API,使得我们可以轻松地创建和配置模拟对象。 下面是一个使用Moq进行模拟的示例: ```csharp // 需要进行单元测试的类 public class Calculator { private IDataProvider _dataProvider; public Calculator(IDataProvider dataProvider) { _dataProvider = dataProvider; } public int AddNumbers(int a, int b) { // 从数据提供者获数据 int data = _dataProvider.GetData(); // 执行计算 int result = a + b + data; return result; } } // 数据提供者的接口 public interface IDataProvider { int GetData(); } // 使用Moq创建并配置模拟对象进行测试 [Test] public void AddNumbers_ShouldReturnCorrectSum() { // 创建模拟对象 var dataProviderMock = new Mock<IDataProvider>(); // 配置模拟对象的行为 dataProviderMock.Setup(dp => dp.GetData()).Returns(10); // 创建被测试对象,并传入模拟对象 var calculator = new Calculator(dataProviderMock.Object); // 执行测试 int result = calculator.AddNumbers(1, 2); // 验证结果 Assert.AreEqual(13, result); } ``` 在上面的示例中,我们使用Moq创建了一个IDataProvider的模拟对象,并配置了模拟对象的行为。然后,我们创建了被测试对象Calculator,并将模拟对象传递给它的构造函数。最后,我们执行了AddNumbers方法,并验证了最终的结果。 使用模拟对象可以帮助我们解决以下问题: 1. 解除对外部依赖项(如数据库、网络请求等)的依赖,使得测试更加独立和可控。 2. 模拟复杂的对象和行为,以测试各种边界情况和异常情况。 3. 提供自定义的返回值或异常,以测试不同的路径和条件。 相关问题: 1. 除了Moq之外,还有哪些常用的C#单元测试模拟框架? 2. 在使用模拟对象时,如何验证方法的参数是否被正确调用? 3. 如何配置模拟对象的返回值或抛出异常? 4. 在一些特殊情况下,如何模拟异步方法的行为? 5. 在多个测试中共享模拟对象有什么注意事项?

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值