在C#中单元测试私有方法

417

Visual Studio允许通过自动生成的访问器类对私有方法进行单元测试。我已经编写了一个可以成功编译的私有方法测试,但在运行时失败了。下面是代码和测试的较小版本:

//in project MyProj
class TypeA
{
    private List<TypeB> myList = new List<TypeB>();

    private class TypeB
    {
        public TypeB()
        {
        }
    }

    public TypeA()
    {
    }

    private void MyFunc()
    {
        //processing of myList that changes state of instance
    }
}    

//in project TestMyProj           
public void MyFuncTest()
{
    TypeA_Accessor target = new TypeA_Accessor();
    //following line is the one that throws exception
    target.myList.Add(new TypeA_Accessor.TypeB());
    target.MyFunc();

    //check changed state of target
}

运行时错误为:

Object of type System.Collections.Generic.List`1[MyProj.TypeA.TypeA_Accessor+TypeB]' cannot be converted to type 'System.Collections.Generic.List`1[MyProj.TypeA.TypeA+TypeB]'.
根据智能感知(Intellisense)和编译器的提示,target是TypeA_Accessor类型。但在运行时,它是TypeA类型,因此列表添加操作失败。
有没有办法可以避免这个错误?或者更可能的是,其他人有什么建议(我预测可能会有“不要测试私有方法”和“不要让单元测试操纵对象状态”的建议)。

你需要为私有类TypeB创建一个访问器。访问器TypeA_Accessor提供了对TypeA的私有和受保护方法的访问权限。但是TypeB不是一个方法,它是一个类。 - Dima
访问器提供对私有/受保护方法、成员、属性和事件的访问。它不提供对类中私有/受保护类的访问。而私有/受保护的类(TypeB)仅应由拥有类(TypeA)的方法使用。基本上,您正在尝试将来自TypeA之外的私有类(TypeB)添加到“myList”中,该列表是私有的。由于您正在使用访问器,因此可以访问myList没有问题。但是,您无法通过访问器使用TypeB。可能的解决方案是将TypeB移出TypeA。但这可能会破坏您的设计。 - Dima
1
认为测试私有方法应该按照以下方式进行 - nate_weldon
18个回答

822

34
现在微软已经添加了PrivateObject,这就是正确的答案。 - Zoey
4
好的回答,但请注意,PrivateMethod 需要被声明为“protected”,而不是“private”。 - HerbalMart
27
也许我误解了你的意思,但如果你是在暗示PrivateObject只能访问受保护的成员而不能访问私有成员,那么你是错误的。 - kmote
25
对于静态方法,您可以使用“PrivateType pt = new PrivateType(typeof(MyClass));”语句创建一个pt对象,然后像调用私有对象的Invoke方法一样,在pt对象上调用InvokeStatic方法。 - Steve Hibbert
14
如果有人想知道,截至MSTest.TestFramework v1.2.1,PrivateObject和PrivateType类对于目标为.NET Core 2.0的项目不可用。这个问题已经在Github上被记录下来:https://github.com/Microsoft/testfx/issues/366 - shiitake
显示剩余21条评论

118

“标准或最佳实践并不存在,它们可能仅仅是普遍看法”。

这一讨论同样适用。

输入图像描述

这完全取决于你认为什么是一个单元,如果你认为单元是一个类,那么你只会调用公共方法。如果你认为单元是代码行,则调用私有方法将不会让你感到内疚。

