公开私有方法对其进行单元测试......好主意?

时间:2011-08-16 09:11:43

标签: c# java unit-testing

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


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

通常这是因为该方法包含在类中的其他方法之间共享的逻辑,并且它自己测试逻辑更加整洁,或者其他原因可能是我想要测试同步线程中使用的逻辑而不必担心关于线程问题。

其他人发现自己这样做了,因为我真的不喜欢这样做吗?我个人认为奖金超过了公开方法的问题,并没有在班级之外提供任何服务......

更新

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

然而,我可以看到每个人都指出它永远不应该发生。当我考虑更多时,我认为更改代码以适应测试是一个坏主意 - 毕竟我认为测试是一种支持工具,并且如果你愿意,将系统更改为“支持支持工具”,则是公然的糟糕的做法。

33 个答案:

答案 0 :(得分:182)

  

注意:
  这个答案最初是针对问题Is unit testing alone ever a good reason to expose private instance variables via getters? 发布的,这个问题已经合并到了这个问题中,所以它可能与那里提供的用例有关。

作为一般性陈述,我通常都是为了重构"生产"代码使其更容易测试。但是,我认为这不是一个好的电话。一个好的单元测试(通常)不应该关心课程'实现细节,仅关于其可见行为。您可以测试该类在调用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());
    }
}

答案 1 :(得分:147)

就个人而言,我宁愿使用公共API进行单元测试,而且我绝对不会将私有方法公开只是以便于测试。

如果您真的想要单独测试私有方法,可以使用Easymock / Powermock来实现此目的。

你必须务实,你也应该了解事情难以测试的原因。

'Listen to the tests' - 如果难以测试,那会告诉你一些关于你的设计的事吗?您是否可以通过公共API测试对这种方法的测试是否微不足道并轻松覆盖?

以下是迈克尔·费尔斯在“Working Effectively With Legacy Code

中所说的话
  

“很多人花了很多时间试图弄清楚如何解决这个问题...真正的答案是,如果你有测试私有方法的冲动,那么这个方法不应该是私有的;如果制作的话公众困扰你的方法很可能,因为它是一个单独的责任的一部分;它应该在另一个类上。“ [有效地使用传统代码(2005)由M. Feathers]

答案 2 :(得分:62)

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

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

答案 3 :(得分:39)

很多答案建议只测试公共界面,但恕我直言这是不现实的 - 如果一个方法做了5个步骤的事情,你会想要分别测试这五个步骤,而不是一起测试。这需要测试所有五种方法,(除测试之外)可能是private

测试“私有”方法的常用方法是为每个类提供自己的接口,并创建“私有”方法public,但不要在接口中包含它们。这样,它们仍然可以进行测试,但它们不会破坏界面。

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

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

是的,这是屁股的痛苦。

不幸的是,这是众多 sacrifices we make to make code testable 中的一个。也许未来的语言(或者甚至是未来的C#/ Java版本)将具有使类和模块可测试性更方便的功能;但与此同时,我们必须跳过这些箍。


有些人会争辩说每个步骤都应be its own class,但我不同意 - 如果它们都共享状态,则没有理由创建五个单独的类,其中有五个方法可以。更糟糕的是,这导致文件和类膨胀。 Plus ,它会感染您模块的公共API - 如果您想从另一个模块测试它们,那么所有这些类必须是public(或者包含测试代码)在同一模块中,这意味着将测试代码与您的产品一起发送)

答案 4 :(得分:26)

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

答案 5 :(得分:19)

IMO,您应该编写测试,而不是对您的类在内部实现的方式做出深刻的假设。您可能希望稍后使用另一个内部模型重构它,但仍然保留先前实现所提供的相同保证。

记住这一点我建议你专注于测试你的合同是否仍然存在,无论你的班级目前有什么内部实施。基于属性的公共API测试。

答案 6 :(得分:18)

如何将其打包为私有?然后您的测试代码可以看到它(以及您的包中的其他类),但它仍然对您的用户隐藏。

但实际上,你不应该测试私有方法。这些是实施细节,而不是合同的一部分。他们所做的一切都应该通过调用公共方法来解决(如果他们的代码在那里没有被公共方法行使,那么应该去)。如果私有代码太复杂,那么该类可能做了太多事情而且不需要重构。

将方法公之于众是一项重大承诺。一旦你这样做,人们将能够使用它,你不能再改变它们了。

答案 7 :(得分:14)

更新I have added a more expanded and more complete answer to this question in numerous other places. This is can be found on my blog

如果我需要公开测试它,通常会暗示被测系统不遵循Single Reponsibility Principle。因此,应该引入一个缺少的类。将代码提取到新类后,将其公开。现在您可以轻松测试,并且您正在关注SRP。你的其他课只需要通过合成来调用这个新类。

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

例如:

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

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

答案 8 :(得分:12)

一些很棒的答案。我没有看到提到的一件事是,通过测试驱动开发(TDD),在重构阶段创建私有方法(查看Extract Method以获取重构模式的示例),因此应该已经有了必要的测试覆盖率如果正确完成(当然,在正确性方面,你会得到一堆混合的意见),你不必担心必须公开私有方法,以便你可以测试它。 / p>

