C#的单元测试中如何测试受保护方法

92

我来自Java EE世界,但现在我正在从事.Net项目。在Java中,当我想要测试一个受保护的方法时,很容易,只需要拥有与测试类相同的包名即可。

对于C#是否有类似的东西?有没有关于单元测试受保护方法的良好实践?我只发现一些框架和人们说我应该只测试公共方法。

应该可以在不使用任何框架的情况下完成...

9个回答

113

你可以在测试类中继承你要测试的类。

[TestClass]
public class Test1 : SomeClass
{
    [TestMethod]
    public void MyTest
    {
        Assert.AreEqual(1, ProtectedMethod());
    }

}

2
谢谢你的快速回复。我已经测试了,它可以工作。我不知道这是否是一种好的方法,但至少我可以开始进行单元测试了。 - fiso
3
如果我的被测试类没有无参构造函数(例如,它只有四个参数的构造函数),该怎么办?当我从它派生一个测试类时,会出现“TestClass does not contain a constructor that takes 0 arguments”的编译错误。 - dragan.stepanovic
4
我在我的单元测试项目中添加了一个名为“Testable”的类,它扩展了其他类并使用“TestableMethods”包装了它们的受保护方法,并在必要时重新实现了相应的构造函数。结果相同,但我喜欢将测试与实现分开。 - Élie
我认为这可能实际上是 Asert.AreEqual(1, Test1.ProtectedMethod());,因为派生类型需要在内部调用。尽管从基类继承,但基本方法仍然是受保护的。 - edwardrbaker
16
"这是错误的答案",测试类不应该与被测试的类相同...有许多情况下,被测试的类需要特殊初始化(例如,它仅在某些容器中有效),而这种解决方案会引起问题。 - Selvin
1
在依赖注入的场景中,或者任何你需要将模拟依赖项传递到正在测试的类中的情况下,这似乎不是一个好选择。 - Phil B

47

你可以在继承你想要测试的类的新类中暴露受保护的方法。

public class ExposedClassToTest : ClassToTest
{
    public bool ExposedProtectedMethod(int parameter)
    {
        return base.ProtectedMethod(parameter);
    }
}

45

另一个选择是在这些方法中使用 internal,然后使用 InternalsVisibleTo 来允许您的测试组件访问这些方法。这不会阻止其他同一程序集中的类使用这些方法,但确实阻止它们被其他非测试程序集访问。

这样做并不能提供太多封装和保护,但它相当简单直接,可以很有用。

在包含内部方法的程序集的 AssemblyInfo.cs 中添加以下内容:

[assembly: InternalsVisibleTo("TestsAssembly")]

11
protected方法可以在子类中被看到(并且可以被重写),即使这些子类不在同一个程序集中,而internal方法则不能。它们在功能上不是等效的,因此只适用于不需要真正的protected方法的情况。 - E-Riz
3
@E-Riz 如果你想让子类在程序集外重写内部方法,你可以使用 protected internal https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/protected-internal (既不是 protected internal,也不是 internal 等同于 protected,但它们在测试时提供了不同的选项)。对于大多数情况,使用子类的顶级答案可能是最好的解决方案。 - Richard Garside

8
你可以使用PrivateObject类来访问所有私有/受保护的方法/字段。
PrivateObject是Microsoft单元测试框架中的一个类,它是一个包装器,可用于调用通常不可访问的成员进行单元测试。

21
如果解释一下什么是PrivateObject,这个回答会更有帮助。 - Emond
1
这很简单,只需查阅文档 https://msdn.microsoft.com/zh-cn/library/microsoft.visualstudio.testtools.unittesting.privateobject.aspx - Usama Aslam
20
注意:这仅适用于使用MSTest的情况,不适用于NUnit和XUnit。 - Matt
使用私有对象进行单元测试的示例 https://dev59.com/wGox5IYBdhLWcg3wklCE#15607491。这适用于私有和受保护的方法。 - LazyDog

6
尽管被接受的答案是最好的,但它并没有解决我的问题。从受保护的类派生污染了我的测试类,使其产生了很多其他东西。最终,我选择将要测试的逻辑提取到一个公共类中并进行测试。当然,这种方法可能不适用于每个人,可能需要进行相当多的重构,但如果您一直滚动到这个答案,它可能会帮助您。 :) 这里有一个例子。
旧情况:
protected class ProtectedClass{
   protected void ProtectedMethod(){
      //logic you wanted to test but can't :(
   }
}

