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,因此列表添加失败。
有什么方法可以停止这个错误吗?或者,更有可能的是,其他人有什么其他的建议(我预测可能是“不要测试私有方法”和“不要使用单元测试来操纵对象的状态”)。
另一个没有提到的选项是创建单元测试类作为您正在测试的对象的子类。NUnit的例子:
[TestFixture]
public class UnitTests : ObjectWithPrivateMethods
{
[Test]
public void TestSomeProtectedMethod()
{
Assert.IsTrue(this.SomeProtectedMethod() == true, "Failed test, result false");
}
}
这将允许轻松测试私有和受保护的(但不继承私有)方法,并且允许将所有测试与实际代码分开,这样就不必将测试程序集部署到生产环境中。在许多继承对象中,将私有方法切换为受保护的方法是可以接受的,而且这是一个相当简单的更改。
然而……
虽然这是解决如何测试隐藏方法问题的一种有趣的方法,但我不确定我是否会主张这是在所有情况下解决问题的正确解决方案。在内部测试一个对象似乎有点奇怪,我怀疑在某些情况下,这种方法可能会让您失败。(例如,不可变对象可能会使一些测试非常困难)。
虽然我提到了这种方法,但我认为这更像是一个头脑风暴的建议,而不是一个合理的解决方案。对它持保留态度。
编辑:我发现人们投票否决这个答案真的很滑稽,因为我明确地把它描述为一个坏主意。这是否意味着人们同意我的观点?我很困惑.....
现在是2022年了!
...我们有。net 6
虽然这并没有真正地回答问题,但我现在更喜欢的方法是在同一个c#项目中搭配代码和测试,使用<ClassName>. tests .cs这样的命名约定。然后我使用内部访问修饰符而不是私有。
在项目文件中,我有这样的东西:
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<Compile Remove="**\*.Tests.cs" />
</ItemGroup>
在发布版本中排除测试文件。根据需要进行修改。
FAQ 1:但是有时候你也想在发布(优化)版本中测试代码。
答:我觉得没必要。我相信编译器将完成它的工作而不会打乱我的意图。到目前为止,我还没有理由质疑它这样做的能力。
FAQ 2:但是我真的想保持方法(或类)私有。
答:本页有许多优秀的解决方案可供尝试。根据我的经验,将访问修饰符设置为内部通常就足够了,因为方法(或类)在它所定义的项目之外是不可见的。除此之外,没什么好隐瞒的了。
这里的另一个想法是将测试扩展到“内部”类/方法,给这种测试更多的白盒意义。您可以在程序集上使用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
}
}