65.9K
CodeProject 正在变化。 阅读更多。
Home

使用部分 Mock 来测试非公共方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2013 年 2 月 10 日

CPOL

3分钟阅读

viewsIcon

46686

downloadIcon

209

使用模拟框架来测试非公共方法的模式。代码示例基于 RhinoMock 框架,但很容易应用于任何其他模拟框架。

引言

如果您在单元测试中使用任何模拟框架,并且有兴趣找到一种测试类中非公共功能的方法,那么本文将讨论一种可能的解决方案。 所描述的解决方案和文章的代码示例使用了 RhinoMock 框架,但它应该很容易应用于任何其他模拟框架。

背景

许多人会争论在类中测试非公共方法的必要性,我可能在某种程度上同意,但有时我想超越公共类 API 并生成可以模拟内部行为的测试,这样做的驱动因素包括:

  • 更简洁和“更小”的测试
  • 更易于维护和理解
  • 单元测试用于声明/描述类中公共方法和非公共方法之间的约定,帮助其他人理解类设计

许多人可能会争辩说,基于私有方法构建测试会导致脆弱的测试,当您重构私有实现时,这些测试很容易中断,这也是需要牢记的另一点; 但是,同样,有时您会发现单独测试私有方法更有意义,并且当以后他们需要维护您的代码时,这应该会使其他人的生活更轻松。 我认为测试是阐明业务需求、范围和开发人员关注点的最佳工具。

有些人可能会争辩说,为了解决上述问题,最好的方法可能是将该功能重新定位到外部类中,并在公共方法中公开该功能。 这又是一个很好的观点,我倾向于在大多数时候同意; 但是我将代码分解为类的程度是有限制的。 其他一些人可能会争辩说,将非公共方法更改为公共方法,以便可以按照我们期望的方式对其进行测试; 但我发现这种方法是错误的:它以一种非预期的方式污染了类的“API”。

某些模拟框架中提供了该问题的解决方案,例如,Moq 提供了 Protected 帮助方法,JustMock 具有 NonPublic 方法。 两种方法的缺点是,如果方法签名被重构,该机制是多么脆弱。 但它可能会为某些人解决问题

本文提出了一种适用于大多数模拟框架的简单解决方案

  • 使私有方法成为受保护的和虚拟的
  • 文本类继承自要测试的类
  • 使用部分模拟来获得所需的混合行为

要测试的类

为了演示该方法,我将一个简单的类放在一起,该类公开了一个委托给两个私有方法的单一方法; 该示例过于简化,但足以演示如何将其应用于您自己的测试。 示例类采用一个整数,并根据一组简单的业务需求返回一个枚举值

    /// <summary>
    /// Determines the business <see cref="RuleType"/> from a given integer value
    /// </summary>
    public class BusinessRuleCalculator
    {
        /// <summary>
        /// Determines the business <see cref="RuleType"/> from
        /// a passed value
        /// </summary>
        /// <param name="value">Integer value to determine the business value from</param>
        /// <returns>
        /// Zero if value is zero,
        /// see <see cref="GetRuleForNegative"/> for a negative value, 
        /// see <see cref="GetRuleForPositive"/> for a positive value
        /// </returns>
        public  RuleType GetRule(int value)
        {
            if(value == 0) return RuleType.Zero;
            return value > 0
                       ? GetRuleForPositive(value)
                       : GetRuleForNegative(value);
        }

        /// <summary>
        /// Determines the business rule type for a negative value
        /// </summary>
        /// <param name="value">Negative value to determine the rule type</param>
        /// <returns>
        /// OverThreshold if the value is less than -10, 
        /// NegativeAnomaly if the value is between -6 and -8 (inclusive), 
        /// otherwise NegativeNormalCase
        /// </returns>
        protected virtual RuleType GetRuleForNegative(int value)
        {
            if(value < -10) return RuleType.OverThreshold;
            return value.IsBetween(-6, -8)
                ? RuleType.NegativeAnomaly
                : RuleType.NegativeNormalCase;
        }

        /// <summary>
        /// Determines the business rule type for a positive value
        /// </summary>
        /// <param name="value">Positive value to determine the rule type</param>
        /// <returns>
        /// OverThreshold if the value is greater than 10, 
        /// PositiveAnomaly if the value is between 3 and 5 (inclusive), 
        /// otherwise PositiveNormalCase
        /// </returns>
        protected virtual RuleType GetRuleForPositive(int value)
        {
            if(value > 10) return RuleType.OverThreshold;
            return value.IsBetween(3, 5) 
                ? RuleType.PositiveAnomaly 
                : RuleType.PositiveNormalCase;
        }
    }