新情况:

protected class ProtectedClass{
   private INewPublicClass _newPublicClass;

   public ProtectedClass(INewPublicClass newPublicClass) {
      _newPublicClass = newPublicClass;
   }

   protected void ProtectedMethod(){
      //the logic you wanted to test has been moved to another class
      _newPublicClass.DoStuff();
   }
}

public class NewPublicClass : INewPublicClass
{
   public void DoStuff() {
      //this logic can be tested!
   }
}

public class NewPublicClassTest
{
    NewPublicClass _target;
    public void DoStuff_WithoutInput_ShouldSucceed() {
        //Arrange test and call the method with the logic you want to test
        _target.DoStuff();
    }
}

5

18
也许我错了,但我觉得如果需要反射来进行测试,那么可能是因为我做错了什么。但如果在 C# 中这很正常,那我会逐渐习惯的。 - fiso
我也希望不必使用它。我只是想指出这种技术,以防你不知道。在我看来,你可能有一个私有方法需要独立测试,以便能够轻松地测试各种输入和预期结果。 - Justin Harvey
我认为使用反射是测试类的非公共特性的合理方法。在Java中,我经常这样做,否则你就必须使用一个“不正确”的修饰符,以便可以测试一个正确构造的类。如果您需要私有或受保护的内容,则仍应该能够进行测试,而无需向真实类添加垃圾代码以允许访问(并可能导致您的代码从安全性的角度失败)。 - millebi
1
反射,无论感觉如何,从语义上讲是正确的,因为您可以保留方法的可见性,而不必将一个方法封装在另一个方法中。 - Professor of programming

3

你可以创建一个存根(stub),其中包含一个公共方法,该方法调用基类中的受保护方法。这也是在生产中使用此受保护方法的方式。

public class FooStub : Bar 
{
    public string MyMethodFoo()
    {
        return MyMethodBar();
    }
}

public abstract class Bar 
{
    protected string MyMethodBar()
    {
        return "Hello World!"
    }
}

0

这里是我在测试项目中使用的扩展方法(什么是扩展方法?)。 唯一的缺点是,您必须将名称文字字面写出来,因为nameof()遵循保护规则,不允许您引用受保护或私有成员。

        public static MethodInfo GetNonPublicMethod(this Type type, string method)
        {
            MemberInfo[] temp = type.GetMember(method, MemberTypes.Method, BindingFlags.NonPublic | BindingFlags.Instance);
            if (temp.Length == 1)
            {
                if (temp[0] is MethodInfo ret)
                {
                    return ret;
                }
                else
                {
                    throw new ArgumentException("Not a method.");
                }
            }
            else
            {
                if (temp.Length == 0)
                {
                    throw new ArgumentException("Method was not found.");
                }
                else
                {
                    throw new ArgumentException("Multiple methods found.");
                }
            }
        }

关于此方法的更多信息请参见:https://learn.microsoft.com/en-us/dotnet/api/system.type.getmember?view=net-5.0

PS:使用methodInfo.Invoke(instance, params)来调用它。


已经有一个答案建议使用反射,根据该答案的评论,这不是保持代码清洁的方法。 - Jerome2606
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community
  1. 这是一个在测试环境中获取受保护方法的工作示例,虽然有建议使用它,但没有可复制的示例。
  2. 此代码仅用于测试环境 - 通常情况下,在不引入任何测试代码到原始代码库的情况下,打破传统以测试事物更为可接受。我承认代码可以改进,但这段代码确切地做到了所要求的。
- Daniel B

0
第一个答案中提到的方法行不通,所有受保护的方法都被隐藏了。应该添加base(),这样受保护的方法就会变得可见。
[TestClass]
public class Test1 : SomeClass
{
    public Test1() : base()
    {
    }

    [TestMethod]
    public void MyTest
    {
        Assert.AreEqual(1, ProtectedMethod());
    }
}

1
感谢您对Stack Overflow社区做出贡献的兴趣。这个问题已经有很多答案了,其中一个答案已经得到社区广泛验证。您确定您的方法之前没有被提到过吗?如果是这样的话,能否解释一下您的方法与众不同的地方,在什么情况下您的方法可能更好,并且为什么您认为之前的答案不够满意。您可以编辑您的回答并提供解释吗? - Jeremy Caney

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