答案 9 :(得分:10)

为什么不将堆栈管理算法拆分为实用程序类?实用程序类可以管理堆栈并提供公共访问器。其单元测试可以专注于实现细节。对算法上棘手的类进行深度测试非常有助于消除边缘情况并确保覆盖。

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

答案 10 :(得分:9)

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

答案 11 :(得分:9)

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

如果你想测试它们,你有几个选择。

  • 使用反射
  • 提供方法包访问

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

此问题非常good article here

答案 12 :(得分:6)

在单元测试方面,你绝对不应该添加更多的方法;我相信你最好只为你的first()方法做一个测试用例,在每次测试之前调用它;然后,您可以多次调用 - next()previous()last()来查看结果是否符合您的预期。 我想如果你不为你的课程添加更多方法(仅用于测试目的),你会坚持“黑盒子”测试原则;

答案 13 :(得分:6)

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

但实际上,你并不关心类的内部状态,你只想测试公共方法在你预期的情况下返回你期望的结果。

答案 14 :(得分:6)

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

然后将属性添加到dll

[assembly:InternalsVisibleTo(“MyTestAssembly”)]

现在所有方法都在MyTestAssembly项目中可见。也许并不完美,但更好的方法是将私人方法公之于众地进行测试。

答案 15 :(得分:5)

首先看看该方法是否应该被提取到另一个类并公开。如果不是这种情况,请将其保护为包,并使用@VisibleForTesting在Java中进行注释。

答案 16 :(得分:5)

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

  1. 黑盒测试

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

  2. 白盒测试。

    白盒子学校认为自然地使用关于课程实施的知识,然后测试课程以了解它的工作原理。

  3. 我真的不能参与讨论。我只是觉得知道测试类(或库或其他)有两种不同的方法会很有趣。

答案 17 :(得分:5)

我想说这不是一个坏主意,因为我不确定你是否会获得任何好处以及潜在的问题。如果你要更改一个调用的契约,只是为了测试一个私有方法,你就不会测试它如何被使用,而是创建一个你从未打算过的人为场景。

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

答案 18 :(得分:4)

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

答案 19 :(得分:4)

您想要独立测试的私有方法表明您的课程中还有另一个“概念”。将“概念”提取到自己的类并将其作为单独的“单元”进行测试。

看一下this video对这个主题的一个非常有趣的看法。

答案 20 :(得分:3)

你永远不应该让你的测试决定你的代码。我不是说TDD或其他DD我的意思,正是你的要求。您的应用是否需要公开这些方法。如果确实如此,则测试它们。如果没有,则不要仅仅为了测试而将它们公开。与变量和其他变量相同。让您的应用程序需要决定代码,并让您的测试测试满足需求。 (同样,我并不是指先测试或不是我的意思是更改类结构以满足测试目标)。

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

例如(这里是伪代码);

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

没有理由测试“添加”,你可以测试“书籍”。

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

答案 21 :(得分:2)

我倾向于同意将单元测试的奖金超过提高某些成员的知名度的问题。稍微改进是使其受保护和虚拟,然后在测试类中覆盖它以暴露它。

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

答案 22 :(得分:2)

不,因为有更好的方法可以给那只猫上皮。

某些单元测试工具依赖于类定义中的宏,这些宏在测试模式下构建时会自动扩展以创建钩子。非常C风格,但它的工作原理。

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

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

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

答案 23 :(得分:2)

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

答案 24 :(得分:2)

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

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

当:

  • 通过将类分解为多个类,使得类的可读性显着降低,
  • 只是为了让它更具可测性,
  • 并提供对内部的一些测试访问权限就可以了。

宗教似乎胜过工程学。

答案 25 :(得分:2)

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

有时候这个方法被包装在一个ifdef块中(我主要用C ++编写),所以它不会被编译用于发布。但它在发布时通常很有用,可以提供验证方法,让程序的对象树检查事物。

答案 26 :(得分:2)

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

这有点使您的构建过程变得复杂,它需要过滤掉测试类。 我通过命名所有测试类TestedClassTest并使用正则表达式来过滤这些类来实现此目的。

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

答案 27 :(得分:1)

正如其他人的评论所广泛注意到的,单元测试应该关注公共API。但是,除了优点/缺点和理由之外,您可以使用反射在单元测试中调用私有方法。您当然需要确保您的JRE安全性允许它。调用私有方法是Spring Framework使用的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.如果确实需要,可以在单元测试中使用反射来访问私有实例方法。

答案 28 :(得分:1)

在.Net中有一个名为PrivateObject的特殊类,专门用于访问类的私有方法。

the MSDNStack Overflow

上查看更多相关信息

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

在某些情况下,这还不够,在这种情况下,您将不得不使用反射。

仍然我会坚持不测试私有方法的一般建议,但是像往常一样总有例外。

答案 29 :(得分:0)

