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


我偶尔发现自己需要将类中的私有方法设为public,只是为了为它编写一些单元测试。

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

其他人发现他们这样做是因为我不喜欢吗?我个人认为,公开一个方法的好处超过了它在类之外没有提供任何服务的问题……

更新

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

然而,我可以看到,每个人都指出它不应该真的发生。再仔细想想,我觉得改变你的代码来适应测试是一个坏主意——毕竟我认为测试在某种程度上是一个支持工具,而改变一个系统来“支持一个支持工具”是明显的坏做法。


就我个人而言,我宁愿使用公共API进行单元测试,我当然不会为了方便测试而将私有方法设为公共。

如果您真的想单独测试私有方法,那么在Java中可以使用Easymock / Powermock来实现这一点。

你必须务实,你也应该意识到为什么事情很难测试的原因。

“倾听测试”——如果测试很困难,这是否能告诉你关于你的设计的一些东西?你能不能重构到这样一个地方,对这个方法的测试将是微不足道的,并且很容易通过公共api进行测试?

以下是Michael Feathers在《有效使用遗留代码》一书中所说的话

“很多人花了很多时间试图弄清楚如何解决这个问题……真正的答案是,如果你想测试一个私有方法,这个方法不应该是私有的;如果公开方法让您感到困扰,很可能是因为它是单独责任的一部分;它应该上另一门课。”[有效地使用遗留代码(2005),作者:M. Feathers]

我倾向于认为进行单元测试所带来的好处超过了增加某些成员曝光度所带来的问题。稍微改进一下,将其设置为受保护的虚拟对象,然后在测试类中覆盖它以公开它。

或者,如果你想单独测试它的功能,它是否表明你的设计中缺少一个对象?也许你可以把它放在一个单独的可测试类中……那么你现有的类只是委托给这个新类的一个实例。

我想说这是一个坏主意,因为我不确定你是否会得到任何好处,并可能出现问题。如果你改变一个调用的契约,只是为了测试一个私有方法,你并不是在测试这个类将如何使用它,而是在创建一个你从未打算发生的人为场景。

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

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

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

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

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

在java中,还可以选择将其包设置为private(即去掉可见性修饰符)。如果您的单元测试与被测试的类在同一个包中,那么它应该能够看到这些方法,并且比将方法完全公开要安全一些。

实际上,有些情况下你应该这样做(例如,当你实现一些复杂的算法时)。只做package-private,这就足够了。 但在大多数情况下,你可能有太复杂的类,这就需要把逻辑分解到其他类中。

我通常将测试类保持在与测试类相同的项目/程序集中。 这样,我只需要内部可见性来使函数/类可测试。

这在一定程度上使您的构建过程变得复杂,需要过滤掉测试类。 我通过将所有测试类命名为TestedClassTest并使用正则表达式筛选这些类来实现这一点。

当然,这只适用于你的问题中的c# / .NET部分

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

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

使用反射 给这些方法包访问权

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

这里有一篇很好的文章。

就我个人而言,我在测试私有方法时也有同样的问题,这是因为一些测试工具是有限的。 如果你的设计被有限的工具所驱动,而这些工具并不能满足你的需求,那么你需要改变的是工具而不是设计。 因为你要求使用c#,我不能提出好的测试工具,但是对于Java,有两个强大的工具:TestNG和PowerMock,并且你可以找到对应的。net平台的测试工具

这都是实用主义。你的单元测试在某种程度上是代码的客户端,为了达到良好的代码覆盖率,你需要让你的代码非常可测试。如果为了让您能够在代码中没有有效的公共接缝的情况下设置必要的极端情况,测试代码非常复杂,那么您的解决方案将是潜在的失败。使用IoC也有助于解决这个问题。

单元测试的目的是确认该单元的公共api的工作情况。应该没有必要让一个私有方法只用于测试,如果是这样,那么你的接口应该重新考虑。私有方法可以被认为是公共接口的“助手”方法,因此通过公共接口进行测试,因为它们将调用私有方法。

我认为你有这样做的“需要”的唯一原因是你的类并不是为测试而设计的。

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

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

首先查看是否应该将该方法提取到另一个类中并将其设为public。如果不是这样的话,将其设置为包保护,并在Java中使用@VisibleForTesting进行注释。

我经常会在类中添加类似validate、verify、check等方法,这样就可以调用它来测试对象的内部状态。

