如何对私有方法进行单元测试?

我正在构建一个类库,它将有一些公共&私有方法。我希望能够对私有方法进行单元测试(主要是在开发过程中,但也可以用于将来的重构)。

正确的做法是什么?

199518 次浏览

将它们声明为internal,然后使用InternalsVisibleToAttribute让单元测试程序集可以看到它们。

如果你正在使用。net,你应该使用InternalsVisibleToAttribute

如果您想对私有方法进行单元测试,那么可能会出现一些错误。单元测试(一般来说)用于测试类的接口,即类的公共(和受保护)方法。你当然可以“破解”一个解决方案(即使只是通过公开方法),但你可能还想考虑:

  1. 如果您想测试的方法确实值得测试,那么将它移到自己的类中可能是值得的。
  2. 向调用私有方法的公共方法添加更多测试,测试私有方法的功能。(正如评论者所指出的,只有当这些私有方法的功能确实是公共接口的一部分时,才应该这样做。如果它们实际上执行对用户隐藏的功能(即单元测试),这可能是不好的)。

我还使用了InternalsVisibleToAttribute方法。值得一提的是,如果你为了达到这个目的而将你之前的私有方法变成内部方法,那么也许它们不应该成为直接单元测试的对象。

毕竟,你测试的是类的行为,而不是具体实现——你可以在不改变前者的情况下改变后者,你的测试仍然应该通过。

我倾向于不使用编译器指令,因为它们很快就会把事情弄得乱七八糟。如果你真的需要它们,一种缓解它的方法是把它们放在一个partial类中,并让你的构建在制作产品版本时忽略那个.cs文件。

在极少数情况下,我想要测试私有函数,我通常会将它们修改为受保护的,并且我已经编写了一个带有公共包装器函数的子类。

类:

...


protected void APrivateFunction()
{
...
}


...

测试子类:

...


[Test]
public void TestAPrivateFunction()
{
APrivateFunction();
//or whatever testing code you want here
}


...

MS Test内置了一个很好的特性,通过创建一个名为VSCodeGenAccessors的文件,可以在项目中使用私有成员和方法

