OOP分解和单元测试困境

时间:2016-03-08 22:55:48

标签: java unit-testing oop

说我有这样的代码:

class BookAnalysis {
   final List<ChapterAnalysis> chapterAnalysisList;
 }

class ChapterAnalysis {
   final double averageLettersPerWord;
   final int stylisticMark;
   final int wordCount;
   // ... 20 more fields
 }

 interface BookAnalysisMaker {
   BookAnalysis make(String text);
 }

 class BookAnalysisMakerImpl implements BookAnalysisMaker {
   public BookAnalysis make(String text) {
     String[] chaptersArr = splitIntoChapters(text);

     List<ChapterAnalysis> chapterAnalysisList = new ArrayList<>();
     for(String chapterStr: chaptersArr) {
        ChapterAnalysis chapter = processChapter(chapterStr);
        chapterAnalysisList.add(chapter);
     }

     BookAnalysis book = new BookAnalysis(chapters);
   }

   private ChapterAnalysis processChapter(String chapterStr) {
      // Prepare
      int letterCount = countLetters(chapterStr);
      int wordCount = countWords(chapterStr);
      // ... and 20 more

      // Calculate
      double averageLettersPerWord = letterCount / wordCount;
      int stylisticMark = complexSytlisticAppraising(letterCount, wordCount);
      HumorEvaluation humorEvaluation = evaluateHumor(letterCount, stylisticMark);
      // ... and 20 more

      // Return
      return new ChapterAnalysis(averageLettersPerWord, stylisticMark, wordCount, ...);
   }
 }

在我的特定情况下,我有一个更高级别的嵌套(想想BookAnalysis - &gt; ChapterAnalysis - &gt; SectionAnalysis)和章节分析(想想每章跨越的PageAnalysis)和SectionAnalysis(想想FootnotesAnalysis等)的几个类。水平。我对如何构建这个问题感到困惑。问题在于processChapter方法:

  • 准备和计算步骤都需要不可忽略的时间/资源
  • 计算步骤取决于多个准备步骤

一些担忧:

  • 上述课程,考虑到章节分析中有20个字段会很长
  • 测试整个测试需要一个非常复杂的准备方法来测试大量的代码。要确认这一点是难以忍受的。 countLetters按预期工作,我不得不不必要地复制几乎所有输入,只是为了测试countLetters行为不同的两种不同情况

包含复杂性并允许测试性的解决方案:

  • processChapter拆分为私有方法,但不能/不应该测试它们
  • 分成多个类,但是我需要大量辅助数据类(对于计算阶段的每个方法)或一个大厨房接收器(保留准备阶段的所有数据)
  • 使辅助方法包私有。虽然这解决了我可以测试它们的意义上的测试问题,但我不应该&#34;部分仍然适用

任何提示,特别是来自类似的现实世界的经历?

修改更新了命名,并根据当前答案添加了一些说明。

我主要关注的是分为类,它不是线性/单级的。例如,上面的countLetters会产生complexSytlisticAppraising所需的结果。让我们说为这两者(LetterCounterComplexSytlisticAppraiser)制作单独的类是有意义的。现在我必须为ComplexSytlisticAppraiser.appraise的输入创建单独的bean,例如:

class ComplexSytlisticAppraiserInput {
  final int letterCount;
  final int wordCount;
  // ... 10 more things it might need
}

哪个好,除了现在我有HumorEvaluator我需要这个:

class HumorEvaluatorInput {
  final int letterCount;
  final int stylisticMark;
  // ... 5 more things it might need
}

虽然这可以通过在很多情况下列出参数来完成,但一个重要的问题是返回参数。即使我必须返回两个整数,我也必须创建一个具有这两个整数的单独bean,构造函数,equals / hashCode,getters。

class HumorEvaluatorOutput {
   final int letterCount;
   final int stylisticMark;

   public HumorEvaluatorOutput(int letterCount, int stylisticMark) {
      this.letterCount = letterCount;
      this.stylisticMark = stylisticMark;
   }

   public int getLetterCount() {
      return this.letterCount;
   }

   public int getStylisticMark() {
      return this.stylisticMark;
   }