因此,与其放置测试来为所有可能的业务案例调用 GetRule,不如测试以下内容

  • 如果传递零值,则该方法返回 Zero 枚举实例
  • 如果传递正值,则该方法调用 GetRuleForPositive 方法,但不调用 GetRuleForNegative
  • 如果传递负值,则该方法调用 GetRuleForNegative 方法,但不调用 GetRuleForPositive

然后,我们应该委托给其他测试集来验证私有(受保护的)方法是否正常工作。

测试类

因此,如上所述,我们创建了 BusinessRuleCalculatorPublicApiTests

    [TestClass]
    public class BusinessRuleCalculatorPublicApiTests : BusinessRuleCalculator
    {
        /// <summary>
        /// If zero value is passed the method returns Zero
        /// </summary>
        [TestMethod]
        public void GetRuleForZeroCase()
        {
            var calculator = new BusinessRuleCalculator();
            const RuleType expected = RuleType.Zero;
            var result = calculator.GetRule(0);
            Assert.AreEqual(expected, result);
        }

        /// <summary>
        /// Value is positive so ensure that the GetRuleForPositive
        /// method is invoked and that the GetRuleForNegative is not
        /// </summary>
        [TestMethod]
        public void GetRuleForPositiveValue()
        {
            var calculator = MockRepository.GeneratePartialMock<BusinessRuleCalculatorPublicApiTests>();
            const int value = 1;
            const RuleType expected = RuleType.PositiveNormalCase;
            calculator.Expect(c => c.GetRuleForPositive(value))
                .Return(expected)
                .Repeat.Once();

            calculator.Expect(c => c.GetRuleForNegative(value))
                .IgnoreArguments()
                .Repeat.Never();

            var result = calculator.GetRule(value);
            Assert.AreEqual(expected, result);
            calculator.VerifyAllExpectations();
        }

        /// <summary>
        /// Value is positive so ensure that the GetRuleForPositive
        /// method is invoked and that the GetRuleForNegative is not
        /// </summary>
        [TestMethod]
        public void GetRuleForNegativeValue()
        {
            var calculator = MockRepository.GeneratePartialMock<BusinessRuleCalculatorPublicApiTests>();
            const int value = -1;
            const RuleType expected = RuleType.NegativeNormalCase;
            calculator.Expect(c => c.GetRuleForPositive(value))
                .IgnoreArguments()
                .Repeat.Never();

            calculator.Expect(c => c.GetRuleForNegative(value))
                .Return(expected)
                .Repeat.Once();

            var result = calculator.GetRule(value);
            Assert.AreEqual(expected, result);
            calculator.VerifyAllExpectations();
        }
    }

由于 BusinessRuleCalculatorPublicApiTests 类继承自要测试的类,因此我们可以创建一个部分模拟,然后替换 GetRuleForNegativeGetRuleForPositive 的原始实现,以便在调用公共方法时执行我们的预期。 请注意,使受保护的方法 virtual 可确保部分模拟正常工作,否则仍会调用原始受保护方法实现

摘要

您可以下载本文中提供的示例代码并亲自尝试。 希望下次您创建单元测试时,您可能会发现它很有帮助。

© . All rights reserved.