[System.Diagnostics.DebuggerStepThrough()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TestTools.UnitTestGeneration", "1.0.0.0")]
internal class BaseAccessor
{


protected Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject m_privateObject;


protected BaseAccessor(object target, Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType type)
{
m_privateObject = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject(target, type);
}


protected BaseAccessor(Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType type)
:
this(null, type)
{
}


internal virtual object Target
{
get
{
return m_privateObject.Target;
}
}


public override string ToString()
{
return this.Target.ToString();
}


public override bool Equals(object obj)
{
if (typeof(BaseAccessor).IsInstanceOfType(obj))
{
obj = ((BaseAccessor)(obj)).Target;
}
return this.Target.Equals(obj);
}


public override int GetHashCode()
{
return this.Target.GetHashCode();
}
}

使用派生自BaseAccessor的类

[System.Diagnostics.DebuggerStepThrough()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TestTools.UnitTestGeneration", "1.0.0.0")]
internal class SomeClassAccessor : BaseAccessor
{


protected static Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType m_privateType = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType(typeof(global::Namespace.SomeClass));


internal SomeClassAccessor(global::Namespace.Someclass target)
: base(target, m_privateType)
{
}


internal static string STATIC_STRING
{
get
{
string ret = ((string)(m_privateType.GetStaticField("STATIC_STRING")));
return ret;
}
set
{
m_privateType.SetStaticField("STATIC_STRING", value);
}
}


internal int memberVar    {
get
{
int ret = ((int)(m_privateObject.GetField("memberVar")));
return ret;
}
set
{
m_privateObject.SetField("memberVar", value);
}
}


internal int PrivateMethodName(int paramName)
{
object[] args = new object[] {
paramName};
int ret = (int)(m_privateObject.Invoke("PrivateMethodName", new System.Type[] {
typeof(int)}, args)));
return ret;
}
CC -Dprivate=public

“CC”是我使用的系统上的命令行编译器。-Dfoo=bar的作用相当于#define foo bar。因此,这个编译选项有效地将所有私有内容更改为公共。

在CodeProject上,有一篇文章简要讨论了测试私有方法的优缺点。然后它提供一些反射代码来访问私有方法(类似于Marcus上面提供的代码)。我在这个示例中发现的唯一问题是代码没有考虑重载方法。

你可以在这里找到文章:

http://www.codeproject.com/KB/cs/testnonpublicmembers.aspx

MbUnit有一个很好的包装,叫做Reflector。

Reflector dogReflector = new Reflector(new Dog());
dogReflector.Invoke("DreamAbout", DogDream.Food);

您还可以从属性中设置和获取值

dogReflector.GetProperty("Age");

关于“私人测试”,我同意…在完美的世界里。做私有单元测试是没有意义的。但在现实世界中,您可能最终希望编写私有测试,而不是重构代码。

有时候,测试私有声明是很好的。 基本上,编译器只有一个公共方法:Compile(string outputFileName, params string[] sourceSFileNames)。我相信您可以理解,如果不测试每个“隐藏”声明,就很难测试这样的方法!< / p >

这就是为什么我们创建了Visual t#:来简化测试。它是一个免费的。net编程语言(兼容c# v2.0)。

我们增加了‘。——“操作符。它就像'。操作符,除了您还可以从测试中访问任何隐藏的声明,而无需更改已测试项目中的任何内容。

看看我们的网站:下载 it 免费

由于某些原因,私有类型、内部变量和私有成员是这样的,通常你不想直接打乱它们。如果您这样做了,很可能稍后会崩溃,因为不能保证创建这些程序集的人会保留私有/内部实现。

但是,有时,在对编译或第三方程序集进行一些hack /探索时,我自己最终想要初始化一个私有类或具有私有或内部构造函数的类。或者,有时,在处理无法更改的预编译遗留库时——我最终会针对私有方法编写一些测试。

因此诞生了AccessPrivateWrapper - http://amazedsaint.blogspot.com/2010/05/accessprivatewrapper-c-40-dynamic.html -它是一个快速的包装类,使用c# 4.0的动态特性和反射可以使工作变得容易。

您可以创建内部/私有类型,例如

    //Note that the wrapper is dynamic
dynamic wrapper = AccessPrivateWrapper.FromType
(typeof(SomeKnownClass).Assembly,"ClassWithPrivateConstructor");


//Access the private members
wrapper.PrivateMethodInPrivateClass();
你可以在Visual studio 2008中为私有方法生成测试方法。当您为私有方法创建单元测试时,test References文件夹将添加到测试项目中,访问器将添加到该文件夹中。存取器也在单元测试方法的逻辑中被引用。这个访问器允许单元测试调用正在测试的代码中的私有方法。 详情请看

http://msdn.microsoft.com/en-us/library/bb385974.aspx

我不同意“您应该只对测试外部接口感兴趣”的理念。这有点像说汽车修理店只应该测试轮子是否转动。是的,最终我对外部行为感兴趣,但我喜欢我自己的,私人的,内部测试更具体一点,更切题。是的,如果我进行重构,我可能不得不更改一些测试,但除非是大规模的重构,否则我只需要更改一些测试,而其他(未更改的)内部测试仍然有效这一事实是重构成功的一个很好的指标。

您可以尝试仅使用公共接口来覆盖所有内部用例,理论上可以完全通过使用公共接口来测试每个内部方法(或者至少是每个重要的方法),但您可能最终不得不站在自己的头上来实现这一点,并且通过公共接口运行的测试用例与它们被设计用来测试的解决方案的内部部分之间的连接可能很难或不可能辨别。已经指出,保证内部机制正常工作的单个测试非常值得重构所带来的微小测试更改——至少这是我的经验。如果您必须为每次重构对测试进行巨大的更改,那么这可能没有意义,但在这种情况下,也许您应该完全重新考虑您的设计。一个好的设计应该足够灵活,允许大多数的变化,而不需要大量的重新设计。

测试私有方法可能没有用处。然而,我有时也喜欢从测试方法调用私有方法。大多数时候为了防止代码重复用于测试数据生成…

微软为此提供了两种机制:

访问器

  • 转到类定义的源代码
  • 右键单击类名
  • 选择“创建私人访问器”;
  • 选择要在其中创建访问器的项目 =比;您将得到一个名为foo_accessor的新类。 该类将在编译期间动态生成,并提供所有公共成员

然而,当涉及到原始类的接口更改时,该机制有时有点棘手。所以,大多数时候我避免使用这个。

< >强PrivateObject类 另一种方法是使用microsoft . visualstudio . testtools . unittest . privateobject

// Wrap an already existing instance
PrivateObject accessor = new PrivateObject( objectInstanceToBeWrapped );


// Retrieve a private field
MyReturnType accessiblePrivateField = (MyReturnType) accessor.GetField( "privateFieldName" );


// Call a private method
accessor.Invoke( "PrivateMethodName", new Object[] {/* ... */} );

还要注意,internalsvisibletoattribute有一个要求,你的程序集必须是强命名,如果你在一个以前没有这个要求的解决方案中工作,这就会产生它自己的一组问题。我使用访问器来测试私有方法。关于这个的例子,请参见这个问题

我很惊讶还没有人说过这一点,但我采用的一个解决方案是在类内部创建一个静态方法来测试自身。这使您可以访问用于测试的所有公共和私有内容。

此外,在脚本语言(具有OO功能,如Python、Ruby和PHP)中,您可以在运行时对文件本身进行测试。确保您的更改不会破坏任何东西的快速方法。这显然为测试所有类提供了可伸缩的解决方案:只需运行所有类即可。(你也可以用void main在其他语言中这样做,它也总是运行它的测试)。

有两种类型的私有方法。静态私有方法和非静态私有方法(实例方法)。下面两篇文章用示例解释了如何对私有方法进行单元测试。

  1. 单元测试静态私有方法
  2. 单元测试非静态私有方法

你可以用两种方法对私有方法进行单元测试

  1. 你可以创建PrivateObject类的实例,语法如下

    PrivateObject obj= new PrivateObject(PrivateClass);
    //now with this obj you can call the private method of PrivateCalss.
    obj.PrivateMethod("Parameters");
    
  2. You can use reflection.

    PrivateClass obj = new PrivateClass(); // Class containing private obj
    Type t = typeof(PrivateClass);
    var x = t.InvokeMember("PrivateFunc",
    BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public |
    BindingFlags.Instance, null, obj, new object[] { 5 });
    
这是关于私有方法的单元测试的好文章。但是我不知道哪个更好,是让你的应用程序专门为测试而设计(就像只为测试而创建测试)还是使用反射来测试。

.我们大多数人都会选择第二种方式

这里有一个例子,首先是方法签名:

private string[] SplitInternal()
{
return Regex.Matches(Format, @"([^/\[\]]|\[[^]]*\])+")
.Cast<Match>()
.Select(m => m.Value)
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
}

下面是测试:

/// <summary>
///A test for SplitInternal
///</summary>
[TestMethod()]
[DeploymentItem("Git XmlLib vs2008.dll")]
public void SplitInternalTest()
{
string path = "pair[path/to/@Key={0}]/Items/Item[Name={1}]/Date";
object[] values = new object[] { 2, "Martin" };
XPathString xp = new XPathString(path, values);


PrivateObject param0 = new PrivateObject(xp);
XPathString_Accessor target = new XPathString_Accessor(param0);
string[] expected = new string[] {
"pair[path/to/@Key={0}]",
"Items",
"Item[Name={1}]",
"Date"
};
string[] actual;
actual = target.SplitInternal();
CollectionAssert.AreEqual(expected, actual);
}

我认为应该问的一个更基本的问题是,为什么要首先测试私有方法。这是一种代码气味,你试图通过类的公共接口测试私有方法,而该方法是私有的,因为它是一个实现细节。人们应该只关心公共接口的行为,而不是它在背后是如何实现的。

如果我想测试私有方法的行为,通过使用公共重构,我可以将其代码提取到另一个类中(可能具有包级可见性,以确保它不是公共API的一部分)。然后我可以单独测试它的行为。

重构的产物意味着私有方法现在是一个独立的类,它已经成为原始类的合作者。通过它自己的单元测试,它的行为将被很好地理解。

然后,当我试图测试原始类时,我可以模拟它的行为,这样我就可以集中精力测试该类公共接口的行为,而不必测试公共接口的组合爆炸及其所有私有方法的行为。

我认为这类似于开车。当我开车时,我不会把引擎盖打开,这样我就能看到发动机在工作。我依靠汽车提供的接口,即转速计数器和速度计来知道发动机是否在工作。我依靠的是当我踩下油门踏板时,汽车实际上在移动。如果我想测试引擎,我可以单独检查它。: D

当然,如果您有遗留应用程序,直接测试私有方法可能是最后的手段,但我更希望对遗留代码进行重构,以实现更好的测试。迈克尔·费瑟就这个主题写了一本很棒的书。http://www.amazon.co.uk/Working-Effectively-Legacy-Robert-Martin/dp/0131177052

首先,您不应该测试代码的私有方法。你应该测试“公共接口”或API,即类的公共内容。API是所有公开给外部调用者的公共方法。

原因是一旦您开始测试类的私有方法和内部结构,您就将类的实现(私有的东西)耦合到您的测试中。这意味着当您决定更改实现细节时,您也必须更改您的测试。

出于这个原因,你应该避免使用internalsvisibletoattribute。

下面是Ian Cooper关于这个主题的精彩演讲:Ian Cooper: TDD,到底哪里出了问题

一种方法是让你的方法protected并编写一个测试fixture,它继承了你要测试的类。通过这种方式,你不是在转换你的方法public,而是启用了测试。

1)如果你有一个遗留代码,那么测试私有方法的唯一方法就是反射。

2)如果它是新代码,那么你有以下选项:

  • 使用反射(使之复杂)
  • 在同一个类中编写单元测试(使生产代码变得丑陋) 其中还包含测试代码)
  • 在某种util类中重构并使方法为公共
  • 使用@VisibleForTesting注释并删除private