   @Override
   public String toString() {
      StringBuilder sb = new StringBuilder();
      sb.append("HumorEvaluatorOutput [letterCount=");
      sb.append(letterCount);
      sb.append(", stylisticMark=");
      sb.append(stylisticMark);
      sb.append("]");
      return sb.toString();
   }

   @Override
   public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + letterCount;
      result = prime * result + stylisticMark;
      return result;
   }

   @Override
   public boolean equals(Object obj) {
      if (this == obj)
         return true;
      if (obj == null)
         return false;
      if (getClass() != obj.getClass())
         return false;
      HumorEvaluatorOutput other = (HumorEvaluatorOutput) obj;
      if (letterCount != other.letterCount)
         return false;
      if (stylisticMark != other.stylisticMark)
         return false;
      return true;
   }
}

那是2对53行代码 - 哎呀!

所以这一切都很好,但是:

  • 不可重复使用。其中绝大多数仅用于使代码可测试。考虑一下分析器,例如:BookAnalyzerCarAnalyzerGrainAnalyzerToothAnalyzer。他们绝对没有任何共同点
  • 除了允许测试
  • 之外,从1中制作20个课程不会产生太多
  • 你可以争辩说,无论它是分为类还是方法,从使零件足够小以便理解和操纵这一点的差异并不是那么大
  • 另一方面,如果我考虑到可测试性,那么将会有大量的噪音和间接性。相比:
    • 管理10个文件= 10个分析器* 1个文件,包含20个私有方法
    • 800个文件= 10个分析器*(20个接口,20个实现,20个输入和20个输出bean)
    • 400个文件,如果我们删除输入/输出bean并去一些其他路由(例如每个分析器hack一个大的I / O bean) 请注意,数百个文件将非常短,主要是样板 - 可能大部分逻辑将在每行10行以下(在第一种情况下==私有方法)
  • 这有很大的开销。如果我要私有方法调用1次,那么创建额外的输入和输出bean就会加起来......

正确地做正确的事情就是做正确的事。只是想看看我是否还有其他一些我可以追求的选项,我很想念。或者我的逻辑纯粹是坏事?

修改:根据评论进行的其他更新。我们可以缩短HumorEvaluatorOutput,而不是一个大问题:

class HumorEvaluatorOutput {
   final HumorCategoryEnum humorCategory;
   final int humorousWordsCount;

   public HumorEvaluatorOutput(HumorCategoryEnum humorCategory, int humorousWordsCount) {
      this.humorCategory = humorCategory;
      this.humorousWordsCount = humorousWordsCount;
   }

   public HumorCategoryEnum getHumorCategory() {
      return this.humorCategory;
   }

   public int getHumorousWordsCount() {
      return this.humorousWordsCount;
   }
}

那2对17行代码 - 仍然是呀!当你考虑一个例子时,它并不多。如果您有20个不同的分析器(BookAnalyzerCarAnalyzer,...),包含20个不同的子分析器(如上所述:ComplexSytlisticAppraiserHumorEvaluator,所有分析器类似其他分析仪,显然是非常不同的类别),代码增加了8倍。

对于BookAnalyzer vs CarAnalyzerBook vs Chapter子分析工具 - 实际上,我需要比较BookAnalyzerCarAnalyzer,就像我将要拥有的那样。我肯定会在所有章节中重复使用Chapter子分析器。但是,我不会将它重新用于任何其他分析仪。即我有这个:

BookAnalyzer
  ChapterSubAnalyzer
  HumorSubAnalyzer
  ... // 25 more
CarAnalyzer
  EngineSubAnalyzer
  DrivertrainSubAnalyzer
  ... // 15 more
GrainAnalyzer
  LiquidContentSubAnalyzer
  FiberContentSubAnalyzer
  ... // 20 more

按照上面的说法,我不得不创建20个接口,20个非常短的子类,20个输入/输出bean,而不会重复使用。分析书籍和汽车很少在过程中的任何地方使用相同的方法和相同的步骤。

再一次 - 我做上述事情我很好,但除了允许测试之外,我没有看到任何好处。这就像驾驶Toyota Thundra到隔壁邻居的聚会一样。你可以这样做,就像参加聚会的所有其他人一样吗?当然。你应该这样做吗? Ehhh ...

