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

jun*_*iro 265 c# unit-testing

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
}
Run Code Online (Sandbox Code Playgroud)

运行时错误是:

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]'.
Run Code Online (Sandbox Code Playgroud)

根据intellisense - 因此我猜编译器 - 目标是TypeA_Accessor类型.但在运行时它的类型为TypeA,因此列表添加失败.

有什么办法可以阻止这个错误吗?或者,或许更有可能的是,其他人有什么其他建议(我预测可能"不测试私有方法"和"没有单元测试操纵对象的状态").

Scu*_*tle 612

您可以使用PrivateObject类

Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(expectedVal, retVal);
Run Code Online (Sandbox Code Playgroud)

  • 这是正确的答案,现在微软已经添加了PrivateObject. (23认同)
  • @HerbalMart:也许我误解了你,但如果你建议PrivateObject只能访问受保护的成员而不是私有成员,那你就错了. (20认同)
  • @JeffPearce对于静态方法,您可以使用"PrivateType pt = new PrivateType(typeof(MyClass));",然后在pt对象上调用InvokeStatic,就像在私有对象上调用Invoke一样. (13认同)
  • 万一有人想知道自MSTest.TestFramework v1.2.1起-面向.NET Core 2.0的项目无法使用PrivateObject和PrivateType类-为此存在github问题:https://github.com/Microsoft/testfx/issues/ 366 (11认同)
  • 很好的答案但请注意,PrivateMethod需要"保护"而不是"私人". (4认同)
  • @Kiquenet obj.Invoke("TestMethod",toPassIn); 该方法将对象数组作为参数 (3认同)

Kei*_*las 266

是的,不要测试私有方法....单元测试的想法是通过其公共'API'测试单元.

如果您发现需要测试很多私有行为,那么很可能您在要测试的类中隐藏了一个新的"类",将其解压缩并通过其公共接口进行测试.

一条建议/思考工具.....有一种想法,任何方法都不应该是私密的.意味着所有方法都应该存在于对象的公共接口上....如果您觉得需要将其设置为私有,则很可能存在于另一个对象上.

