调用相同类中其他方法的单元测试方法的最佳方法


35

我最近正在和一些朋友讨论以下两种方法中哪一种最适合从同一个类中的方法中返回结果或调用同一个类中的方法。

这是一个非常简化的示例。实际上,功能要复杂得多。

例:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

因此,要测试这一点,我们有2种方法。

方法1:使用函数和操作来替换方法的功能。例:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

方法2:使成员成为虚拟成员,派生类,并在派生类中使用“功能和操作”替换功能示例:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

我想知道更好,为什么?

更新:注意:FunctionB也可以是公共的


您的示例很简单,但并不完全正确。FunctionA返回布尔值,但仅设置局部变量x,不返回任何内容。
Eric P.

1
在此特定示例中,FunctionB可以public static处于不同的类中。

对于代码审查,您应该发布实际代码而不是其简化版本。请参阅常见问题解答。就其立场而言,您正在问一个特定的问题,而不是寻找代码审查。
Winston Ewert

1
FunctionB被设计破坏了。new Random().Next()几乎总是错的。您应该注入的实例Random。(Random这也是一个设计不良的类,可能会导致一些其他问题)
CodesInChaos

在通过代表从更一般DI是精绝恕我直言
JK。

Answers:


32

在原始海报更新后进行编辑。

免责声明:不是C#程序员(主要是Java或Ruby)。我的答案是:我根本不会测试它,而且我不认为您应该这样做。

较长的版本是:私有/受保护的方法不是API的组成部分,它们基本上是实现选择,您可以决定对其进行查看,更新或完全丢弃,而不会影响外部。

我想您对FunctionA()进行了测试,这是从外部可见的类的一部分。它应该是唯一具有实施合同(并且可以进行测试)的合同。您的私人/受保护方法没有履行和/或测试的合同。

在那里查看相关讨论:https : //stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

注释之后,如果FunctionB是公共的,我将仅使用单元测试对它们进行测试。您可能会认为FunctionA的测试并非完全是“单元”(因为它称为FunctionB),但我对此不必太担心:如果FunctionB测试有效但FunctionA测试无效,则意味着问题不在FunctionB的子域,对我来说,足以作为区分符。

如果您确实希望能够将两个测试完全分开,那么在测试FunctionA时(通常,返回固定的已知正确值),我将使用某种模拟技术来模拟FunctionB。我缺乏C#生态系统知识来建议特定的模拟库,但是您可能会看到这个问题


2
完全同意@Martin的答案。为类编写单元测试时,不应测试方法。您正在测试的是一种行为,即满足合同(应该做什么类的声明)。因此,您的单元测试应涵盖该类(使用公共方法/属性)公开的所有要求,包括特殊情况

您好,感谢您的答复,但没有回答我的问题。我不关心FunctionB是私有的还是受保护的。它也可以是公共的,仍然可以从FunctionA调用。

在不重新设计基类的情况下,处理此问题的最常见方法是MyClass使用您想存根的功能对方法进行子类化和覆盖。将您的问题更新为FunctionB可能公开的也是一个好主意。
Eric P.

1
protected方法是类的公共表面的一部分,除非您确保在不同的程序集中没有类的实现。
CodesInChaos

2
从单元测试的角度来看,FunctionA调用FunctionB的事实是无关紧要的细节。如果正确编写了FunctionA的测试,则这是一个实现细节,可以稍后在不中断测试的情况下进行重构(只要FunctionA的总体行为保持不变)。真正的问题是,FunctionB的随机数检索需要使用注入的对象来完成,以便您可以在测试过程中使用模拟来确保返回已知数。这使您可以测试众所周知的输入/输出。
丹·里昂斯

11

我赞成这样一种理论,即如果一个功能对测试很重要或对替换很重要,那么它就足够重要,而不是成为被测类的私有实现细节,而是成为不同类的公共实现细节。

因此,如果我处于

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

然后我要重构。

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

现在,我有一个方案,其中D()是可独立测试的,并且可以完全替换。