有时这个方法被包装在一个ifdef块中(我主要是用c++写的),这样它就不会被编译发布。但是在发行版中,提供遍历程序对象树检查内容的验证方法通常是有用的。

正如其他人的评论所广泛指出的那样,单元测试应该关注公共API。然而,撇开利弊和论证不考虑,您可以通过使用反射在单元测试中调用私有方法。当然,您需要确保JRE安全性允许这样做。调用私有方法是Spring框架使用它的ReflectionUtils(参见makeAccessible(Method)方法)。

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

public class A {
    private void doSomething() {
        System.out.println("Doing something private.");
    }
}

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

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,将打印正在进行私有操作。如果确实需要,可以在单元测试中使用反射来访问私有实例方法。

一些很棒的答案。我没有看到提到的一件事是,在测试驱动开发(TDD)中,私有方法是在重构阶段创建的(关于重构模式的示例,请参阅Extract Method),因此应该已经具有必要的测试覆盖率。如果做得正确(当然,当涉及到正确性时,您将得到各种各样的意见),您应该不必担心必须将私有方法设为公共,以便您可以测试它。

如果你正在使用c#,你可以让方法是内部的。这样就不会污染公共API。

然后将属性添加到dll

[组装:InternalsVisibleTo(“MyTestAssembly”)]

现在所有的方法都在MyTestAssembly项目中可见。也许不完美,但总比为了测试而将私有方法设为公共要好。

许多答案建议只测试公共接口,但恕我直言,这是不现实的——如果一个方法执行的操作需要5个步骤,那么您将希望分别测试这5个步骤,而不是一起测试。这需要测试所有五个方法,否则它们(除了测试之外)可能是私有的。

测试“私有”方法的通常方法是给每个类一个自己的接口,并将“私有”方法设为公共的,但不将它们包含在接口中。这样,它们仍然可以被测试,但不会使界面膨胀。

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

是的,这确实使公共和私有说明符变得多余。

是啊,这真让人头疼。

不幸的是,这是我们为使代码可测试而做出的众多牺牲之一。也许未来的语言(或者甚至是c# /Java的未来版本)将具有使类和模块可测试性更加方便的特性;但与此同时,我们得跳过这些障碍。


There are some who would argue that each of those steps should be its own class, but I disagree - if they all share state, there is no reason to create five separate classes where five methods would do. Even worse, this results in file- and class-bloat. Plus, it infects the public API of your module - all those classes must necessarily be public if you want to test them from another module (either that, or include the test code in the same module, which means shipping the test code with your product).

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

黑盒测试 黑盒学派认为,类应该被视为一个黑盒,没有人可以看到其中的实现。测试这一点的唯一方法是通过公共API——就像类的用户将使用它一样。 白盒测试。 白盒学派认为自然地使用关于类实现的知识,然后测试类以了解它应该工作。

在这次讨论中我真的不能支持任何一方。我只是想知道有两种不同的方法来测试一个类(或库或其他)会很有趣。

Guava有一个@VisibleForTesting注释,用于标记那些扩大了作用域(包或公共)的方法。我使用@Private注释来做同样的事情。

虽然必须测试公共API,但有时获取通常不是公共的东西是方便和明智的。

当:

通过将一个类分解为多个类,类的可读性会显著降低, 为了让它更容易测试, 并且提供一些对内部的测试访问就可以做到这一点

宗教似乎战胜了工程学。

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

如果我需要公开一些东西来测试它,这通常暗示被测试的系统没有遵循单一责任原则。因此,应该引入一个缺失的类。将代码提取到一个新类后,将其设为public。现在您可以轻松地进行测试,并且遵循SRP。您的其他类只需通过组合调用这个新类。

使方法公开/使用语言技巧,例如将代码标记为对测试程序集可见,应该始终是最后的手段。

例如:

public class SystemUnderTest
{
   public void DoStuff()
   {
      // Blah
      // Call Validate()
   }

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

通过引入验证器对象重构此对象。

public class SystemUnderTest
{
    public void DoStuff()
    {
       // Blah
       validator.Invoke(..)
    }
}

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

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

看看这个关于这个话题的有趣视频。

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

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

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

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

记住这一点,我建议你把重点放在测试你的契约仍然存在,不管你的类目前有什么内部实现。基于属性的公共api测试。

注意: 这个答案最初是针对以下问题发布的:单独的单元测试是否是通过getter公开私有实例变量的好理由?这是合并到这个,所以它可能是一个特定于这里提出的用例。

一般来说,我通常都支持重构“生产”代码,以使其更容易测试。然而,我不认为这是一个好的决定。一个好的单元测试(通常)不应该关心类的实现细节,只关心它的可见行为。与其将内部堆栈暴露给测试,不如测试类在调用first()或last()后是否按照预期的顺序返回页面。

例如,考虑以下伪代码:

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());
    }
}

