将私有方法设为公共以进行单元测试…好主意吗?

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


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

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

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

更新

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

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

56181 次浏览

就我个人而言,我宁愿使用公共API进行单元测试,我当然不会让私有方法公共只是来方便测试。

如果你真的想要单独测试私有方法,在Java中你可以使用Easymock / Powermock来允许你这样做。

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

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

以下是迈克尔·费瑟在“有效地使用遗留代码”中所说的话

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

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

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

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

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

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

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

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

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

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

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

我通常将测试类保持在与被测试类相同的项目/程序集中 这样,我只需要internal可见性来使函数/类可测试 这在一定程度上使您的构建过程变得复杂,需要过滤掉测试类。 我通过将所有测试类命名为TestedClassTest并使用正则表达式过滤这些类来实现这一点

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

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

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

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

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

这里有一个非常这里有篇好文章

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

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

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

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

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

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

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

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

有时这个方法被包装在一个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,将打印Doing something private.如果你真的需要它,反射可以在单元测试中使用,以访问私有实例方法。

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

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

然后将属性添加到dll

[组装:InternalsVisibleTo(“MyTestAssembly”)]

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

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

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

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

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

是啊,这真让人头疼。

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


有些人会认为这些步骤中的每一步都应该成为它自己的类,但我不同意——如果它们都共享状态,就没有理由在五个方法中创建五个单独的类。更糟糕的是,这会导致文件和类膨胀。+,它感染了模块的公共API——如果你想从另一个模块(或者将测试代码包含在同一个模块中,这意味着将测试代码与您的产品一起发布)测试它们,所有这些类都必须是public

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

  1. 黑盒测试

    黑盒学派认为,类应该被视为一个黑盒,没有人可以看到其中的实现。测试这一点的唯一方法是通过公共API——就像类的用户将使用它一样。< / p >

  2. 白盒测试。

    白盒派认为自然地使用关于类实现的知识,然后对类进行测试,以了解它是否按应有的方式工作

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

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

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

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

看一看这个视频关于这个主题的一个非常有趣的观点。

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

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

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

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

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

< p > 注意:
这个答案最初是为问题单元测试本身是一个通过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());
}
}

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

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

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

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

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

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

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

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

你不应该让你的测试决定你的代码。我不是在说TDD或其他dd,我的意思是,正是你所要求的。你的应用是否需要这些方法是公共的。如果是,那就测试他们。如果没有,那就不要仅仅为了测试而公开它们。变量和其他变量也是一样。让应用程序的需求决定代码,并让测试测试需求是否得到满足。(再次强调,我不是说先测试还是不测试,我是说改变类结构来满足测试目标)。

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

例如(此处为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或这里堆栈溢出上查看更多信息

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

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

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

回答得很好 IHMO,来自@BlueRaja的优秀回答 - Danny Pflughoeft是最好的之一

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


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


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

这很少是最好的方法。
单元测试必须测试一个 API方法/函数的行为。
如果测试的public方法调用了属于同一组件的另一个public方法,则不需要单元测试该方法。同时测试多个 public方法。
因此,您可能会重复测试、测试fixture、测试断言、测试维护以及更普遍的应用程序设计 随着测试值的降低,编写或维护测试的开发人员通常会对测试失去兴趣 为了避免所有这些重复,与其使private方法成为public方法,在许多情况下更好的解决方案是将__ABC0方法提取为新类或现有类中的public方法.
它不会产生设计缺陷。
这将使代码更有意义,类更少膨胀 此外,有时private方法是类的一个例程/子集,而行为更适合于特定的结构 最后,它还使代码更具可测试性,避免了测试重复 我们确实可以通过在public方法自己的测试类和客户端类的测试类中对public方法进行单元测试来防止测试重复,我们只需要模拟依赖项

嘲弄私有方法? < br >

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

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

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

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

创建真实对象的间谍。间谍调用真正的方法,除非他们>>存根。

真正的间谍应该小心地偶尔使用,例如当

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

下面是我用来决定private方法应该保持private还是被重构的准则。 < br >

情况1)如果private方法被调用一次,则永远不要让private方法public 它是一个单一的方法的private方法。因此,您永远无法复制测试逻辑,因为它只调用一次

情况2)你应该想知道,如果private方法被调用不止一次private方法是否应该被重构为public方法


  • private方法在测试中不产生重复 →保持私人方法不变

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


代码示例 < br >

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

1)在测试代码中private方法不创建重复的例子 < br >

这是一个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"));
}


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

2) private方法在测试代码中创建不必要的重复的示例 < br >

这是一个MessageService类,提供了创建消息的方法 所有public方法都使用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);
}


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

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

重构的步骤

假设在这种情况下,createHeader()可以作为API的一部分 我们将首先重构MessageService类,将createHeader()的访问修饰符更改为public:

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


}
但是我们之前为使用createHeader()的类的public方法编写的测试呢 差别不大。< br > 事实上,我们仍然很恼火,因为这些public方法仍然需要测试返回的头文件值 如果我们删除这些断言,我们可能检测不到关于它的回归 我们应该能够自然地隔离这个处理,但我们不能,因为createHeader()方法属于被测试的组件 这就是为什么我在回答的开头解释说,在大多数情况下,我们应该倾向于提取另一个类中的private方法,而不是将访问修饰符更改为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的新版本:

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

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

一个很好的解决方案是:

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

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

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

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

  1. 如果所有的小方法都是私有的并且不能被其他人重用, 也许我做错了什么:我没有拉右边 抽象从代码,我只是拆分大方法 将它们视为行/字符串,而不是心理障碍
  2. 当我来到正确的小方法(我再次重构, 完全改变小方法),并移动小方法 进入另一个公开为公共方法供他人使用的类, 现在我可以测试它们(我应该测试它们,它们将被更多地使用) 并且值得关注)

简介:

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