这条建议在实践中并没有很好的解决,但它的建议大多是好的,而且往往会促使人们将对象分解成更小的对象.

  • 我不同意.在OOD中,私有方法和属性是不重复自己的内在方式(https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).黑盒编程和封装背后的想法是隐藏订户的技术细节.因此,确实需要在代码中使用非平凡的私有方法和属性.如果它不重要,则需要进行测试. (315认同)
  • 当你被抛弃在如此可怕的OO代码库上并被要求"逐步改进它"时,发现我无法对私有方法进行一些初步测试是令人失望的.是的,也许在教科书中这些方法不会在这里,但在现实世界中我们有用户有产品要求.我不能只是做一个大规模的"看看ma清理代码"重构,而不需要在项目中进行一些测试来构建.感觉像是另一个迫使练习程序员进入天真似乎很好的途径的例子,但没有考虑到真正凌乱的狗屎. (31认同)
  • 错误.您可能希望使用私有方法来避免代码重复.或者用于验证.或者出于公众世界不应该了解的许多其他目的. (24认同)
  • 它不是内在的,一些OO语言没有私有方法,私有属性可以包含具有可以测试的公共接口的对象. (8认同)
  • 这个建议的重点是,如果你的对象做了一件事,而且是干的,那么通常没有理由拥有私有方法.通常私有方法做对象不是真正负责的事情,但是非常有用,如果非常微不足道,那么它通常是另一个对象,因为它可能违反了SRP (7认同)
  • 没有.....你会发现,那些方法需要住在其他地方,否则你往往会违反SRP (5认同)
  • 微软不是真相的来源,我不知道他们是否有任何关于这个主题的东西:)在微软甚至在单元测试的想法之前存在这些想法!但是,在早期的极端编程用户群中,由促进单元测试的人们向世界广泛讨论了如何处理私有方法的概念.积极重构和​​SRP的想法应该不需要私有,如果有的话,应该通过公共接口轻松测试. (4认同)
  • @Jaro在类上使用关键字internal。这正是它的意思 (4认同)
  • 这是一个由TDD之类的东西驱动的设计过程,当你处理现有的代码库时,如果它很糟糕,那么你使用不同的技术,虽然我还没有测试私有方法,但是有一本好书"与遗产有效地合作代码"(我是其中的贡献者)正好处理这些问题@ dune.rocks (3认同)
  • "不要测试私人方法" - 这是一个相当的声明,我真的不确定我是否同意.你有来自微软的文档备份吗? (2认同)
  • @KeithNicholas如果我尝试编写简洁的代码,则意味着我将把大型方法的一部分提取到较小的方法中,并从最初的大型方法中调用它们。将这些较小的新方法保留在同一类中并私有是有意义的,不是吗?通常,这些较小的方法可能包含一些不重要的逻辑,应该对其进行测试,并且它们可能会多次重复使用。您是说要仅通过原始的公共大型方法来测试此逻辑吗? (2认同)
  • @JoeDyndale大方法分解为小方法,很好的第一步。下一步是将您的对象分解为较小的基于SRP的对象。您的对象只会通过其公共接口(被测单元)使用,因此请通过该对象对其进行测试。无法通过公共接口轻松测试的非平凡的隐藏逻辑通常是另一个对象。我从1999年开始进行极限编程和单元测试,从此再也不必测试私有方法。我见过人们这样做,而且我总是可以随时将其代码重构为带有组合对象的更简单的OO代码。 (2认同)
  • @KeithNicholas 也许我只是在考虑以不同(可能更糟)的方式进行测试。我的方法也可以通过公共接口进行测试,但是程序要么必须为每个测试做很多不必要的工作,要么在一个测试中测试太多东西。我可以将我提取的方法分离到具有公共接口的单独类中,但不是以一种比简单地将现有类中的方法公开更有意义的方式开始。后者是实用的,但严格来说不是正确的 IMO。 (2认同)
  • “是的,不要测试私有方法......单元测试的想法是通过其公共'API'来测试单元。” - 这简直是错误的。如果你的单元(让我们谈谈一个简单的类)有它自己的内部要求并且它满足了一些方法,我应该能够单独测试它们以查看是否满足要求。举个例子:这个类在外部似乎运行良好,但在内部它没有做一些先进先出的东西,这提高了性能。好的,代码有效,但我希望它以最初预期的方式工作。 (2认同)
  • 尽管将大型类分解为较小的类并公开私有方法的建议听起来很正确,但事实并非如此。您最终会遇到很多小类,阅读代码并理解它将是一场噩梦。 (2认同)
  • @ vipero07,如果您有一个if语句,该语句包含300行,而else包含300行,那么关于什么是好的设计,我们甚至不在同一页上。对我而言,这应该永远不会因完善的代码而出现。这意味着在您的600个奇数行代码中可能存在多个类。 (2认同)
  • @ vipero07可以肯定的是,如果您的论点是您从设计错误的代码开始,那么请确保您需要做一些妥协的事情作为中间步骤。但是,尽管如此,我建议可能使用https://refactoring.com/catalog/replaceMethodWithMethodObject.html。不过,我的原始建议是,使用完善的代码,您不需要测试私有方法,并且对于大多数情况,您不需要任何私有方法。如果您确实需要一个,那么就值得考虑一下,是否有更好的设计。 (2认同)

Shi*_*ala 91

"没有什么叫做标准或最佳实践,可能它们只是流行的观点".

同样适用于此讨论.

在此输入图像描述

这一切都取决于你认为是一个单位,如果你认为UNIT是一个类,那么你只会点击公共方法.如果你认为UNIT是代码行,打私人方法不会让你感到内疚.

如果要调用私有方法,可以使用"PrivateObject"类并调用invoke方法.您可以观看此深入视频(http://www.youtube.com/watch?v=Vq6Gcs9LrPQ),其中显示了如何使用"PrivateObject",还讨论了私有方法的测试是否合乎逻辑.

  • 代码明智说了什么。“迂腐的狂妄”——完美地概括了这一点。 (5认同)
  • 我们这个领域中所有迂腐的行为造成的问题比它解决的问题还要多。这就是为什么我的一位编程老师说:“将所有内容公开!” (3认同)

Jef*_*eff 47

这里的另一个想法是将测试扩展到"内部"类/方法,给出更多的白盒感测.您可以在程序集上使用InternalsVisibleToAttribute将这些内容公开给单独的单元测试模块.

结合密封类,您可以进行这样的封装,即测试方法只能从您的方法的unittest程序集中看到.考虑密封类中的受保护方法事实上是私有的.

[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()
       {            
       }
   }
}
Run Code Online (Sandbox Code Playgroud)

