使用 Moq 在 C# 中单元测试受保护的方法

38

最近我注意到,可以使用Moq对抽象基类进行单元测试,而不是在测试中创建一个实现该抽象基类的虚拟类。请参见如何使用Moq测试抽象类中的具体方法?例如,您可以这样做:

public abstract class MyAbstractClass 
{
    public virtual void MyMethod()
    {
        // ...    
    }
}

[Test]
public void MyMethodTest()
{
    // Arrange            
    Mock<MyAbstractClass> mock = new Mock<MyAbstractClass>() { CallBase = true };

    // Act
    mock.Object.MyMethod();

    // Assert
    // ... 
}

我想知道是否有类似的技术,让我无需创建包装类就能够测试受保护成员。也就是说,你如何测试这个方法:

public class MyClassWithProtectedMethod
{
    protected void MyProtectedMethod()
    {

    }
}

我知道Moq.Protected名称空间,但据我所见,它只允许您使用例如设置期望的方式。

mock.Protected().Setup("MyProtectedMethod").Verifiable();
我知道显而易见的答案是“不要测试受保护的方法,只测试公开的方法”,但这是另一个辩论!我只想知道是否可以使用Moq实现这一点。 更新:下面是我通常会进行测试的方式:
public class MyClassWithProtectedMethodTester : MyClassWithProtectedMethod
{
    public void MyProtectedMethod()
    {
        base.MyProtectedMethod();
    }
}

提前感谢。


这是你想要的吗?(http://blogs.clariusconsulting.net/kzu/mocking-protected-members-with-moq/) - bzlm
我真的很想知道为什么.Setup(...)没有.Returns(..)- API文档链接似乎已经失效了,而且我现在也没有VS - 你确定没有返回值吗?- bzlms的帖子似乎表明曾经有一个... - Random Dev
@bzlm 不,那和我上面发布的一样,它只是让你设置一个保护方法被调用的期望。我想要在测试中实际执行这个受保护的方法。也就是说,我想为了测试目的将受保护方法的可访问性更改为公共的,这可以通过包装类来实现。 - magritte
3个回答

37

在Moq中调用受保护成员的另一种方式是使用以下模板:

  1. 在您的类中,将要受保护的成员标记为virtual。例如:

public class ClassProtected
    {
        public string CallingFunction(Customer customer)
        {
            var firstName = ProtectedFunction(customer.FirstName);
            var lastName = ProtectedFunction(customer.LastName);

            return string.Format("{0}, {1}", lastName, firstName);
        }

        protected virtual string ProtectedFunction(string value)
        {
            return value.Replace("SAP", string.Empty);
        }
    }

然后在你的单元测试中添加引用

 using Moq.Protected;

在你的单元测试中,你可以编写以下代码:

    [TestFixture]
    public class TestClassProttected
    {
        [Test]
        public void all_bad_words_should_be_scrubbed()
        {
            //Arrange
            var mockCustomerNameFormatter = new Mock<ClassProtected>();

            mockCustomerNameFormatter.Protected()
                .Setup<string>("ProtectedFunction", ItExpr.IsAny<string>())
                .Returns("here can be any value")
                .Verifiable(); // you should call this function in any case. Without calling next Verify will not give you any benefit at all

            //Act
            mockCustomerNameFormatter.Object.CallingFunction(new Customer());

            //Assert
            mockCustomerNameFormatter.Verify();
        }
    }

请注意使用ItExpr而不是It。在Verifiable方面还有一个需要注意的问题,如果不调用Verifiable,则Verify将不会被调用。


我看不出“From”方法是从哪里来的?mockCustomerNameFormatter.Object的类型是“ClassProtected”,该类没有“From”方法?我猜应该是“CallingFunction”而不是“From”? - Gerrie Pretorius
抱歉打错字了。你说得对,应该是CallingFunction而不是from。我会更正我的答案。 - Yuriy Zaletskyy

12

首先,对一个抽象方法进行单元测试是没有意义的。因为它没有实现代码!你可能会想要对一个不纯的抽象类进行单元测试,以验证该抽象方法是否被调用:

[Test]
public void Do_WhenCalled_CallsMyAbstractMethod()
{
    var sutMock = new Mock<MyAbstractClass>() { CallBase = true };
    sutMock.Object.Do();
    sutMock.Verify(x => x.MyAbstractMethod());
}

public abstract class MyAbstractClass
{
    public void Do()
    {
        MyAbstractMethod();
    }

    public abstract void MyAbstractMethod();
}
请注意,我将 CallBase 设置为将其转换为部分模拟,以防 Do 是虚拟的。否则,Moq 将替换 Do 方法的实现。
使用 Protected(),您可以以类似的方式验证已调用受保护的方法。
当您使用 Moq 或其他库创建模拟时,重写实现是整个重点。测试受保护的方法涉及公开现有实现。这不是 Moq 设计的目的。Protected() 只是提供了访问(可能是通过反射,因为它是基于字符串的)来覆盖受保护成员的方法。
要么编写一个测试后代类,其中包含调用您的受保护方法的方法,要么在单元测试中使用反射调用受保护的方法。
或者更好的方法是,不直接测试受保护的方法。

好的,谢谢指出!抱歉我匆忙写下这个问题,因为我要赶火车;-)现在我已经更新了它。当然,我的意思是在抽象类上测试非抽象方法。 - magritte
测试子类的方法是我已经在使用的,我只是想知道Moq是否有一种可以帮我完成这个过程的方式。这个想法来自于这个问题:https://dev59.com/9Wsz5IYBdhLWcg3wv6fu - magritte
@TonyLeeper:据我所知没有。你可以编写一个反射助手,允许你访问受保护的方法;这将非常简短。 - TrueWill
1
如果您正在实现一个库,比如说一个基类,它提供的功能不是公共的,但肯定是由派生类使用的,那么您应该能够对其进行测试。另外,值得一提的是,protected 对于继承层次结构来说是公共的。例如,创建 XML 文档时,未注释的 protected 成员将生成警告。 - mdarefull
问题是关于 protected 方法的,不是 public 的。 - Yousha Aleayoub
显示剩余5条评论

7

您已经提到了“测试公共API而不是私有API”的思路,同时也提到了从类继承并通过此方式测试其受保护成员的技巧。这两种方法都是有效的。

在这背后,简单的事实是,您认为这个实现细节(因为私有或受保护成员就是如此)足够重要,以至于需要直接测试它而不是通过使用它的公共API间接测试。如果它确实如此重要,也许它足够重要,可以晋升为自己的类。(毕竟,如果它如此重要,也许这是一个MyAbstractClass不应该具有的责任。)该类的实例将受到MyAbstractClass的保护,因此只有基类和派生类型才能访问该实例,但该类本身在其他方面完全可测试且可在其他地方使用,如果需要的话。

abstract class MyAbstractClass 
{
     protected ImportantMethodDoer doer;
}

class ImportantMethodDoer
{
     public void Do() { }
}

否则,您只能使用已经识别出的方法。
*Moq可能会提供一些机制来访问私有或受保护的成员,但我不能确定,因为我不使用该特定工具。我的答案更多地从架构角度考虑。

嗨,安东尼,我同意你的所有观点,但是所讨论的方法是在Asp.Net MVC Web应用程序的global.asax.cs中的Application_AcquireRequestState(object sender, EventArgs e)事件处理程序。它不是我的代码库的一部分。我可以将该方法标记为public,但这只是为了测试目的。 - magritte

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