我更喜欢注释法,最简单,最简单。唯一的问题是我们提高了能见度,我认为这不是一个大问题。 我们应该始终对接口进行编码,因此如果我们有一个接口MyService和一个实现MyServiceImpl,那么我们就可以有相应的测试类MyServiceTest(测试接口方法)和MyServiceImplTest(测试私有方法)。无论如何,所有的客户端都应该使用这个接口,所以在某种程度上,即使私有方法的可见性增加了,也不会有什么影响

我想在这里创建一个清晰的代码示例,您可以在任何想要测试私有方法的类上使用它。

在您的测试用例类中只包括这些方法,然后按照指示使用它们。

  /**
*
* @var Class_name_of_class_you_want_to_test_private_methods_in
* note: the actual class and the private variable to store the
* class instance in, should at least be different case so that
* they do not get confused in the code.  Here the class name is
* is upper case while the private instance variable is all lower
* case
*/
private $class_name_of_class_you_want_to_test_private_methods_in;


/**
* This uses reflection to be able to get private methods to test
* @param $methodName
* @return ReflectionMethod
*/
protected static function getMethod($methodName) {
$class = new ReflectionClass('Class_name_of_class_you_want_to_test_private_methods_in');
$method = $class->getMethod($methodName);
$method->setAccessible(true);
return $method;
}


