zoukankan      html  css  js  c++  java
  • 【PRO ASP.NE MVC4 学习札记】使用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杜撰那些依赖项,回传想要的数据给测试目标。这样我们就能心无旁骛地达到我们的测试目标。

  • 相关阅读:
    解析Zigbee技术在智能家居应用中的优缺点
    ZigBee无线网络技术在小区路灯照明系统的应用
    Zigbee技术特点
    梯度下降法-理解共轭梯度法
    感知机--理解系数向量和样本点递归
    fisher线性判别
    iso data 聚类算法
    近邻算法--类与类间最小损失函数
    聚类算法--理解最大最小距离分类
    类间距离测度方法
  • 原文地址:https://www.cnblogs.com/firstdown/p/4467635.html
Copyright © 2011-2022 走看看