关于c#:公开私有方法对其进行单元测试…好主意?

Making a private method public to unit test it…good idea?

版主注:这里已经发布了39个答案(有些已被删除)。在发布答案之前,请考虑是否可以为讨论添加一些有意义的内容。你很可能只是重复别人已经说过的话。

我偶尔会发现自己需要在类中公开一个私有方法,只是为了为它编写一些单元测试。

通常这是因为该方法包含类中其他方法之间共享的逻辑,并且更整洁地单独测试逻辑,或者另一个原因可能是我希望测试同步线程中使用的逻辑,而不必担心线程问题。

其他人发现自己这样做是因为我真的不喜欢这样做吗??我个人认为奖金超过了公开一种方法的问题,这种方法并不能真正提供课外服务…

更新

感谢大家的回答,似乎激起了人们的兴趣。我认为普遍的共识是应该通过公共API进行测试,因为这是使用类的唯一方式,我同意这一点。我上面提到的几个我会在上面做这些的案例都是不常见的,我认为这样做的好处是值得的。

然而,我可以看到每个人都指出,这不应该真的发生。当我更多地考虑它的时候,我认为改变你的代码来适应测试是一个坏主意——毕竟我认为测试在某种程度上是一个支持工具,如果你愿意的话,改变一个系统来"支持一个支持工具"是明显的坏做法。


Note:
This answer was originally posted for the question Is unit testing alone ever a good reason to expose private instance variables via getters? which was merged into this one, so it may be a tad specific to the usecase presented there.

作为一个一般性的声明,我通常都是为了重构"生产"代码以使其更容易测试。不过,我不认为这是个好电话。好的单元测试(通常)不应该关心类的实现细节,只关心它的可见行为。您可以测试类在调用first()last()之后是否按预期的顺序返回页面,而不是将内部堆栈公开给测试。

例如,考虑以下伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class NavigationTest {
    private Navigation nav;

    @Before
    public void setUp() {
        // Set up nav so the order is page1->page2->page3 and
        // we've moved back to page2
        nav = ...;
    }

    @Test
    public void testFirst() {
        nav.first();

        assertEquals("page1", nav.getPage());

        nav.next();
        assertEquals("page2", nav.getPage());

        nav.next();
        assertEquals("page3", nav.getPage());
    }

    @Test
    public void testLast() {
        nav.last();

        assertEquals("page3", nav.getPage());

        nav.previous();
        assertEquals("page2", nav.getPage());

        nav.previous();
        assertEquals("page1", nav.getPage());
    }
}


就我个人而言,我更愿意使用公共API进行单元测试,而且我绝对不会为了方便测试而将私有方法公开。

如果您确实想单独测试私有方法,在Java中可以使用EasyMoCK/PrimeMcCK来实现这一点。

你必须实事求是,你也应该知道为什么事情很难测试的原因。

"听测试"——如果很难测试,那是不是告诉了你一些关于你的设计?您能否重构到这样一个地方:通过公共API进行测试,这个方法的测试将是琐碎且容易覆盖的?

以下是Michael Feathers在"有效地处理遗留代码"中所说的话

"Many people spend a lot of time trying ot figure out how to get around this problem ... the real answer is that if you have the urge to test a private method, the method shouldn't be private; if making the method public bothers you, chances are, it is because it is part of a separate reponsibility; it should be on another class." [Working Effectively With Legacy Code (2005) by M. Feathers]


正如其他人所说,有点怀疑是单元测试私有方法;单元测试公共接口,而不是私有实现细节。

也就是说,当我想对C中私有的东西进行单元测试时,我使用的技术是将可访问性保护从私有降级为内部,然后使用InternalsVisibleTo将单元测试程序集标记为友元程序集。单元测试组件将被允许将内部构件视为公共的,但是您不必担心意外地添加到公共的表面积中。


很多答案都建议只测试公共接口,但我认为这是不现实的——如果一个方法做了5个步骤的事情,那么您将希望分别测试这5个步骤,而不是全部测试。这就需要测试所有五种方法,否则这五种方法(测试除外)可能是private

