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]'.
根据智能感知 - 因此我猜是编译器 - 目标是 TypeA_Accessor 类型。但在运行时它是 TypeA 类型,因此列表添加失败。
有什么办法可以阻止这个错误?或者,也许更有可能,其他人有什么其他建议(我预测可能“不要测试私有方法”和“不要让单元测试操纵对象的状态”)。
您可以使用 PrivateObject
类:
Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(expectedVal, retVal);
注意:PrivateObject
和 PrivateType
不适用于面向 netcoreapp2.0 - GitHub Issue 366 的项目
“没有所谓的标准或最佳实践,可能它们只是流行的观点”。
本次讨论也是如此。
https://i.stack.imgur.com/MA56m.png
这完全取决于您认为什么是 unit ,如果您认为 UNIT 是一个类,那么您只会点击 public 方法。如果你认为 UNIT 是一行代码,碰到私有方法不会让你感到内疚。
如果要调用私有方法,可以使用“PrivateObject”类并调用调用方法。您可以观看这个深入的 youtube 视频 (http://www.youtube.com/watch?v=Vq6Gcs9LrPQ),它展示了如何使用“PrivateObject”,还讨论了私有方法的测试是否合乎逻辑。
这里的另一个想法是将测试扩展到“内部”类/方法,给这种测试更多的白盒感觉。您可以使用程序集上的 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
}
}
测试私有方法的一种方法是通过反射。这也适用于 NUnit 和 XUnit:
MyObject objUnderTest = new MyObject();
MethodInfo methodInfo = typeof(MyObject).GetMethod("SomePrivateMethod", BindingFlags.NonPublic | BindingFlags.Instance);
object[] parameters = {"parameters here"};
methodInfo.Invoke(objUnderTest, parameters);
call methods
静态和非静态?
Ermh... 遇到了完全相同的问题:测试一个简单,但关键的private 方法。看完这个帖子,好像是“我想在这块简单的金属上钻一个简单的孔,我想确保质量符合规格”,然后“好吧,这不是很简单。首先,没有合适的工具可以做到这一点,但是你可以在你的花园里建造一个引力波天文台。阅读我在 http://foobar.brigther-than-einstein.org/ 的文章,当然首先,你必须参加一些高级量子物理课程,那么你需要大量的超冷氮,当然,我的书可以在亚马逊上买到”......
换句话说...
不,第一件事。
每一种方法,可能是私有的、内部的、受保护的、公共的,都必须是可测试的。必须有一种方法来实现这样的测试,而不用像这里介绍的那样麻烦。
为什么?正是因为一些贡献者到目前为止所做的架构提及。也许对软件原则的简单重申可能会消除一些误解。
在这种情况下,通常的嫌疑人是:OCP、SRP,以及一如既往的 KIS。
但是等一下。将所有东西都公开的想法更多的是不那么政治化,而是一种态度。但。谈到代码,即使在当时的开源社区,这也不是教条。相反,“隐藏”某些东西是一种很好的做法,可以更容易地熟悉某个 API。例如,您会隐藏新上市的数字温度计构建模块的核心计算——不是为了向好奇的代码读者隐藏真实测量曲线背后的数学,而是为了防止您的代码变得依赖于某些,或许突然间重要的用户无法抗拒使用您以前私有的、内部的、受保护的代码来实现他们自己的想法。
我在说什么?
私人双 TranslateMeasurementIntoLinear(双实际测量);
很容易宣布水瓶座时代或现在所谓的时代,但如果我的传感器从 1.0 升级到 2.0,Translate... 的实现可能会从一个易于理解的简单线性方程变为“重新-对每个人都可用”,到使用分析或其他东西的非常复杂的计算,所以我会破坏其他人的代码。为什么?因为他们不了解软件编码的基本原理,甚至不了解 KIS。
为了让这个童话故事变得简短:我们需要一种简单的方法来测试私有方法——不用多说。
第一:大家新年快乐!
第二:排练你的建筑师课程。
第三:“公共”修饰语是宗教,而不是解决方案。
另一个没有提到的选项是创建单元测试类作为您正在测试的对象的子类。 NUnit 示例:
[TestFixture]
public class UnitTests : ObjectWithPrivateMethods
{
[Test]
public void TestSomeProtectedMethod()
{
Assert.IsTrue(this.SomeProtectedMethod() == true, "Failed test, result false");
}
}
这将允许轻松测试私有和受保护(但不是继承的私有)方法,并且它允许您将所有测试与真实代码分开,因此您不会将测试程序集部署到生产中。在许多继承对象中,将私有方法切换为受保护方法是可以接受的,而且这是一个非常简单的更改。
然而...
虽然这是解决如何测试隐藏方法的问题的有趣方法,但我不确定我是否会主张这是在所有情况下解决问题的正确方法。在内部测试一个对象似乎有点奇怪,我怀疑这种方法可能会在某些情况下对你产生影响。 (例如,不可变对象可能会使某些测试变得非常困难)。
虽然我提到了这种方法,但我认为这更像是一个头脑风暴的建议,而不是一个合法的解决方案。带上一粒盐。
编辑:我发现人们投票否决这个答案真的很搞笑,因为我明确地将其描述为一个坏主意。这是否意味着人们同意我的观点?我感到很困惑.....
来自《有效地使用遗留代码》一书:
“如果我们需要测试一个私有方法,我们应该将它公开。如果公开它让我们感到困扰,在大多数情况下,这意味着我们的班级做得太多,我们应该修复它。”
根据作者的说法,修复它的方法是创建一个新类并将方法添加为 public
。
作者进一步解释:
“好的设计是可测试的,不可测试的设计是坏的。”
因此,在这些限制内,您唯一真正的选择是在当前类或新类中创建方法 public
。
我使用这个助手(对象类型扩展)
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);
优点之一是它使用泛型来预先确定返回类型。
将私有方法提取到另一个类,在该类上进行测试;阅读更多关于 SRP 原则(单一职责原则)的信息
看来您需要将 private
方法提取到另一个类;这应该是 public
。您应该测试另一个类的 public
方法,而不是尝试测试 private
方法。
我们有以下场景:
Class A
+ outputFile: Stream
- _someLogic(arg1, arg2)
我们需要测试_someLogic
的逻辑;但似乎 Class A
的作用超出了它的需要(违反了 SRP 原则);只需重构为两个类
Class A1
+ A1(logicHandler: A2) # take A2 for handle logic
+ outputFile: Stream
Class A2
+ someLogic(arg1, arg2)
这样 someLogic
可以在 A2 上进行测试;在 A1 中创建一些假 A2,然后注入构造函数以测试 A2 是否被调用到名为 someLogic
的函数。
public static class PrivateMethodTester
{
public static object InvokePrivateMethodWithReturnType<T>(this T testObject, string methodName, Type[] methodParamTypes, object[] parameters)
{
//shows that we want the nonpublic, static, or instance methods.
var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance;
//gets the method, but we need the methodparamtypes so that we don't accidentally get an ambiguous method with different params.
MethodInfo methodInfo = testObject.GetType().GetMethod(methodName, flags, null, methodParamTypes, null);
if (methodInfo == null)
{
throw new Exception("Unable to find method.");
}
//invokes our method on our object with the parameters.
var result = methodInfo.Invoke(testObject, parameters);
if (result is Task task)
{
//if it is a task, it won't resolve without forcing it to resolve, which means we won't get our exceptions.
task.GetAwaiter().GetResult();
}
return result;
}
}
这样称呼它:
Type[] paramTypes = new Type[] { typeof(OrderTender), typeof(string) };
var parameters = new object[] { orderTender, OrderErrorReasonNames.FailedToCloneTransaction };
myClass.InvokePrivateMethodWithReturnType("myPrivateMethodName", paramTypes, parameters);
在 VS 2005/2008 中,您可以使用 private accessor 来测试私有成员,但这种方式在以后的 VS 版本中消失
您可以使用嵌套类来测试私有方法。例如(使用 NUnit v3):
internal static class A
{
// ... other code
private static Int32 Sum(Int32 a, Int32 b) => a + b;
[TestFixture]
private static class UnitTests
{
[Test]
public static void OnePlusTwoEqualsThree()
{
Assert.AreEqual(3, Sum(1, 2));
}
}
}
此外,可以使用“部分类”功能将测试相关代码移动到另一个文件,使用“条件编译”等从发布版本中排除。高级示例:
文件 A.cs
internal static partial class A
{
// ... other code
private static Int32 Sum(Int32 a, Int32 b) => a + b;
}
文件 A.UnitTests.cs
#if UNIT_TESTING
partial class A
{
[TestFixture]
private static class UnitTests
{
[Test]
public static void OnePlusTwoEqualsThree()
{
Assert.AreEqual(3, Sum(1, 2));
}
}
}
#endif
我有另一种对我有用的方法。因为我总是在调试模式下运行我的测试,所以我使用 #if DEBUG
在我的私有方法之前添加 public
。所以我的私有方法是这样的:
public class Test
{
#if (DEBUG)
public
#endif
string PrivateMehtod()
{
return "PrivateMehtod called";
}
}
#if (DEBUG || OptimizeMode)
并在调试模式下进行测试,最后确保在 OptimizeMode
中进行测试
遗憾的是 .net6 中没有 PrivateObject
类
但是,我编写了一个能够使用反射调用私有方法的小型扩展方法。
看一下示例代码:
class Test
{
private string GetStr(string x, int y) => $"Success! {x} {y}";
}
var test = new Test();
var res = test.Invoke<string>("GetStr", "testparam", 123);
Console.WriteLine(res); // "Success! testparam 123"
这是扩展方法的实现:
/// <summary>
/// Invokes a private/public method on an object. Useful for unit testing.
/// </summary>
/// <typeparam name="T">Specifies the method invocation result type.</typeparam>
/// <param name="obj">The object containing the method.</param>
/// <param name="methodName">Name of the method.</param>
/// <param name="parameters">Parameters to pass to the method.</param>
/// <returns>The result of the method invocation.</returns>
/// <exception cref="ArgumentException">When no such method exists on the object.</exception>
/// <exception cref="ArgumentException">When the method invocation resulted in an object of different type, as the type param T.</exception>
/// <example>
/// class Test
/// {
/// private string GetStr(string x, int y) => $"Success! {x} {y}";
/// }
///
/// var test = new Test();
/// var res = test.Invoke<string>("GetStr", "testparam", 123);
/// Console.WriteLine(res); // "Success! testparam 123"
/// </example>
public static T Invoke<T>(this object obj, string methodName, params object[] parameters)
{
var method = obj.GetType().GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (method == null)
{
throw new ArgumentException($"No private method \"{methodName}\" found in class \"{obj.GetType().Name}\"");
}
var res = method.Invoke(obj, parameters);
if (res is T)
{
return (T)res;
}
throw new ArgumentException($"Bad type parameter. Type parameter is of type \"{typeof(T).Name}\", whereas method invocation result is of type \"{res.GetType().Name}\"");
}
现在是2022年!
...我们有 .NET6
虽然这并不能真正回答问题,但这些天我首选的方法是在同一个 C# 项目中配置代码和测试,命名约定类似于 <ClassName>.Tests.cs
。然后我使用 internal
访问修饰符而不是 private
。
在项目文件中,我有这样的东西:
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<Compile Remove="**\*.Tests.cs" />
</ItemGroup>
排除 release
构建中的测试文件。根据需要进行修改。
常见问题 1:但有时您还想在发布(优化)构建中测试代码。
答:我觉得没必要。我相信编译器会在不打乱我意图的情况下完成它的工作。到目前为止,我没有理由质疑它这样做的能力。
FAQ 2:但我真的想保留方法(或类)private
。
答案:此页面中有很多出色的解决方案可供尝试。根据我的经验,将访问修饰符设置为 internal
通常就足够了,因为方法(或类)在它定义的项目之外是不可见的。除此之外,没有什么可隐藏的了。
不定期副业成功案例分享