和单元测试:

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
}
}
Run Code Online (Sandbox Code Playgroud)

  • 这是一个很好的答案**只是**因为它没有说"不测试私人方法"但是,它是相当"hacky".我希望有一个解决方案.IMO说"私人方法不应该被测试"是不好的,因为我看到它的方式:它相当于"私人方法不应该是正确的". (25认同)
  • ya ken,我也对那些声称不应在单元测试中测试私有方法的人感到困惑.公共API是输出,但有时错误的实现也会提供正确的输出.或者实现产生了一些不好的副作用,例如保存不必要的资源,引用对象以防止它被gc收集......等等.除非他们提供的其他测试可以覆盖私有方法而不是单元测试,否则我会认为他们无法维护100%经过测试的代码. (5认同)
  • 我认为这也应该是公认的答案。关于私有方法测试的争论读起来很酷,但这更多的是一个观点问题。在我看来,双方都提出了很好的论据。如果您仍然想要/需要测试私有方法,您应该有办法。这是唯一提供的。此外,您可以对整个程序集执行此操作,在 AssemblyInfo.cs 中使用: [程序集:InternalsVisibleTo("UT.cs")] (3认同)
  • 是的,这就是我的建议。有点“ hacky”,但至少它们不是“ public”。 (2认同)
  • 好的。我不知道这一点。正是我需要的。假设我构建了一个 3rd 方组件供其他人使用,但我只想公开一个非常有限的 API。在内部,它非常复杂,我想测试单个组件(例如输入解析器或验证器),但我不想公开这些。最终用户不需要知道这些。我知道这里的标准方法是“仅测试您的公共 API”,但我更愿意测试单一职责的代码单元。这使我可以在不公开的情况下做到这一点。 (2认同)

Jac*_*son 25

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

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

  • 反射依赖方法的缺点是,当您使用R#重命名方法时,它们往往会中断.对于小型项目来说这可能不是一个大问题,但是在巨大的代码库上,让单元测试以这种方式破解然后不得不四处走动并快速修复它们会变得有点唠叨.从这个意义上说,我的钱归到杰夫的答案. (2认同)
  • @XDS 太糟糕了 nameof() 无法从其类外部获取私有方法的名称。 (2认同)

Rog*_*ill 11

另一个未提及的选项是将单元测试类创建为您正在测试的对象的子类。NUnit 示例:

[TestFixture]
public class UnitTests : ObjectWithPrivateMethods
{
    [Test]
    public void TestSomeProtectedMethod()
    {
        Assert.IsTrue(this.SomeProtectedMethod() == true, "Failed test, result false");
    }
}
Run Code Online (Sandbox Code Playgroud)

这将允许轻松测试私有和受保护(但不是继承的私有)方法,并且它允许您将所有测试与实际代码分开,这样您就不会将测试程序集部署到生产中。在许多继承的对象中,将私有方法切换为受保护的方法是可以接受的,这是一个非常简单的更改。

然而...

虽然这是解决如何测试隐藏方法的问题的有趣方法,但我不确定我是否会主张这在所有情况下都是问题的正确解决方案。在内部测试一个对象似乎有点奇怪,我怀疑在某些情况下这种方法可能会让你失望。(例如,不可变对象可能会使某些测试变得非常困难)。

虽然我提到了这种方法,但我认为这更像是一个集思广益的建议,而不是一个合法的解决方案。把它和一粒盐一起吃。

编辑:我发现人们对这个答案投反对票真的很有趣,因为我明确地将其描述为一个坏主意。这是否意味着人们同意我的观点?我感到很困惑.....

  • 这是一个创造性的解决方案,但有点老套。 (2认同)

小智 10

Ermh ...出现了完全相同的问题:测试一个简单但关键的私有方法。读完该线程后,看起来就像“我想在这种简单的金属上钻出这个简单的孔,然后确保质量符合规格”,然后出现“好吧,这并不容易。首先,有这样做没有适当的工具,但你可以建立在你的花园引力波天文台。在阅读我的文章http://foobar.brigther-than-einstein.org/首先,当然,你必须参加一些高级量子物理学课程,然后您需要大量的超冷氮,然后,当然,我的书可以在亚马逊上找到。”

换一种说法...

