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





5.00/5 (1投票)
使用模拟框架来测试非公共方法的模式。代码示例基于 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
类继承自要测试的类,因此我们可以创建一个部分模拟,然后替换 GetRuleForNegative
和 GetRuleForPositive
的原始实现,以便在调用公共方法时执行我们的预期。 请注意,使受保护的方法 virtual
可确保部分模拟正常工作,否则仍会调用原始受保护方法实现

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