测试"private"方法的常用方法是给每个类自己的接口,并使"private"方法public,但不包括在接口中。这样,它们仍然可以被测试,但是它们不会膨胀接口。

是的,这将导致文件和类膨胀。

是的,这确实使publicprivate说明符冗余。

是的,这是屁股疼。

不幸的是,这是我们为使代码可测试而做出的众多牺牲之一。也许未来的语言(或者甚至是未来版本的C /爪哇)将具有使类和模块可测试性更方便的特性,但同时,我们必须跳过这些环。

有些人会争辩说,这些步骤中的每一个都应该是自己的类,但我不同意——如果它们都共享状态,就没有理由在五个方法可以做到的地方创建五个独立的类。更糟糕的是,这会导致文件和类膨胀。此外,它还会影响模块的公共API——如果您想从另一个模块测试这些类(或者将测试代码包含在同一个模块中,这意味着将测试代码与产品一起装运),那么所有这些类都必须是public


单元测试应该测试公共契约,这是类如何在代码的其他部分中使用的唯一方法。私有方法是实现细节,您不应该测试它,只要公共API工作正常,实现就不重要,并且可以在不改变测试用例的情况下进行更改。


在我看来,您应该编写测试,而不是对类如何在内部实现进行深入的假设。您可能希望稍后使用另一个内部模型对其进行重构,但仍然要做出与前一个实现相同的保证。

记住这一点,我建议您集中精力测试您的合同是否仍然有效,不管您的类目前有什么内部实现。公共API的基于属性的测试。


让它成为私有包怎么样?然后您的测试代码可以看到它(以及包中的其他类),但它仍然对您的用户隐藏。

但实际上,您不应该测试私有方法。这些是实施细节,不是合同的一部分。它们所做的一切都应该通过调用公共方法来覆盖(如果其中有公共方法未执行的代码,那么应该这样做)。如果私有代码太复杂,那么类可能做了太多的事情,并且缺少重构。

公开一种方法是很大的承诺。一旦你这样做了,人们就可以使用它,你不能再改变它们了。


更新:我在许多其他地方为这个问题添加了一个更广泛、更完整的答案。这可以在我的博客上找到。

如果我需要公开一些东西来测试它,这通常意味着被测试的系统不遵循单一的责任性原则。因此,应该引入一个缺少的类。将代码提取到新类中后,将其公开。现在您可以很容易地进行测试,并且您正在遵循SRP。您的另一个类只需通过组合调用这个新类。

公开方法/使用langauge技巧(如将代码标记为对测试组合可见)应始终是最后的手段。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SystemUnderTest
{
   public void DoStuff()
   {
      // Blah
      // Call Validate()
   }

   private void Validate()
   {
      // Several lines of complex code...
   }
}

通过引入一个验证器对象来重构这个。

1
2
3
4
5
6
7
8
public class SystemUnderTest
{
    public void DoStuff()
    {
       // Blah
       validator.Invoke(..)
    }
}

现在我们要做的就是测试验证程序是否被正确调用。验证的实际过程(以前的私有逻辑)可以在纯隔离中进行测试。不需要进行复杂的测试设置来确保验证通过。


一些很好的答案。我没有看到提到的一件事是,使用测试驱动开发(TDD),私有方法是在重构阶段创建的(查看提取方法以获得重构模式的示例),因此应该已经有了必要的测试覆盖范围。如果做得正确(当然,当涉及到正确性时,你会得到一个混合的意见包),你不必担心必须公开一个私有方法,这样你就可以测试它。


为什么不把堆栈管理算法分解成一个实用程序类呢?实用程序类可以管理堆栈并提供公共访问器。它的单元测试可以集中在实现细节上。算法复杂类的深度测试对于解决边缘情况和确保覆盖非常有帮助。

然后,当前类可以干净地委托给实用程序类,而不公开任何实现细节。它的测试将与其他人推荐的分页要求相关。


在爪哇,也有选择使它包私有(即离开能见度修改器)。如果单元测试与被测试的类在同一个包中,那么它应该能够看到这些方法,并且比将该方法完全公开要安全一些。


私有方法通常用作"助手"方法。因此,它们只返回基本值,从不在对象的特定实例上操作。