就单元测试而言,你绝对不应该添加更多的方法;我相信你最好做一个关于你的第一个()方法的测试用例,它将在每次测试之前被调用;然后,您可以多次调用- next(), previous()和last(),以查看结果是否符合您的期望。 我猜如果你不给你的类添加更多的方法(只是为了测试的目的),你会坚持测试的“黑盒”原则;

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

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

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

一些单元测试利用依赖于类定义中的宏,当在测试模式中构建时,这些宏会自动扩展以创建钩子。很有C风格,但是很好用。

一个更简单的OO习惯用法是使您想测试的任何东西都是“受保护的”,而不是“私有的”。测试工具继承自被测试类,然后可以访问所有受保护的成员。

或者你选择“朋友”。就我个人而言,这是我最不喜欢的c++特性,因为它打破了封装规则,但它恰好是c++实现某些特性所必需的。

无论如何,如果您正在进行单元测试,那么您很可能需要向这些成员中注入值。白盒短信是完全有效的。这真的会破坏您的封装。

You should never ever ever let your tests dictate your code. I'm not speaking about TDD or other DDs I mean, exactly what your asking. Does your app need those methods to be public. If it does then test them. If it does not then then don't make them public just for testing. Same with variables and others. Let your application's needs dictate the code, and let your tests test that the need is met. (Again I don't mean testing first or not I mean changing a classes structure to meet a testing goal).

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

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

   public int books(int a) {
     return add(a, 2);
   }
   private int add(int a, int b) {
     return a+b;
   } 

没有理由测试“add”,你可以测试“books”。

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

在. net中,有一个叫做PrivateObject的特殊类,专门用于允许您访问类的私有方法。

在MSDN或Stack Overflow上查看更多信息

(我很奇怪,到目前为止还没有人提到它。)

但在某些情况下,这是不够的,在这种情况下,你必须使用反射。

尽管如此,我还是会坚持不测试私有方法的一般建议,然而,像往常一样,总会有例外。

非常有答案的问题。 IHMO,来自@BlueRaja的精彩回答- Danny Pflughoeft是最好的回答之一。

许多答案建议只测试公共界面,但恕我直言 这是不现实的——如果一个方法只需要5个步骤, 您需要分别测试这五个步骤,而不是一起测试。 这需要测试所有五个方法,它们(除了测试之外) 否则可能是私人的。


最重要的是,我想强调的是,“我们是否应该将私有方法公开以进行单元测试”是一个客观正确答案取决于多个参数的问题。 所以我认为在某些情况下我们不需要这样做,而在其他情况下我们应该这样做。


将私有方法设为公共方法还是将私有方法提取为另一个类(新类或现有类)中的公共方法?

It is rarely the best way. A unit test has to test the behavior of one API method/function. If you test a public method that invokes another public method belonging to the same component, you don't unit test the method. You test multiple public methods at the same time. As a consequence, you may duplicate tests, test fixtures, test assertions, the test maintenance and more generally the application design. As the tests value decreases, they often lose interest for developers that write or maintain them.

To avoid all this duplication, instead of making the private method public method, in many cases a better solution is extracting the private method as a public method in a new or an existing class. It will not create a design defect. It will make the code more meaningful and the class less bloat. Besides, sometimes the private method is a routine/subset of the class while the behavior suits better in a specific structure. At last, it also makes the code more testable and avoid tests duplication. We can indeed prevent tests duplication by unit testing the public method in its own test class and in the test class of the client classes, we have just to mock the dependency.

嘲弄私有方法?

虽然可以通过使用反射或PowerMock等工具来实现,但我认为这通常是绕过设计问题的一种方法。 私有成员不是为向其他类公开而设计的。 测试类是另一个类。所以我们应该对它应用同样的规则。

嘲笑被测试对象的公共方法?

您可能希望将修饰符private更改为public以测试该方法。 然后,为了测试使用这个重构的公共方法的方法,您可能会试图通过使用Mockito(间谍概念)工具来模拟重构的公共方法,但与模拟私有方法类似,我们应该避免模拟被测试的对象。