所以:

  • 在800个文件中将10行文件中的500行变为5000行(可能不是完全正确的数字,但你得到的结论)是不是更好,只是为了遵循OOP并启用测试?
  • 如果没有,其他人如何做到这一点,仍然坚持不打破OOP /测试&#34;规则&#34; (例如,通过使用反射来测试不应该首先测试的私有方法)?
  • 如果是,其他人都这样做,那很好。实际上,然后是一个子问题 - 你如何设法在那里找到你需要的东西,并按照应用程序的流程消除所有噪音?

2 个答案:

答案 0 :(得分:2)

这个问题可能更适合CodeReview。也就是说,感觉您已经知道解决方案是将类分解为更小的类,以便更容易测试。

BookMakerImpl,它似乎已经做了至少两个截然不同的工作。它将文本分成几部分并对这些部分进行分析。乍一看,它还不清楚你是否有命名问题。 Chapter并未真正代表您提供的代码示例中的章节(正如我所料)。它实际上代表了给定章节的分析结果(您似乎没有将章节文本传递给它的构造函数,尽管这可能是您发布的代码中的遗漏)。

您可能采取的一种方法来简化测试(假设我对Chapter表示的内容是正确的,如果不是方法类似,但名称和元素显然需要更改)是提取分析分成一个(或多个类)。使用您提供的代码,您似乎可以创建类似ChapterTextAnalyser类的内容。这将采用string(在示例中提供它将是章节文本),然后返回结果类似于ChapterAnalysis(替换您当前的Chapter类)。

如果你在Chapters和其他部分之间有类似的分析,那么这个结构可能需要重新设计才能在给定的域中有意义并在适当的时候共享功能,但实际上你可能有类似的东西(伪代码)。 ..

class BookAnalyserImpl implements BookAnalyser
    // Pass in analyser factory and book parser
    // to constructor so mocked version can
    // be used for testing
    public BookAnalyserImpl(TextAnalyserFactory textAnalyserFactory,
                            BookParser bookParser) {
        if(null != textAnalyserFactory) {
           mTextAnalyserFactory = textAnalyserFactory;
        } else {
           mTextAnalyserFactory = new AnalyserFactoryImpl();
        }
        // Same for bookParser
    }
    BookAnalysis analyse(String bookText) {
        BookAnalysis bookAnalysis = new BookAnalysis();
        ChapterAnalyser chapterAnalyser = mTextAnalyserFactory.GetChapterAnalyser();

        foreach(chapterText in mBookParser.splitIntoChapters(bookText)) {
            bookAnalysis.AddChapterAnalysis(chapterAnalyser.analyse(chapterText));
        }
    }
}

class TextAnalyserFactoryImpl implements TextAnalyserFactory {
    ChapterAnalyser GetChapterAnalyser() {...}
}

class ChapterAnalyserImpl implements ChapterAnalyser {
     ChapterAnalysis analyse(String chapterText) { ... }
}

正如您所说,这将导致您有更多课程。如果课程有意义且具有不同的职责,这本身并不是一件坏事。

如果您不喜欢拥有大量课程,那么您可以简单地将分析推送到另一个具有公共界面的课程中。

class BookAnalyser {
    ChapterAnalysis analyseChapter(String text)  { ... }
    PageAnalysis analysePage(String text) {...}
    // ...
}

通过使您想要调用的方法成为您调用它们的类的功能的一部分来避免私有测试问题。

回复您的部分修改:

首先,重要的是要记住OOP是可选的,采用alternate approach解决问题非常有效。

你真的在编写分析书籍,汽车,谷物和牙齿的软件吗?感觉有点做作,这使得问题空间很难被买入并因此理解,这被编码示例不完整的事实所放大。虽然在您的问题域的当前迭代中,分析器之间没有明显的共性,但不难想象分析可能相似的区域。例如,孔隙度分析可以应用于谷物,牙齿和书籍的页面,以提供有意义的信息。但是,您的图书分析基于纯文本输入,因此这不太可能成为您的问题域的一部分,至少对于Book而言。

  

最好将10个文件中的500行放入800个文件中的5000行(可能不是完全正确的数字,但你明白了这一点)只是为了遵循OOP并启用测试?