如果你想调用私有方法,可以使用 "PrivateObject" 类并调用 invoke 方法。你可以观看这个详细的 YouTube 视频(http://www.youtube.com/watch?v=Vq6Gcs9LrPQ),该视频展示了如何使用 "PrivateObject" 并讨论测试私有方法是否合理。


13
我们领域里的所有迂腐炫技都会带来更多问题而非解决问题。这就是为什么我的一位编程老师说:“把一切都公开!” - codewise
5
Codewise所说的“过于追求细节”的行为,确实恰如其分。 - Richard Moore
1
另一个方法是注意到这是由不同范例、目标和实现相互作用引起的困境。然后,根据需求作出选择。例如,也许更喜欢将测试保留在单独的项目中,编译不同的程序集,但仍然测试那些基本上被限制在测试程序集中的测试方法,声明这些方法为“public”。我认为这样的架构决策需要经过深思熟虑。但话说回来,我确实喜欢一些迂腐的东西。 - CervEd

110

另一个想法是将测试扩展到“内部”类/方法,从而在测试中给出更白盒的感觉。您可以在程序集上使用InternalsVisibleTo属性将其公开给单独的单元测试模块。

与封闭类结合使用,您可以接近这种封装,使得测试方法仅从单元测试程序集和您的方法可见。请注意,密封类中的受保护方法实际上是私有的。

[assembly: InternalsVisibleTo("MyCode.UnitTests")]
namespace MyCode.MyWatch
{
    #pragma warning disable CS0628 //invalid because of InternalsVisibleTo
    public sealed class MyWatch
    {
        Func<DateTime> _getNow = delegate () { return DateTime.Now; };
    

       //construktor for testing purposes where you "can change DateTime.Now"
       internal protected MyWatch(Func<DateTime> getNow)
       {
           _getNow = getNow;
       }

       public MyWatch()
       {            
       }
   }
}

以及单元测试:

namespace MyCode.UnitTests
{

[TestMethod]
public void TestminuteChanged()
{
    //watch for traviling in time
    DateTime baseTime = DateTime.Now;
    DateTime nowforTesting = baseTime;
    Func<DateTime> _getNowForTesting = delegate () { return nowforTesting; };

    MyWatch myWatch= new MyWatch(_getNowForTesting );
    nowforTesting = baseTime.AddMinute(1); //skip minute
    //TODO check myWatch
}

[TestMethod]
public void TestStabilityOnFebruary29()
{
    Func<DateTime> _getNowForTesting = delegate () { return new DateTime(2024, 2, 29); };
    MyWatch myWatch= new MyWatch(_getNowForTesting );
    //component does not crash in overlap year
}
}

3
是的,这就是我建议的。这有点“hacky”,但至少它们不是“公开的”。 - Jeff
48
这是一个出色的回答,仅仅因为它没有说“不要测试私有方法”,但是,是有点“hacky”的。我希望有个解决方案。在我看来,说“私有方法不应该被测试”是错误的,因为这等价于说“私有方法不应该正确”。 - MasterMastic
5
我了解你的困惑,有些人声称私有方法不应该在单元测试中进行测试。公共API是输出结果,但有时错误的实现也可以得到正确的输出。或者实现会产生一些负面影响,比如持有不必要的资源、引用对象导致垃圾回收无法回收等等。除非他们提供其他测试可以覆盖私有方法,否则我认为他们不能维护一个百分之百经过测试的代码。请注意,上述翻译仅供参考,不作为正式翻译使用。 - mr.Pony
3
好的。我之前不知道这个内容。正是我需要的。假设我为别人构建了一个第三方组件,但我只想公开非常有限的API。在内部它非常复杂,我想测试单独的组件(例如输入解析器或验证器),但我不希望公开它们。最终用户不应该知道这些信息。我知道标准方法是“仅测试您的公共API”,但我更喜欢测试单一职责的代码单元。这使我可以在不公开它们的情况下进行测试。 - kowgli
3
我认为它也应该是被接受的答案。对于私有方法测试的讨论很有意思,但这更多是一个观点问题。在我看来,双方都提出了相当好的论点。 如果你仍然想/需要测试私有方法,你应该有一种方式可以做到。这是唯一提供的方法。 此外,你可以通过在AssemblyInfo.cs中使用以下语句: [assembly: InternalsVisibleTo("UT.cs")] 来为整个程序集进行操作。 - mgueydan
显示剩余4条评论

49

通过反射来测试私有方法是一种方法。这也适用于 NUnit 和 XUnit:

MyObject objUnderTest = new MyObject();
MethodInfo methodInfo = typeof(MyObject).GetMethod("SomePrivateMethod", BindingFlags.NonPublic | BindingFlags.Instance);
object[] parameters = {"parameters here"};
methodInfo.Invoke(objUnderTest, parameters);

调用方法中的静态和非静态? - Kiquenet
2
反射依赖方法的缺点是,当您使用R#重命名方法时,它们往往会出现故障。在小型项目上可能不是一个大问题,但在庞大的代码库上,单元测试以这种方式中断并且需要快速修复,这变得有点烦人。从这个意义上说,我认同Jeff的答案。 - XDS
5
很遗憾,nameof() 方法无法在类的外部获取私有方法的名称。 - Gabriel Morin

27
嗯...我和其他人一样有一个问题:测试一个简单但至关重要的私有方法。在阅读了这个帖子之后,它看起来就像是“我想在这个简单的金属片上钻一个简单的孔,并确保质量符合规格”,然后出现了“好的,这并不容易。首先,没有适当的工具可以做到这一点,但是你可以在花园里建立一个引力波观测站。请阅读我在http://foobar.brigther-than-einstein.org/上的文章。当然,你首先需要参加一些高级量子物理课程,然后需要大量的超酷氮气,当然还有我的书可在亚马逊购买”...

换句话说...

不,先解决最重要的问题。

每一个方法,无论是私有的、内部的、受保护的、公开的都必须可测试。必须有一种实现这些测试的方法,而不是像这里所展示的那样费事费力。

为什么?正是因为一些贡献者迄今所做的架构提及。也许对软件原则的简单重申可以澄清一些误解。

在这种情况下,通常的嫌疑人是:OCP、SRP,以及始终如一的KIS。

等等。将所有内容公开的想法更多地是一种政治和一种态度。但是,即使在开源社区中,当涉及到代码时,这并不是教条。相反,“隐藏”某些内容是一种好的做法,可以使某个API更易于熟悉。例如,您会隐藏市场新型数字温度计构建块的核心计算 - 不是为了向好奇的代码读者隐藏实际测量曲线背后的数学原理,而是为了防止您的代码依赖于某些可能突然重要的用户,他们无法抵制使用您以前的私有、内部、受保护的代码来实现自己的想法。

我在说什么?

private double TranslateMeasurementIntoLinear(double actualMeasurement);

容易宣布水瓶座时代或现在所称的东西,但如果我的传感器从1.0到2.0,Translate...的实现可能会从一个简单的线性方程式改变为一个相当复杂的计算,使用分析或其他方法,因此我会破坏其他人的代码。为什么?因为他们没有理解软件编码的基本原则,甚至连KIS都不知道。

总之,我们需要一种简单的方法来测试私有方法 - 不需要任何麻烦。

首先:大家新年快乐!

第二步:复习你的架构课程。

第三步:"public"修饰符是一种信仰,而不是解决方案。


1
我完全同意。我也在想,如果销售包含代码的产品时我们应该怎么做。我们必须明确公共API,但为内部功能编写一堆测试也是很好的。如果所有东西都经过测试并且很小,那么它可能会正常工作。但是那些没有人需要调用的小函数也需要进行测试! - Darkgaze
2
选择你的毒药。是使用严格访问权限但只有少量单元测试的代码,还是在完全公开方法的类上进行大量单元测试。这并不是专业人士想要做出的选择。 - Mike Wise

14
另一个未提及的选项是将单元测试类创建为要测试的对象的子类。NUnit示例:
[TestFixture]
public class UnitTests : ObjectWithPrivateMethods
{
    [Test]
    public void TestSomeProtectedMethod()
    {
        Assert.IsTrue(this.SomeProtectedMethod() == true, "Failed test, result false");
    }
}

这将允许轻松测试私有和受保护的方法(但不包括继承的私有方法),并且可以使您将所有测试与真实代码分开,以便您不必将测试程序集部署到生产环境中。在许多继承对象中,将私有方法更改为受保护方法是可以接受的,并且这是一个相当简单的更改。
然而...
虽然这是解决如何测试隐藏方法的一种有趣方法,但我不确定在所有情况下都会推荐这是正确的解决方案。在内部测试对象似乎有点奇怪,我怀疑在某些情况下,这种方法可能会出现问题。(例如,不可变对象可能会使某些测试变得非常困难)。
虽然我提到了这种方法,但我认为这更像是一个构思的建议,而不是一个合理的解决方案。请谨慎对待。
编辑:我发现人们对这个答案投票反对,真是太有趣了,因为我明确表示这是一个坏主意。这是否意味着人们同意我的观点?我真的很困惑.....
多年后编辑:我正在回顾一些旧的回答,然后发现了这个。我最初的意图是玩笑般地提出我认为是一个愚蠢的想法,只是为了给问题的作者一些疯狂的构思选项。
我认为这个回答很愚蠢,因为它打破了使用私有方法的整个理念,并通过绕过安全模型将所有内容公开暴露出来。
如果我今天要回答这个问题,我不会绕圈子那么多,只会坚决主张将方法设为公开。我敢说,可能有99%的私有方法从来都不需要是私有的,封装隐藏的整个做法完全是浪费时间。(砰!)
总会有一些例外情况,其中确实需要使用私有方法,但我坚信,如果你无法测试你的代码,那么你可能在写糟糕的代码。
优秀的工程师会测试他们的所有工作,没有例外。- 我
所有方法的默认状态应该是公开的,只有在功能完全失败时才进行更改。你不应该为了使你的代码防弹而绕过封装。
所以,打开你的代码并进行测试吧!

2
这是一个有创意的解决方案,但有点不太正规。 - shinzou
这个很有效。根据需要我已经做过几次了。好处是,重命名目标类或测试方法不会破坏任何东西,就像使用反射一样。 - Display name
这与密封类不兼容。 - Paweł Kanarek

9

来自书籍《遗留代码有效工作》:

"如果我们需要测试一个私有方法,我们应该将它变为公有的。如果将其变为公有的让我们感到不适,那么在大多数情况下,这意味着我们的类正在做太多的事情,我们应该对其进行修复。"

根据作者的说法,解决方法是创建一个新类,并将该方法添加为 public

作者进一步解释道:

"良好的设计是可测试的,而不可测试的设计则是糟糕的。"

因此,在这些限制下,您唯一的选择是将方法设置为 public,可以放在当前类或者新建一个类中。


6
安全编码实践的原则是,除了像API接口这样的必须公开的内容,我们应该将一切都保持为私有。如果程序中的每一个小部分都被定义为公共的,那么恶意用户就可以利用你的代码攻击系统。你需要减少二进制文件受攻击的可能性,同时也要保证可测试性。 - HackSlash
3
@hackslash 将事物设为私有并不会隐藏任何内容,也不会阻止其他程序集或对象实际调用它们。它只是增加了编译时类型检查。 - CervEd
4
面对这个问题时,这是一个值得考虑的明智建议,但肯定不是最佳答案。本质上它是在说“永远不要使用私有方法”(或者换句话说:不要测试你的所有方法)。在童话般的学术研究或内部应用开发场景中,这可能有效。但对于具有公共 API 的复杂库来说,则需要忘记这个建议。想象一下,你已经拥有了一个包含相互依赖的大量类对象图的复杂库。现在通过将所有私有代码移动到新的虚拟类中,以某种方式与它们共享状态来使其变得更加困难。 - enzi
3
这个回复没有回答原来的问题(“如何测试私有方法?”),而是回答了没有人问到的问题(“我应该测试私有方法吗?”)。 - bazzilic
仅为了单元测试而将方法公开,这真的值得吗?如果该方法用于特定目的并绑定到特定类,则将其公开并移动到单独的类中可能会将其暴露给应用程序的其他部分。遵循编码指南直到它更适合,仅仅是为了遵循它可能会导致严重问题,异常就在那里! - Sumit Jambhale
为了单元测试而公开方法,这真的值得吗?如果该方法用于特定目的并绑定到特定类,则将其公开并移至单独的类可能会使其暴露给应用程序的其他部分。只是为了遵循编码指南而盲目跟随,可能会导致严重问题,例外情况总是存在的! - Sumit Jambhale

8

现在是2022年!

......我们已经拥有了.NET6

虽然这并没有真正回答这个问题,但我现在更喜欢在同一个C#项目中使用命名约定如<ClassName>.Tests.cs来放置代码和测试。然后我使用internal访问修饰符代替private

在项目文件中,我的设置类似于以下内容:

<ItemGroup Condition="'$(Configuration)' == 'Release'">
  <Compile Remove="**\*.Tests.cs" />
</ItemGroup>

发布版本中排除测试文件。 根据需要进行修改。

常见问题 1:但有时您还想在Release(优化)构建中测试代码。

答案:我觉得这是不必要的。我相信编译器会按照我的意图执行其工作而不会弄乱它。到目前为止,我没有理由质疑它这样做的能力。

常见问题 2:但我真的想保持方法(或类)private

答案:本页中有许多出色的解决方案可供尝试。根据我的经验,将访问修饰符设置为internal通常已经足够了,因为该方法(或类)不会在所定义的项目之外可见。超出此范围后,就没有更多需要隐藏的内容了。


非常有趣,但它们是否能与单元测试运行器或IDE良好地配合使用? - nodakai
1
@nodakai 是的,他们可以。我主要使用Visual Studio编码,偶尔使用VS Code。只要“Configuration”未设置为“Release”,测试就可用并像您预期的那样运行。 - Soma Mbadiwe
如果您愿意使用internal,您仍然可以为您的代码和测试使用单独的程序集,并使用InternalsVisibleToAttribute允许特定的程序集访问internal类或方法。 - undefined

5
我使用这个助手(对象类型扩展)。
 public static  TReturn CallPrivateMethod<TReturn>(
        this object instance,
        string methodName,
        params object[] parameters)
    {
        Type type = instance.GetType();
        BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Instance;
        MethodInfo method = type.GetMethod(methodName, bindingAttr);

        return (TReturn)method.Invoke(instance, parameters);
    }

你可以这样调用
Calculator systemUnderTest = new Calculator();
int result = systemUnderTest.CallPrivateMethod<int>("PrivateAdd",1,8);

其中之一的优点是它使用泛型来预定返回类型。


@Vitaly 在测试项目中无法访问“private”方法以在nameof表达式中调用它。 - undefined
@CodingEnthusiast,你说得对,我有时候忘记了这段代码是从测试类外部调用的。 - undefined

2

将私有方法提取到另一个类中,并在该类上进行测试;详细了解SRP原则(单一职责原则)。

看起来你需要将私有方法提取到另一个类中;在这种情况下应该是公共的。不要试图测试私有方法,而是应该测试该另一个类的公共方法。

我们有以下场景:

Class A
+ outputFile: Stream
- _someLogic(arg1, arg2) 

我们需要测试_someLogic的逻辑;但是看起来A类承担了比它应该承担更多的角色(违反了SRP原则);可以将其重构为两个类。

Class A1
    + A1(logicHandler: A2) # take A2 for handle logic
    + outputFile: Stream
Class A2
    + someLogic(arg1, arg2) 

这样可以在A2上测试someLogic;在A1中只需创建一些虚假的A2,然后将其注入构造函数以测试是否将A2调用到名为someLogic的函数中。


你如何测试一个类的职责是生成数据(它通过处理来自某个第三方库的事件来完成)并以已知格式公开该数据,而你想测试该公共接口的情况?这里没有任何一个回答声称我们不应该测试方法来解决生产者场景的问题。 - allmhuran
@allmhuran:你应该提供另一个构造函数,其中包含输入事件生产者,例如:p = ProduceData(inputEventProducer);然后通过调用inputEventProducer.emit("some event")来模拟事件发生,并测试p的输出。一般来说,如果很难进行测试,那么代码中就存在一些需要重构的问题。或者,如果您无法修改代码,则需要进行更高级别的测试,如集成测试或端到端测试(E2E)。 - o0omycomputero0o
但是,该构造函数不会成为生产应用程序正常流程的一部分。您基本上将在非模拟类中编写一个模拟,仅用于测试。这就是为什么能够调用私有方法作为测试的一部分是有意义的原因...因为这实际上是在生产中发生的情况。也就是说,是的,翻译器接受注入的依赖项,但注入的依赖项来自第三方库(它可能是一个类,而不是一个接口)。 - allmhuran

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接