Mockito.spy()文档说它自己:

创建真实对象的间谍。间谍调用真正的方法,除非他们> >存根。 真正的间谍应该小心地偶尔使用,比如当 处理遗留代码。

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


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

情况1)如果一个私有方法只被调用一次,就不要将该方法设为public。 它是单个方法的私有方法。因此,您永远不能复制测试逻辑,因为它只调用一次。

情况2)如果私有方法被多次调用,您应该考虑是否应该将私有方法重构为公共方法。

如何决定?

The private method doesn't produce duplication in the tests. -> Keep the method private as it is. The private method produces duplication in the tests. That is, you need to repeat some tests, to assert the same logic for each test that unit-tests public methods using the private method. -> If the repeated processing may make part of the API provided to clients (no security issue, no internal processing, etc...), extract the private method as a public method in a new class. -> Otherwise, if the repeated processing has not to make part of the API provided to clients (security issue, internal processing, etc...), don't widen the visibility of the private method to public. You may leave it unchanged or move the method in a private package class that will never make part of the API and would be never accessible by clients.


代码示例

这些示例依赖于Java和以下库:JUnit、AssertJ(断言匹配器)和Mockito。 但是我认为整个方法对c#也是有效的。

1)私有方法不会在测试代码中创建重复的例子

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

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) };
    }

}

下面是测试代码:

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"));
    }

}

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

2)私有方法在测试代码中创建不必要的重复的例子

下面是一个MessageService类,它提供了创建消息的方法。 所有公共方法都使用createHeader()方法:

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);
    }

}

下面是测试代码:

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);
    }

}

我们可以看到私有方法createHeader()的调用在测试逻辑中创建了一些重复。 createHeader()确实创建了一个我们需要在测试中断言的特定结果。 我们断言了3倍的头内容,而应该只需要一个断言。

We could also note that the asserting duplication is close between the methods but not necessary the same as the private method has a specific logic : Of course, we could have more differences according to the logic complexity of the private method. Besides, at each time we add a new public method in MessageService that calls createHeader(), we will have to add this assertion. Note also that if createHeader() modifies its behavior, all these tests may also need to be modified. Definitively, it is not a very good design.

重构的步骤

假设我们的情况是createHeader()可以作为API的一部分。 我们将通过将createHeader()的访问修饰符更改为public来重构MessageService类:

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

我们现在可以测试这个方法的幺正性:

@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);

}

But what about the tests we write previously for public methods of the class that use createHeader() ? Not many differences. In fact, we are still annoyed as these public methods still need to be tested concerning the returned header value. If we remove these assertions, we may not detect regressions about it. We should be able to naturally isolate this processing but we cannot as the createHeader() method belongs to the tested component. That's why I explained at the beginning of my answer that in most of cases, we should favor the extraction of the private method in another class to the change of the access modifier to public.

我们引入HeaderService:

public class HeaderService {

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

}

我们将createHeader()测试迁移到HeaderServiceTest中。

现在MessageService定义了一个HeaderService依赖:

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()测试的新版本:

@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测试的角度来看,这无关紧要。

当您想要测试的某个方法中有复杂的逻辑时,这是一个很好的指标,表明该类违反了单一责任原则。

一个很好的解决方案是:

为原始方法的功能创建一个接口。 在类中实现并测试该接口。 将接口注入到原始类中。

最近,当重构一个大方法(> 200行)时,我也有同样的想法。对于每个逻辑步骤,我成功地将大方法拆分为较小的方法,因此很容易进行推理。

当涉及到重构出来的小型私有方法时,我想知道我是否应该单独测试它们,因为如果我只测试公共方法,我仍然在测试大方法,测试根本没有从重构中受益

经过一番思考,我意识到:

if all the small methods are private and can't be reused by others, maybe I am doing something wrong: I am not pulling the right abstraction from the code, I am only splitting the big methods treating them like lines/strings, not like mental barrier when I came to the right small methods(I refactored again, completely changing the small methods), and move the small methods into another class exposed as public methods for others to use, now I can test them(and I should test them, they will be used more and deserver the attention)

简介:

我仍然有很多小的私有方法,但是它们共享了很多公共方法,而且小方法真的很小(3-4行,主要是函数调用),但我不会测试它们,我只需要在另一个类中测试共享的公共方法