/**
* Uses reflection class to call private methods and get return values.
* @param $methodName
* @param array $params
* @return mixed
*
* usage:     $this->_callMethod('_someFunctionName', array(param1,param2,param3));
*  {params are in
*   order in which they appear in the function declaration}
*/
protected function _callMethod($methodName, $params=array()) {
$method = self::getMethod($methodName);
return $method->invokeArgs($this->class_name_of_class_you_want_to_test_private_methods_in, $params);
}

$ this - > _callMethod (_someFunctionName,数组(param1, param2 param3));

只需要按照它们在原始私有函数中出现的顺序发出参数

在调试模式下构建时,你也可以将其声明为public或internal(使用InternalsVisibleToAttribute):

    /// <summary>
/// This Method is private.
/// </summary>
#if DEBUG
public
#else
private
#endif
static string MyPrivateMethod()
{
return "false";
}

它使代码膨胀,但在发布版本中它将是private

对于任何想要运行私有方法的人来说。这适用于任何单元测试框架,只使用旧的Reflection。

public class ReflectionTools
{
// If the class is non-static
public static Object InvokePrivate(Object objectUnderTest, string method, params object[] args)
{
Type t = objectUnderTest.GetType();
return t.InvokeMember(method,
BindingFlags.InvokeMethod |
BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.Static,
null,
objectUnderTest,
args);
}
// if the class is static
public static Object InvokePrivate(Type typeOfObjectUnderTest, string method, params object[] args)
{
MemberInfo[] members = typeOfObjectUnderTest.GetMembers(BindingFlags.NonPublic | BindingFlags.Static);
foreach(var member in members)
{
if (member.Name == method)
{
return typeOfObjectUnderTest.InvokeMember(method, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.InvokeMethod, null, typeOfObjectUnderTest, args);
}
}
return null;
}
}

然后在你的实际测试中,你可以这样做:

Assert.AreEqual(
ReflectionTools.InvokePrivate(
typeof(StaticClassOfMethod),
"PrivateMethod"),
"Expected Result");


Assert.AreEqual(
ReflectionTools.InvokePrivate(
new ClassOfMethod(),
"PrivateMethod"),
"Expected Result");

在我看来,你应该只对类的公共API进行单元测试。

为了对方法进行单元测试,将其设为公共,会破坏封装,暴露实现细节。

一个好的公共API解决了客户端代码的直接目标,并且完全解决了这个目标。

我使用PrivateObject类。但如前所述,最好避免测试私有方法。

Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(retVal);

JAVA语言

在这里,您可以使用模拟行为覆盖测试类的特定方法。

对于下面的代码:

public class ClassToTest
{
public void methodToTest()
{
Integer integerInstance = new Integer(0);
boolean returnValue= methodToMock(integerInstance);
if(returnValue)
{
System.out.println("methodToMock returned true");
}
else
{
System.out.println("methodToMock returned true");
}
System.out.println();
}
private boolean methodToMock(int value)
{
return true;
}
}

测试类将是:

public class ClassToTestTest{


@Test
public void testMethodToTest(){


new Mockup<ClassToTest>(){
@Mock
private boolean methodToMock(int value){
return true;
}
};


....


}
}