作为软件复杂性的衡量标准,我并不是代码行的忠实粉丝。你的初步比较是2:53,你在C#中你已经下降到2:17,比率将接近2:9,虽然真正的差异是5(样板行数)+ 2 *字段数(分配一行,获取一行)。使用if .. else ...(5行)明显比1行三元运算符更详细/更清晰?它非常subjective,我曾在有过编码标准的地方工作,说你无法使用三元运算符。

4000000行代码优于5000,似乎不太可能。但我也怀疑,如果你打破问题领域以提取不同的功能和共性,这将是结果。

从代码中考虑这一行

HumorEvaluation humorEvaluation = evaluateHumor(letterCount, stylisticMark);

您没有对humorEvaluation评估做任何事情,但似乎这代表了一个独特的事情。看起来这会被传递到方法中构造的ChapterAnalysis并存储在那里。这样就无需在章级别存储构成HumorEvaluation的各个字段。这种不同的类型似乎也代表了您使用HumorEvaluatorOutput表示的相同概念。除非您从中获益,否则不需要两次代表相同的概念。在这里你似乎没有得到它,所以扔掉它。

  

如果没有,其他人如何做到并且仍然坚持不打破OOP /测试&#34;规则&#34; (例如,通过使用反射来测试不应该首先测试的私有方法)?

我不喜欢直接测试私有方法。它们是一个实现细节,直接测试它们很脆弱。从测试者的角度来看,如果存在私有方法,或者在从测试调用的方法中全部写入,则无关紧要。重要的是整个代码的可衡量的副作用。你说:

  

测试整个版本需要一个非常复杂的准备方法来测试大量的代码。要确认这一点是难以忍受的。 countLetters按预期工作,我不得不不必要地复制几乎整个输入只是为了测试countLetters表现不同的两种不同情况

现实情况是,从测试的角度来看,重要的是构造的Chapter的状态是正确的。如果countLetters按预期工作,那么它将是。如果没有,那么测试将失败。特别是,如果countLetters没有返回您期望的数字,则章节类中预期的averageLettersPerWord和预期的wordCount之间的关系将不正确。< / p>

查看您的一些方法,countLetterscountWords他们有不同的输入和输出,并且不会修改他们所在类的状态。正如我所做的那样。之前说过,这表明他们可能处于一个不同的阶层,让他们公开是有意义的。

class GenericTextAnalyserImpl implements GenericTextAnalyser {
    int countLetters(String text);
    int countWords(String text);
    int complexSytlisticAppraising(int letterCount, int wordCount);
    // ...
}

countLetters可能是其他分析者可能使用的东西(Book,Chapter,Page等)。这些方法不再需要复制到这些其他类中,并且对这些方法的测试变得微不足道。它还允许减少对诸如Book之类的元素的测试,因为您可以模拟调用以确保正在进行正确的调用,而不必为要测试的每个变体复制整个调用结构。

  

如果是,其他人都这样做,那很好。实际上,这是一个子问题 - 你如何设法找到你需要的东西,并在所有噪音中跟踪应用程序的流程?

如果你通过增加800个额外课程将5000行变成4000000,那么你可能找不到自己的方法。如果你创建了一组合理的课程,将你的问题分解为这些领域内的领域和元素,那么它通常不会那么难。

答案 1 :(得分:1)

你是对的,通常通常被认为是测试私有方法的好习惯。然而,重要的是要理解为什么会这样,因为只有这样你才能判断它是否适用于你的情况。

反对测试私有方法的主要论点是测试代码的预期增加的维护工作:如果软件设计得当,私有元素比公共元素更可能被更改。因此,如果测试仅使用公共API,则保持测试代码构建和正常工作的努力会更小。 (他们也应该最好只使用关于被测代码的黑盒智慧,但这是一个不同的故事...)

看看你的例子,我认为方法countLetters可能是私有的。但是,这个名称让我觉得这个方法可能会在代码中实现一个易于理解和稳定的概念。如果是这种情况,一些替代设计选择就是将这种方法分解为一个自己的类 - 它不会是私有的。

然而,这个想法并不意味着你要分解出这个功能(你可以这样做,但这不是我的观点)。关键是要明确这一切都归结为一些代码预期有多稳定的问题。

对于稳定性的这种期望必须与测试的努力进行权衡:首先测试私有元素可以更容易(这可以节省您的工作量),但从长远来看可能会花费您的成本。可能仍然是,长期成本永远不会超过短期胜利。你必须判断。