单元测试的目的是确认该单元的公共API的工作方式。应该不需要将私有方法暴露给测试,如果是这样,那么应该重新考虑你的接口。私有方法可以被认为是公共接口的“帮助”方法,因此通过公共接口进行测试,因为它们将调用私有方法。

我可以看到你有“需要”这样做的唯一原因是你的课程没有为测试而设计得当。

答案 30 :(得分:0)

一切都是关于实用主义的。您的单元测试是代码的客户端,为了实现良好的代码覆盖水平,您需要使代码非常可测试。如果测试代码非常复杂,那么您的解决方案可能会失败,以便您能够在没有有效公共接缝的情况下设置必要的角落案例。使用IoC也有助于解决这个问题。

答案 31 :(得分:0)

就个人而言,我在测试私有方法时遇到了同样的问题,这是因为某些测试工具是有限的。 如果你的设计没有响应你的需要而改变工具而不是设计,那么用有限的工具驱动你的设计并不好。 因为你要求C#i不能提出好的测试工具,但对于Java有两个强大的工具:TestNG和PowerMock,你可以找到相应的.NET平台测试工具

答案 32 :(得分:0)

非常回答的问题。
IHMO,来自@BlueRaja的优秀answer - Danny Pflughoeft是最好的之一。

  

很多答案建议只测试公共界面,但恕我直言   这是不现实的 - 如果一个方法做了5步骤的事情,   你想分别测试这五个步骤,而不是一起测试。   这需要测试所有五种方法(除测试外)   否则可能是私人的。

最重要的是,我想强调一个问题"我们是否应该公开私人方法来对其进行单元测试"是一个客观正确答案取决于多个参数的问题 所以我认为在某些情况下我们没有和其他我们应该

这里的一些答案可以概括为:"通常可以做到这一点" "从来没有,这很糟糕。不要使用API​​作弊,只测试公共行为" 这让我很烦恼,因为测试和实施的设计质量是一个重要的问题,这个问题对两者都有很多影响。

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

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

要避免所有这些重复,而不是使用private方法public方法,在许多情况下,更好的解决方案是private方法提取为{{1}新方法或现有类中的方法
它不会造成设计缺陷 它会使代码更有意义,而且类更少膨胀 此外,有时public方法是类的例程/子集,而行为在特定结构中更适合 最后,它还使代码更易于测试并避免测试重复 我们确实可以通过单独测试自己的测试类和客户端类的测试类中的private方法来防止测试重复,我们只需要模拟依赖性。

嘲弄私人方法?

当然,可以通过反射或使用工具作为PowerMock,但IHMO我认为它通常是一种绕过设计问题的方法。
测试类是另一类 public成员的设计不适用于其他类。所以我们应该对测试类遵循相同的规则。

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

您可能需要将修饰符private更改为private以测试方法 然后,为了测试使用这个重构的公共方法的方法,您可能想要使用Mockito(spy concept)工具来模拟重构的public方法,但类似于模拟public方法,我们应避免嘲笑被测物体。

private文档说明了它自己:

  

创建真实对象的间谍。间谍称实际方法,除非他们是> >存根。

     

真正的间谍应该谨慎使用,偶尔使用,例如   处理遗留代码。

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

以下是我用来确定spy()方法是否应保留private或重构的指南。

案例1)如果调用此方法一次,切勿制作private方法private
这是单个方法的public方法。所以你永远不能复制测试逻辑,因为它只调用一次。

案例2)如果private方法被多次调用 ,您应该想知道private方法是否应该重构为public方法。< BR />

如何决定?

  • private方法在测试中不会产生重复 - &GT;保持方法私有不变。

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

代码示例

示例依赖于Java和以下库:JUnit,AssertJ(断言匹配器)和Mockito。
但我认为整体方法对C#也有效。

1)public方法未在测试代码中创建重复的示例

这是一个private类,它提供了执行某些计算的方法 所有公共方法都使用private方法。

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")); } } 并不会复制测试逻辑。
这是一个中间操作,它不会产生我们需要在测试中断言的特定结果。

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

这是一个mapToInts()类,它提供了创建消息的方法 所有private方法都使用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确实创造了一个我们需要在测试中断言的具体结果 我们断言头部内容的3倍,同时需要一个断言。

我们还可以注意到,断言复制在方法之间很接近,但不一定与createHeader()方法具有特定逻辑相同: 当然,根据createHeader()方法的逻辑复杂性,我们可能会有更多的差异 此外,每次我们在调用private的{​​{1}}中添加新的private方法时,我们都必须添加此断言。
另请注意,如果public修改了其行为,则可能还需要修改所有这些测试 确切地说,它不是一个很好的设计。

重构步骤

假设我们处于可以接受MessageService作为API的一部分的情况 我们将首先通过将createHeader()的访问修饰符更改为createHeader()来重构createHeader()类:

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); } 方法 如果我们删除这些断言,我们可能无法检测到它的回归 我们应该能够自然地隔离这个处理,但我们不能因为public方法属于被测试的组件 这就是我在答案开始时解释的原因,在大多数情况下,我们应该倾向于将另一个类中的createHeader()方法提取到将访问修饰符更改为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 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()); } 测试的角度来看并不重要。