【问题标题】:Appropriate method encapsulation for unit testing单元测试的适当方法封装
【发布时间】:2012-12-20 19:14:10
【问题描述】:

我的班级包含14 private methods1 public method。公共方法直接或通过其他私有方法间接调用所有私有方法。

公共方法还调用了查询数据库的DAO。 我为班级写了一个单元测试。由于您不能为私有方法编写单元测试,因此我将所有私有方法更改为默认访问并为它们编写单元测试。

有人告诉我,我不应该仅仅为了测试目的而更改封装。但是我的公共方法调用了 DAO 并从调用中获取其数据。即使我要为 public 方法编写一个测试,我假设它会非常长和复杂。

我应该如何解决这个问题。一方面,我必须为访问 DAO 的公共方法编写一个非常复杂的测试,另一方面,更改方法的访问级别并为它们编写简短的测试方法。我应该怎么办?

任何建议将不胜感激

【问题讨论】:

    标签: java unit-testing methods junit encapsulation


    【解决方案1】:

    纯粹主义者会告诉你,私有方法可以提取到另一个提供可访问方法的辅助类中,他们可能是对的。

    但是如果将这些实用方法保留在类中是有意义的,如果该类不是公共 API 的一部分并且不打算被子类化(例如,它可能是最终的),我看不到任何使其某些私有方法受到包保护或保护的问题。特别是如果记录了这种非私有可见性,例如使用 Guava 注释 @VisibleForTesting

    【讨论】:

    • 同意,但是 14 个私有方法? :P 通常表明该类有很多责任并且可以被打破以进行测试。
    • 或表示开发者遵循良好实践并创建了非常短的方法(3 或 4 行代码)。
    • 遗憾的是,这不是我每天看到的:/ 无论如何,这是真的 :) 如果代码可读且经过良好测试,那很好。
    【解决方案2】:

    看来你这里有两个问题:

    1. 如何测试私有方法(假设在 Java 中):

      我会看看这个问题:How do I test a class that has private methods, fields or inner classes?

      我个人喜欢Trumpi's 回复:

      测试私有方法的最佳方法是通过另一个公共方法。如果无法做到这一点,则以下条件之一为真:

      1. 私有方法是死代码
      2. 您正在测试的类附近有一种设计气味
      3. 您尝试测试的方法不应是私有的
    2. 如何打破DAO的依赖

      您可以尝试使用Dependency Injection 来摆脱对DAO 的依赖。然后,您可以模拟 DAO 并将其注入您的测试用例。 好处是它真正变成了单元测试,而不是集成测试。

    【讨论】:

      【解决方案3】:

      如果它很复杂,可能是因为你的班级有不止一个责任。通常,当您拥有执行不同操作的私有方法时,您可以拥有不同的类和为您执行此操作的公共方法。您的课程将变得更易于阅读、测试,并且您将分担责任。 14个私有方法通常表示这种事情:P

      例如,你可以有类似的东西

      public class LeFooService {
          private final OtherServiceForConversion barService;
          private final FooDao fooDao;
      
          public LeeFooService(FooDao dao, OtherServiceForConversion barService) {
              this.barService = barService;
              this.fooDao = dao;
          }
      
          public void createAsFoo(Bar bar) throws ConversionException {
              Foo foo = convert(bar);
      
              fooDao.create(foo);
          }
      
          private Foo convert(Bar bar) {
              // lots of conversion stuff, services calling D:
          }
      }
      

      为了正确测试,您必须测试转换是否正确完成。因为它是私有的,所以您必须捕获发送到FooDaofoo 并查看所有字段是否设置正确。您可以使用argThat 捕获发送到fooDao 的内容,然后测试转换。你的测试看起来像

      ....
      @Test
      public void shouldHaveConvertedFooCorrectly() {
           // given
           Bar bar = mock(Bar.class);
      
           // when
           fooService.createAsFoo(bar);
      
           // then
           verify(fooDao).create(argThat(fooIsConvertedCorrectly());
      }
      
      private ArgumentMatcher<Foo> fooIsConvertedCorrectly() {
           return new ArgumentMatcher<Foo>() { /*test stuff*/ };
      }
      ....
      

      但是,如果你将转换分离到另一个类,像这样:

      public class LeFooService {
          private final BarToFooConverter bar2FooConverter;
          private final FooDao fooDao;
      
          public LeeFooService(FooDao dao, BarToFooConverter bar2FooConverter) {
              this.bar2FooConverter = bar2FooConverter;
              this.fooDao = dao;
          }
      
          public void createAsFoo(Bar bar) throws ConversionException {
              Foo foo = bar2FooConverter.convert(bar);
      
              fooDao.create(foo);
          }
      }
      

      您将能够测试对 LeeFooService 真正重要的内容:调用流程。从FooBar 的转换测试将由BarToFooConverter 的单元测试负责。 LeeFooService 的一个示例测试是

      @RunWith(MockitoJUnitRunner.class)
      public class LeFooServiceTest {
           @Mock
           private FooDao fooDao;
           @Mock
           private BarToFooConverter converter;
           @InjectMocks
           private LeeFooService service;
      
           @Test(expected = ConversionException.class)
           public void shouldForwardConversionException() {
               // given 
               given(converter.convert(Mockito.any(Bar.class))
                  .willThrown(ConversionException.class);
      
               // when
               service.createAsFoo(mock(Bar.class));
      
               // then should have thrown exception
           }
      
           @Test
           public void shouldCreateConvertedFooAtDatabase() {
               // given 
               Foo convertedFoo = mock(Foo.class);
               given(converter.convert(Mockito.any(Bar.class))
                  .willReturn(convertedFoo); 
      
               // when
               service.createAsFoo(mock(Bar.class));
      
               // then
               verify(fooDao).create(convertedFoo);
           }
      }
      

      希望能有所帮助:)

      一些可能有用的链接:

      SOLID

      BDD Mockito

      【讨论】:

        【解决方案4】:

        正如父母会告诉他们的孩子:不要暴露你的隐私!

        您不需要公开您的私有方法来测试它们。您可以获得 100 % 的类测试覆盖率,包括那些私有方法,而不会暴露它们。

        问题是有些人认为单元测试中的“单元”是函数,而实际上是类。

        例如:我有一个带有 1 个公共方法的类: bool CheckIfPalindrome(string wordToCheck).

        在内部,我有私有方法来验证 wordToCheck 的长度,如果它是 null,如果它是空的,bla bla bla。

        但作为测试人员,我不需要知道或关心开发人员如何组织(或将组织)内部代码。我正在测试接口的实现。

        '假设单词是“Mike”,当调用 CheckIfPalindronme 时,它​​应该返回 false'

        '给定单词是“妈妈”,当调用 CheckIfPalindronme 时,它​​应该返回 true'

        '给定单词是"",当调用CheckIfPalindronme时,它应该返回false'

        '给定单词为空,当调用 CheckIfPalindronme 时,它​​应该抛出一个错误'

        如果我涵盖所有可能的输入和预期输出,我将测试您的私有函数。

        这是 TDD / BDD 的基础,没有这个,TDD 就不可能实现,因为我们必须等着看你决定如何组织你的代码,然后才能编写我们的测试。

        TDD / BDD 说在编写代码之前就编写测试(顺便说一句,它工作得很好!它可以非常快速地识别需求/设计中的缺陷)

        【讨论】:

          【解决方案5】:

          包含一个公共方法和 14 个私有方法的类几乎无法测试。如果没有看到它,我愿意打赌,它非常不稳定。就像 JB Nizet 说的那样;我和我的纯粹主义同事会将大部分或所有私有方法提取到帮助类。原因是:

          • 易于测试
          • 易于重构
          • 易于阅读
          • 易于重复使用

          不提取的原因: * 很多!!类 * 提取需要时间 * 有时,性能问题会阻碍“正确”设计的美观。

          恕我直言,在以下情况下应始终考虑提取逻辑:

          • 私有方法
          • 大类(我通常是在 垂直滚动条出现在我的编辑器窗口中)
          • 内循环 循环

          这里的关键词是单一职责 (SRP)。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2019-08-27
            • 1970-01-01
            • 1970-01-01
            • 2013-07-08
            • 2012-12-15
            • 2017-04-13
            • 2011-11-13
            • 1970-01-01
            相关资源
            最近更新 更多