如果您想测试它们,您有几个选项。

  • 使用反射
  • 授予方法包访问权限

或者,您可以使用helper方法创建一个新的类作为公共方法,如果它是一个新类的足够好的候选者。

这方面有一篇很好的文章。


如果您使用C,您可以将方法设置为内部方法。这样就不会污染公共API。

然后将属性添加到dll

[程序集:InternalsVisibleTo("MyTestAssembly")]

现在,所有方法都在MyTestAssembly项目中可见。也许不完美,但最好是公开私有方法来测试它。


如果需要,可以使用反射来访问私有变量。

但实际上,您并不关心类的内部状态,您只想测试公共方法是否返回了您可以预期的情况下所期望的内容。


在单元测试方面,你绝对不应该增加更多的方法;我相信你最好做一个关于你的first()方法的测试案例,在每次测试之前都会调用它;然后你可以多次调用-next()previous()last()来看看结果是否符合你的期望。我想如果你不在你的类中添加更多的方法(只是为了测试的目的),你会坚持"黑盒"的测试原则;


我会说这是个坏主意,因为我不确定你是否从中得到任何好处和潜在的问题。如果您要更改调用的约定,只是为了测试私有方法,那么您不是在测试类的使用方式,而是创建一个您从未打算发生的人工场景。

此外,通过将方法声明为公共方法,可以说在六个月内(在忘记将方法公开的唯一原因是为了测试之后),您(或者如果您已经移交了项目)完全不同的人不会使用它,从而导致潜在的意外后果和/或维护噩梦。


首先看看是否应该将该方法提取到另一个类中并公开。如果不是这样的话,让它受到包保护,在Java中用@ VisualFielTestEngt注解。