作为组织的一种方式,我的协作者可能不在同一个命名空间级别。例如,如果A在FooCorp.BLL中,则我的协作者可能会更深一层,例如在FooCorp.BLL.Collaborators中(或任何合适的名称)。我的合作者可能还只能通过internalaccess修饰符在程序集内部可见,然后我也可以通过InternalsVisibleToassembly属性将其公开给我的单元测试项目。得出的结论是,就调用者而言,您仍然可以保持API干净,同时生成可验证的代码。


是,如果ICollaborator需要几种方法。如果你有一个对象是谁的唯一工作就是包装一个单一的方法,我宁愿看到它与委托虽然取代。
jk。

您必须确定命名委托是否有意义,或者接口是否有意义,而我不会为您确定。就个人而言,我并不反对单一(公共)方法类。越小越好,因为它们变得越来越容易理解。
安东尼·佩格拉姆

0

再加上马丁指出,

如果您的方法是私有/受保护的-请勿对其进行测试。它是类的内部内容,不应在类外部访问。

在您提到的两种方法中,我都有这些担忧-

方法1-这实际上更改了测试中被测类的行为。

方法2-这实际上不测试生产代码,而是测试另一个实现。

在陈述的问题中,我看到A的唯一逻辑是查看FunctionB的输出是否为偶数。尽管是说明性的,但FunctionB给出了Random值,很难测试。

我期望有一个现实的场景,在这里我们可以设置MyClass,以便我们知道FunctionB将返回什么。然后我们的预期结果已知,我们可以调用FunctionA并对实际结果进行断言。


3
protected与几乎相同public。仅privateinternal是实施细节。
CodesInChaos

@codeinchaos-我很好奇。对于测试,除非您修改程序集属性,否则受保护的方法是“私有”的。仅派生类型有权访问受保护的成员。除了虚拟之外,我不明白为什么从测试中应该将受保护的处理方式与公共处理类似。你能详细说明一下吗?
Srikanth Venugopalan

由于这些派生类可以位于不同的程序集中,因此它们会暴露给第三方代码,从而成为类的公开表露的一部分。要测试它们,您可以使它们internal protected,使用私有反射帮助器或在测试项目中创建派生类。
CodesInChaos

@CodesInChaos同意派生类可以在不同的程序集中,但范围仍限于基本类型和派生类型。修改访问修饰符以使其可测试是我有点担心的事情。我已经做到了,但是对我来说似乎是一种反模式。
Srikanth Venugopalan

0

我个人使用Method1,即将所有方法都转换为Actions或Funcs,因为这对我来说极大地提高了代码可测试性。与任何解决方案一样,此方法也有优缺点:

优点

  1. 允许使用简单的代码结构,其中仅将代码模式用于单元测试可能会增加额外的复杂性。
  2. 允许密封类,并消除流行的模拟框架(如Moq)所需的虚拟方法。密封类并消除虚拟方法使它们成为内联和其他编译器优化的候选对象。(https://msdn.microsoft.com/zh-cn/library/ff647802.aspx
  3. 简化可测试性,因为在单元测试中替换Func / Action实现就像将新值分配给Func / Action一样简单
  4. 还允许测试是否从另一个方法调用了静态Func,因为无法模拟静态方法。
  5. 易于将现有方法重构为Funcs / Action,因为在调用站点上调用方法的语法保持不变。(对于无法将方法重构为Func / Action的情况,请参见缺点)

缺点

  1. 如果可以派生您的类,则不能使用Funcs / Action,因为Funcs / Action没有像方法那样的继承路径
  2. 无法使用默认参数。使用默认参数创建Func需要创建一个新的委托,这可能会使代码根据使用情况而造成混乱
  3. 不能使用命名参数语法来调用类似方法(firstName:“ S”,lastName:“ K”)
  4. 最大的缺点是您无法访问Funcs and Actions中的“ this”引用,因此必须将对类的任何依赖项显式传递为参数。如您所知,所有依赖关系都很好,但是如果您有Func将依赖的许多属性,那么就不好了。您的里程将根据您的用例而有所不同。

综上所述,如果您知道自己的类永远不会被覆盖,那么使用Funcs和Actions进行单元测试非常有用。

另外,我通常不为Funcs创建属性,而是直接内联它们

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

希望这可以帮助!


-1

使用模拟可能。Nuget:https://www.nuget.org/packages/moq/

并相信我,它相当简单且有意义。

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

模拟需要虚拟方法来覆盖。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.