不,第一件事。

每种方法(可能是私有的,内部的,受保护的,公开的)必须是可测试的。必须有一种方法来实现这种测试,而无需如本文所述。

为什么?正是由于一些贡献者到目前为止所做的体系结构提及。简单地重申软件原理可能会消除一些误解。

在这种情况下,通常的怀疑者是:OCP,SRP和一如既往的KIS。

等一下 使所有内容公开可用的想法更多是出于政治目的,而不是一种态度。但。关于代码,即使在当时的开源社区中,这也不是教条。相反,“隐藏”某些东西是使您更容易熟悉某个API的好习惯。例如,您将隐藏新上市的数字温度计构建模块的核心计算,而不是将数学运算隐藏在真实测得的曲线后面,以免引起好奇的代码阅读者,但要防止您的代码变得依赖某些代码,也许突然之间重要的用户无法抗拒使用您以前私有的,内部受保护的代码来实现自己的想法。

我在说什么

私人double TranslateMeasurementIntoLinear(double actualMeasurement);

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

简而言之:我们需要一种简单的方法来测试私有方法-事不宜迟。

第一:大家新年快乐!

第二:演练您的架构师课程。

第三:“公共”修饰语是宗教,而不是解决方案。

  • 选择你的毒药。具有严格访问权限的代码,但单元测试较少,或者对具有完全公共方法的类进行大量单元测试。这不完全是专业人士想要做出的选择。 (2认同)

Kai*_*ann 9

来自《有效处理遗留代码》一书:

“如果我们需要测试一个私有方法,我们应该将其公开。如果公开它让我们感到困扰,在大多数情况下,这意味着我们的类做得太多了,我们应该修复它。”

根据作者的说法,修复它的方法是创建一个新类并将方法添加为public.

作者进一步解释道:

“好的设计是可测试的,不可测试的设计就是糟糕的。”

public因此,在这些限制内,您唯一真正的选择是在当前类或新类中创建方法。

  • 安全编码实践要求我们应该将所有内容设为私有,除非它绝对需要公开,例如 API 接口。如果程序中的每个小东西都被定义为公共的,那么恶意行为者可能会调用它来使用您的代码来攻击系统。您希望减少二进制文件的攻击面,同时又可测试。 (6认同)
  • 这是面对这个问题时需要考虑的合理建议,但绝对不是首选答案。本质上它说“永远不要使用私有方法”(或者:不要测试所有方法)。在童话学术界或内部应用程序开发场景中可能可行。但对于具有公共 API 的复杂库来说,就忘了它吧。想象一下,您已经有一个复杂的库,其中包含相互依赖的类的大型对象图。现在,通过将所有私有代码移至新的虚拟类并以某种方式与它们共享状态,使其变得更加困难。 (4认同)
  • @hackslash 将事物设为私有不会隐藏任何内容,也不会禁止其他程序集或对象实际调用它们。它只是添加了编译时类型检查。 (3认同)
  • 此响应没有回答最初的问题(“我如何测试私有方法?”),而是回答了没有人问过的问题(“我应该测试私有方法吗?”)。 (3认同)

Som*_*iwe 8

现在已经2022年了!

...我们有.NET6

虽然这并不能真正回答问题,但我现在的首选方法是在同一个 C# 项目中并置代码并进行测试,并使用<ClassName>.Tests.cs. 然后我使用internal访问修饰符而不是private.

在项目文件中,我有这样的内容:

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

在构建中排除测试文件release。根据需要修改。

常见问题解答 1:但有时您还想在发布(优化)版本中测试代码。

:我觉得没有必要。我相信编译器会完成它的工作而不会扰乱我的意图。到目前为止,我没有理由质疑它的能力。

FAQ 2:但我真的想保留该方法(或类)private

回答:本页中有很多优秀的解决方案可供尝试。根据我的经验,将访问修饰符设置为internal通常就足够了,因为方法(或类)在其定义的项目之外不可见。除此之外,没有什么可隐藏的了。


lst*_*zyk 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);
    }
Run Code Online (Sandbox Code Playgroud)

你可以这样称呼它

Calculator systemUnderTest = new Calculator();
int result = systemUnderTest.CallPrivateMethod<int>("PrivateAdd",1,8);
Run Code Online (Sandbox Code Playgroud)

优点之一是它使用泛型来预先确定返回类型。