在您的更新中,您说只使用公共API进行测试是很好的。这里实际上有两所学校。

  • 黑盒测试

    黑匣子学校说,这个类应该被视为一个黑匣子,没有人能看到里面的实现。唯一的测试方法是通过公共API——就像类的用户将使用它一样。

  • 白盒测试。

    白盒学校自然地认为,它使用有关课程实施的知识,然后测试课程,以了解它应该如何工作。

  • 我真的不能站在讨论的一边。我只是想知道有两种不同的方法来测试一个类(或者一个库或者其他什么的)是很有趣的。


    要单独测试的私有方法表明类中隐藏了另一个"概念"。将这个"概念"提取到它自己的类中,并作为一个单独的"单元"对其进行测试。

    请看这段视频,了解这个主题的真正有趣之处。


    实际上,在某些情况下,您应该这样做(例如,当您正在实现一些复杂的算法时)。只需将它打包为私有,这就足够了。但在大多数情况下,您可能有太复杂的类,需要将逻辑分解到其他类中。


    你永远不应该让你的测试命令你的代码。我不是说TDD或其他DDS,我的意思是,确切地说,你要什么。你的应用需要这些方法公开吗?如果有,那么测试它们。如果没有,那么就不要仅仅为了测试而公开它们。变量和其他变量也一样。让应用程序的需求指定代码,让测试测试满足需求。(同样,我不是说先测试或者不测试,我的意思是改变类结构以实现测试目标)。

    相反,你应该"测试更高"。测试调用私有方法的方法。但是您的测试应该测试您的应用程序需求,而不是您的"实现决策"。

    例如(此处为BOD伪代码);

    1
    2
    3
    4
    5
    6
       public int books(int a) {
         return add(a, 2);
       }
       private int add(int a, int b) {
         return a+b;
       }

    没有理由测试"添加",你可以测试"书"。

    永远不要让您的测试为您做代码设计决策。测试你是否得到了预期的结果,而不是你如何得到那个结果。


    我通常将这些方法保留为protected,并将单元测试放在同一个包中(但放在另一个项目或源文件夹中),在那里它们可以访问所有受保护的方法,因为类加载器将它们放在同一名称空间中。


    我经常在类中添加一个名为validateverifycheck等的方法,以便调用它来测试对象的内部状态。

    有时这个方法被封装在一个IFDEF块中(我主要写在C++中),这样它就不会被编译以释放。但是,在发行版中,提供验证方法来检查程序的对象树,这通常很有用。


    我倾向于同意,让IT部门测试的好处大于增加一些成员的可见性的问题。稍微的改进是使其受保护和虚拟化,然后在测试类中重写它以公开它。

    或者,如果您想单独测试它的功能,它是否不建议您的设计中缺少一个对象?也许您可以将它放在一个单独的可测试类中……然后您的现有类只委托给这个新类的一个实例。


    guava有一个@visiblefortesting注释,用于标记扩大了范围(package或public)的方法,否则将扩大范围。我对同一件事使用了@private注释。

    虽然必须测试公共API,但有时获取通常不公开的内容既方便又明智。

    什么时候?

    • 在toto中,通过将一个类分成多个类,使它的可读性显著降低,
    • 只是为了让它更具可测试性,
    • 提供一些进入内脏的测试通道就可以做到这一点。

    宗教似乎胜过工程学。


    我通常将测试类保存在与被测试类相同的项目/程序集中。这样,我只需要internal可见性就可以使函数/类成为可测试的。

    这使您的构建过程有些复杂,需要过滤掉测试类。我通过将所有测试类命名为TestedClassTest并使用regex过滤这些类来实现这一点。

    当然,这只适用于你问题的C/.NET部分。


    不,因为有更好的剥那只猫皮的方法。

    一些单元测试工具依赖于类定义中的宏,当在测试模式中构建时,宏会自动扩展以创建挂钩。非常C风格,但它是有效的。

    一个更简单的OO习语是让您想要测试的任何东西"受保护"而不是"私有"。测试工具从被测试的类继承,然后可以访问所有受保护的成员。

    或者你选择"朋友"选项。就个人而言,这是C++的一个特点,因为它打破了封装规则,但它恰巧是C++实现某些特性所必需的,所以嘿嘿。

    不管怎样,如果您是单元测试,那么您同样可能需要向这些成员注入值。白盒短信是完全有效的。这真的会破坏你的封装。


    在.NET中有一个特殊的类,名为PrivateObject,专门设计为允许您访问类的私有方法。

    在msdn或堆栈溢出上查看更多关于它的信息

    (我想知道到目前为止还没有人提到过。)

    在有些情况下,这是不够的,在这种情况下,你将不得不使用反射。

    尽管如此,我还是坚持不测试私有方法的一般性建议,但是通常也有例外。


    正如其他人的评论所广泛指出的,单元测试应该关注公共API。但是,撇开优缺点和理由不谈,您可以使用反射在单元测试中调用私有方法。当然,您需要确保您的JRE安全性允许它。调用私有方法是Spring框架与ReflectionUtils一起使用的方法(参见makeAccessible(Method)方法)。

    下面是一个带有私有实例方法的小示例类。

    1
    2
    3
    4
    5
    public class A {
        private void doSomething() {
            System.out.println("Doing something private.");
        }
    }

    以及执行私有实例方法的示例类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    public class B {
        public static final void main(final String[] args) {
            try {
                Method doSomething = A.class.getDeclaredMethod("doSomething");
                A o = new A();
                //o.doSomething(); // Compile-time error!
                doSomething.setAccessible(true); // If this is not done, you get an IllegalAccessException!
                doSomething.invoke(o);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (SecurityException e) {
                e.printStackTrace();
            }
        }
    }

    执行b,将打印Doing something private.,如果您真的需要它,反射可以在单元测试中用于访问私有实例方法。


    一切都是实用主义。您的单元测试在某种程度上是代码的客户机,为了获得良好的代码覆盖率,您需要使代码具有可测试性。如果测试代码是非常复杂的,那么您的解决方案将是一个潜在的失败,以便您能够在没有有效公共接缝的情况下设置必要的角落案例。使用国际奥委会在这方面也有帮助。


    单元测试的重点是确认该单元的公共API的工作。应该不需要只为测试而公开一个私有方法,如果是这样,那么应该重新考虑您的接口。私有方法可以被认为是公共接口的"助手"方法,因此可以通过公共接口进行测试,因为它们将调用私有方法。

    我能看到您有"需要"这样做的唯一原因是您的类没有被正确地设计为要进行测试。


    回答得很好。ihmo,@blueraja-danny-pflughoeft的最佳答案之一。好的。

    Lots of answers suggest only testing the public interface, but IMHO
    this is unrealistic - if a method does something that takes 5 steps,
    you'll want to test those five steps separately, not all together.
    This requires testing all five methods, which (other than for testing)
    might otherwise be private.

    Ok.

    首先,我要强调的是,"我们应该公开一个私有方法来进行单元测试吗"这个问题是一个客观正确的答案取决于多个参数的问题。所以我认为在某些情况下我们不需要,而在其他情况下我们应该。好的。

    这里有些答案可以概括为:"这样做通常是好的",或者"从不,这是坏的"。不要欺骗API,只测试公共行为"。这让我非常恼火,因为测试和实现的设计质量是一个重要的问题,这个问题意味着对两者都有许多后果。好的。

    将公共方法设为私有方法还是将私有方法提取为其他类(新的或现有的)中的公共方法?好的。

    对于可接受扩大private方法可见性的情况,依赖于制作private方法public的解决方案通常不是最佳方法。它降低了设计质量和类的可测试性。单元测试必须测试一个API方法/函数的行为。如果测试一个调用属于同一组件的另一个public方法的public方法,则不会对该方法进行单元测试。同时测试多个public方法。因此,您可以复制测试、测试夹具、测试断言、测试维护以及更一般的应用程序设计。随着测试值的降低,他们经常会对编写或维护它们的开发人员失去兴趣。好的。

    为了避免所有这些重复,在许多情况下,一个更好的解决方案是在新类或现有类中提取private方法作为public方法,而不是生成private方法public方法。它不会造成设计缺陷。它将使代码更有意义,减少类的膨胀。此外,有时private方法是类的一个例程/子集,而行为在特定的结构中更适合。最后,使代码更具可测试性,避免了测试重复。我们确实可以通过在自己的测试类和客户机类的测试类中对public方法进行单元测试来防止测试重复,我们只需模拟依赖性即可。好的。

    嘲笑私人方法?好的。

    当然,使用反射或将工具作为powermock是可能的,但ihmo我认为这通常是绕过设计问题的一种方法。测试类是另一个类。private构件的设计不允许暴露于其他类别。所以对于测试类,我们应该遵循相同的规则。好的。

    模拟被测对象的公共方法?好的。

    您可以将修改器private更改为public,以测试该方法。然后,为了测试使用该重构公共方法的方法,您可能会尝试使用工具作为mockito(spy概念)来模拟重构的public方法,但与模拟private方法类似,我们应该避免模拟被测对象。好的。

    Mockito.spy()文件称:好的。

    Creates a spy of the real object. The spy calls real methods unless they are > > stubbed.

    Ok.

    Real spies should be used carefully and occasionally, for example when
    dealing with legacy code.

    Ok.

    根据经验,使用spy()通常会降低测试质量和可读性。此外,它更容易出错,因为被测对象既是模拟对象,又是真实对象。这通常是编写无效验收测试的最佳方法。好的。

    下面是我用来决定private方法是应保持private还是应重构的准则。好的。

    案例1)如果该方法被调用一次,则永远不要生成private方法public。对于单个方法,它是一个private方法。所以您永远不能复制测试逻辑,因为它只被调用一次。好的。

    案例2)如果多次调用private方法,您应该怀疑是否应该将private方法重构为public方法。好的。

    如何决定?好的。

    • private方法在测试中不会产生重复。->保持方法的私有性。好的。

    • private方法在测试中产生重复。也就是说,您需要重复一些测试,为单元使用private方法测试public方法的每个测试断言相同的逻辑。->如果重复处理可能使提供给客户的API成为一部分(没有安全问题、没有内部处理等),则将private方法提取为新类中的public方法。->否则,如果重复处理不需要将提供给客户的API的一部分(安全问题、内部处理等),则不要将private方法的可见性扩大到public。您可以保持它不变,或者将方法移动到一个private包类中,该类永远不会成为API的一部分,也永远不会被客户机访问。好的。

    代码示例好的。

    实例依赖于Java和以下库:JUnit、AsjtJJ(断言匹配器)和MoCito。但我认为整体方法对C也是有效的。好的。

    1)private方法不在测试代码中创建重复的示例好的。

    这里是一个Computation类,它提供执行某些计算的方法。所有公共方法都使用mapToInts()方法。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class Computation {

        public int add(String a, String b) {
            int[] ints = mapToInts(a, b);
            return ints[0] + ints[1];
        }

        public int minus(String a, String b) {
            int[] ints = mapToInts(a, b);
            return ints[0] - ints[1];
        }

        public int multiply(String a, String b) {
            int[] ints = mapToInts(a, b);
            return ints[0] * ints[1];
        }

        private int[] mapToInts(String a, String b) {
            return new int[] { Integer.parseInt(a), Integer.parseInt(b) };
        }

    }

    测试代码如下:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class ComputationTest {

        private Computation computation = new Computation();

        @Test
        public void add() throws Exception {
            Assert.assertEquals(7, computation.add("3","4"));
        }

        @Test
        public void minus() throws Exception {
            Assert.assertEquals(2, computation.minus("5","3"));
        }

        @Test
        public void multiply() throws Exception {
            Assert.assertEquals(100, computation.multiply("20","5"));
        }

    }

    我们可以看到,调用private方法mapToInts()不会复制测试逻辑。它是一个中介操作,不会产生我们需要在测试中断言的特定结果。好的。

    2)当private方法在测试代码中产生不需要的重复时的示例好的。

    这里有一个MessageService类,它提供了创建消息的方法。所有public方法均采用createHeader()方法:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class MessageService {

        public Message createMessage(String message, Credentials credentials) {
            Header header = createHeader(credentials, message, false);
            return new Message(header, message);
        }

        public Message createEncryptedMessage(String message, Credentials credentials) {
            Header header = createHeader(credentials, message, true);
            // specific processing to encrypt
            // ......
            return new Message(header, message);
        }

        public Message createAnonymousMessage(String message) {
            Header header = createHeader(Credentials.anonymous(), message, false);
            return new Message(header, message);
        }

        private Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
            return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
        }

    }

    测试代码如下:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    import java.time.LocalDate;

    import org.assertj.core.api.Assertions;
    import org.junit.Test;

    import junit.framework.Assert;

    public class MessageServiceTest {

        private MessageService messageService = new MessageService();

        @Test
        public void createMessage() throws Exception {
            final String inputMessage ="simple message";
            final Credentials inputCredentials = new Credentials("user","pass");
            Message actualMessage = messageService.createMessage(inputMessage, inputCredentials);
            // assertion
            Assert.assertEquals(inputMessage, actualMessage.getMessage());
            Assertions.assertThat(actualMessage.getHeader())
                      .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                      .containsExactly(inputCredentials, 9, LocalDate.now(), false);
        }

        @Test
        public void createEncryptedMessage() throws Exception {
            final String inputMessage ="encryted message";
            final Credentials inputCredentials = new Credentials("user","pass");
            Message actualMessage = messageService.createEncryptedMessage(inputMessage, inputCredentials);
            // assertion
            Assert.assertEquals("A?4B36ddflm1Dkok49d1d9gaz", actualMessage.getMessage());
            Assertions.assertThat(actualMessage.getHeader())
                      .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                      .containsExactly(inputCredentials, 9, LocalDate.now(), true);
        }

        @Test
        public void createAnonymousMessage() throws Exception {
            final String inputMessage ="anonymous message";
            Message actualMessage = messageService.createAnonymousMessage(inputMessage);
            // assertion
            Assert.assertEquals(inputMessage, actualMessage.getMessage());
            Assertions.assertThat(actualMessage.getHeader())
                      .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                      .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false);
        }

    }

    我们可以看到,调用private方法createHeader()会在测试逻辑中产生一些重复。createHeader()确实产生了一个我们需要在测试中断言的特定结果。我们断言头内容的3倍,而需要一个断言。好的。

    我们还可以注意到,断言复制在方法之间很接近,但不是必需的,正如private方法具有特定的逻辑:当然,根据private方法的逻辑复杂性,我们可以有更多的区别。此外,每次在MessageService中添加一个新的public方法,调用createHeader()时,我们都必须添加这个断言。还要注意,如果createHeader()修改了它的行为,所有这些测试也可能需要修改。最后,这不是一个很好的设计。好的。

    重构步骤好的。

    假设我们是在这样一种情况下,createHeader()是可以接受的,可以成为API的一部分。我们首先通过将createHeader()的访问修饰符改为public来重构MessageService类:好的。

    1
    2
    3
    public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
        return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
    }

    我们现在可以测试单一的这种方法:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Test
    public void createHeader_with_encrypted_message() throws Exception {
      ...
      boolean isEncrypted = true;
      // action
      Header actualHeader = messageService.createHeader(credentials, message, isEncrypted);
      // assertion
      Assertions.assertThat(actualHeader)
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), true);
    }

    @Test
    public void createHeader_with_not_encrypted_message() throws Exception {
      ...
      boolean isEncrypted = false;
      // action
      messageService.createHeader(credentials, message, isEncrypted);
      // assertion
      Assertions.assertThat(actualHeader)
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false);

    }

    但是我们之前为使用createHeader()的类的public方法编写的测试呢?差别不大。实际上,我们仍然很恼火,因为这些public方法仍然需要对返回的头值进行测试。如果我们删除这些断言,我们可能无法检测到它的回归。我们应该能够自然地隔离这一过程,但我们不能,因为createHeader()方法属于被测组件。这就是为什么我在回答的开头解释说,在大多数情况下,我们应该倾向于在另一个类中提取private方法,而不是更改public的访问修饰符。好的。

    因此,我们介绍了HeaderService:好的。

    1
    2
    3
    4
    5
    6
    7
    public class HeaderService {

        public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
            return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
        }

    }

    我们迁移了HeaderServiceTest中的createHeader()测试。好的。

    现在,MessageService定义为HeaderService依赖关系:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class MessageService {

        private HeaderService headerService;

        public MessageService(HeaderService headerService) {
            this.headerService = headerService;
        }

        public Message createMessage(String message, Credentials credentials) {
            Header header = headerService.createHeader(credentials, message, false);
            return new Message(header, message);
        }

        public Message createEncryptedMessage(String message, Credentials credentials) {
            Header header = headerService.createHeader(credentials, message, true);
            // specific processing to encrypt
            // ......
            return new Message(header, message);
        }

        public Message createAnonymousMessage(String message) {
            Header header = headerService.createHeader(Credentials.anonymous(), message, false);
            return new Message(header, message);
        }

    }

    MessageService测试中,我们不再需要断言每个头值,因为已经测试过了。我们只想确保Message.getHeader()返回HeaderService.createHeader()返回的内容。好的。

    例如,这里是新版本的createMessage()测试:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Test
    public void createMessage() throws Exception {
        final String inputMessage ="simple message";
        final Credentials inputCredentials = new Credentials("user","pass");
        final Header fakeHeaderForMock = createFakeHeader();
        Mockito.when(headerService.createHeader(inputCredentials, inputMessage, false))
               .thenReturn(fakeHeaderForMock);
        // action
        Message actualMessage = messageService.createMessage(inputMessage, inputCredentials);
        // assertion
        Assert.assertEquals(inputMessage, actualMessage.getMessage());
        Assert.assertSame(fakeHeaderForMock, actualMessage.getHeader());
    }

    注意,assertSame()用于比较头对象引用,而不是内容引用。现在,HeaderService.createHeader()可能会改变其行为并返回不同的值,从MessageService测试的角度来看,这并不重要。好的。好啊。


    就个人而言,我在测试私有方法时也有同样的问题,这是因为一些测试工具是有限的。如果有限的工具不能响应你的需求,那么用它们驱动你的设计是不好的。改变工具,而不是设计。因为您对C语言的需求不能提出好的测试工具,但是对于Java来说,有两个强大的工具:TestNG和PosiMoCK,并且可以为.NET平台找到相应的测试工具。