单元测试大块代码(映射,翻译等)

And*_*rew 28 c# unit-testing etl

我们对大部分业务逻辑进行单元测试,但仍然坚持如何最好地测试我们的一些大型服务任务和导入/导出例程.例如,考虑将工资单数据从一个系统导出到第三方系统.要以公司需要的格式导出数据,我们需要达到~40个表,这会产生一个创建测试数据和模拟依赖关系的噩梦.

例如,请考虑以下(~3500行导出代码的子集):

public void ExportPaychecks()
{
   var pays = _pays.GetPaysForCurrentDate();
   foreach (PayObject pay in pays)
   {
      WriteHeaderRow(pay);
      if (pay.IsFirstCheck)
      {
         WriteDetailRowType1(pay);
      }
   }
}

private void WriteHeaderRow(PayObject pay)
{
   //do lots more stuff
}

private void WriteDetailRowType1(PayObject pay)
{
   //do lots more stuff
}
Run Code Online (Sandbox Code Playgroud)

我们在这个特定的导出类中只有一个公共方法 - ExportPaychecks().这真的是唯一一个对调用这个类的人有意义的行为......其他一切都是私有的(约80个私有函数).我们可以将它们公开用于测试,但是我们需要模拟它们来单独测试每个(即你不能在没有模拟WriteHeaderRow函数的情况下在真空中测试ExportPaychecks.这也是一个巨大的痛苦.

由于这是单个导出,对于单个供应商而言,将逻辑移入域中是没有意义的.逻辑在此特定类之外没有域重要性.作为测试,我们构建了接近100%代码覆盖率的单元测试......但是这需要将大量的测试数据输入到存根/模拟对象中,加上超过7000行代码,因为存根/模拟我们的许多依赖项.

作为HRIS软件的制造商,我们拥有数百种进出口产品.其他公司真的对这种类型的东西进行单元测试吗?如果是这样,是否有任何捷径可以减少痛苦?我很想说"没有单元测试导入/导出例程",只是稍后实现集成测试.

更新 - 感谢所有答案.我喜欢看到的一件事就是一个例子,因为我还没有看到有人可以将像大文件导出这样的东西变成一个易于测试的代码块,而不会把代码弄得一团糟.

Mar*_*ann 18

这种(尝试)单元测试的方式,你试图通过一个公共方法覆盖整个庞大的代码库总是让我想起外科医生,牙医或妇科医生通过小开口执行复杂的操作.可能,但不容易.

封装是面向对象设计中的一个古老概念,但是有些人认为封装是可测试性受到影响的极端情况.还有另一个称为开放/封闭原理的 OO原则,它更适合可测试性.封装仍然是有价值的,但不是在可扩展性为代价的-事实上,可测试性实际上只是打开/关闭原则另一个词.

我不是说你应该让你的私有方法公开,但我要说的是,你应该考虑重构你的应用程序到组合的部分-即合作,而不是一个大的很多小类别交易脚本.你可能会认为它没有多大意义,为解决单一供应商做到这一点,但现在你的痛苦,这是一个出路.

当您在复杂的API中拆分单个方法时经常会发生的事情是您还获得了很多额外的灵活性.最初的一次性项目可能会变成一个可重复使用的库.


下面是关于如何在手执行重构时对这个问题的一些想法:每个ETL程序必须执行至少以下三个步骤:

  1. 从源中提取数据
  2. 转换数据
  3. 将数据加载到目标

(因此,名称ETL).至于重构一个开始,这给我们至少有三个班级,不同的职责:Extractor,TransformerLoader.现在,你有三个更有针对性的责任,而不是一个大班.没有什么比这更麻烦了,而且已经有点可测试了.

现在放大这三个区域中的每一个区域,看看你可以将责任分得更多.

  • 至少,您需要对每个"行"源数据进行良好的内存表示.如果源是关系数据库,您可能想要使用ORM,但如果不是,则需要对这些类进行建模,以便它们正确保护每行的不变量(例如,如果字段不可为空,则类应该保证这是通过在尝试空值时抛出异常).这些类具有明确定义的目的,可以单独测试.
  • 目的地也是如此:你需要一个好的对象模型.
  • 如果源上有高级应用程序端过滤,您可以考虑使用规范设计模式实现这些过滤.那些往往也是非常可测试的.
  • 转换步骤是很多动作发生的地方,但是现在您拥有源和目标的良好对象模型,转换可以由Mappers执行- 再次可测试的类.

如果您有很多"行"的源和目标数据,您可以在Mappers中为每个逻辑"行"等进一步拆分它.

它永远不需要变得混乱,并且额外的好处(除了自动化测试)是对象模型现在更灵活.如果您需要编写另一个涉及双方之一的ETL应用程序,您已经阅读了至少三分之一的代码.

  • 好答案.管道和过滤器是另一个值得一提的好模式.过滤器非常容易进行单元测试 - http://www.eaipatterns.com/PipesAndFilters.html (2认同)

Wol*_*ang 7

关于重构的一些常见问题:

重构并不意味着你把你的3.5k LOC分成n部分.我不建议将80种方法中的一些公开或类似的东西.这更像是垂直切片代码:

  • 尝试分解自立算法和数据结构,如解析器,渲染器,搜索操作,转换器,专用数据结构......
  • 尝试确定您的数据是否在几个步骤中处理,并且可以构建在一种管道和过滤器机制或分层体系结构中.尝试找到尽可能多的图层.
  • 将逻辑部分的技术(文件,数据库)部分分开.
  • 如果您有许多这些导入/导出怪物,请查看它们的共同点,并将这些部分考虑在内并重新使用它们.
  • 一般来说,您的代码过于密集,即它在LOC中的每个旁边都包含太多不同的功能.访问代码中的不同"发明",并考虑它们是否实际上是值得拥有自己的类的棘手设施.
    • 重构时,LOC和类的数量都可能会增加.
    • 尝试让你的代码在类中变得非常简单('baby code'),并在类之间的关系中复杂化.

因此,您根本不必编写覆盖整个3.5k LOC的单元测试.在一次测试中,只有一小部分被覆盖,并且您将有许多彼此独立的小测试.


编辑

这是一个很好的重构模式列表.其中一个显示了我的意图:分解条件.

在该示例中,某些表达式被分解为方法.不仅使代码更容易阅读,而且您还有机会对这些方法进行单元测试.

更好的是,您可以将此模式提升到更高的级别,并将这些表达式,算法,值等分解为方法,而不仅仅是方法,还要分解给自己的类.


Bur*_*urt 6

你最初应该拥有的是集成测试.这些将测试函数是否按预期执行,您可以为此命中实际的数据库.

一旦你拥有了这个存储网络,你就可以开始重构代码,使其更易于维护并引入单元测试.

正如serbrech Workign提到的那样,有效地使用Legacy代码将帮助您永无止境,我强烈建议您阅读它,即使是绿地项目.

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

我要问的主要问题是代码经常变化多久?如果不经常尝试引入单元测试真的值得,如果它经常更换,那么我肯定会考虑清理一下.


Tom*_*son 2

这是嘲笑一切的概念失败的领域之一。当然,单独测试每个方法将是一种“更好”的做事方式,但是将制作所有方法的测试版本的工作量与将代码指向测试数据库的工作量进行比较(如果需要,在每个测试运行开始时重置) )。

这就是我在组件之间有很多复杂交互的代码中使用的方法,并且效果很好。由于每个测试将运行更多代码,因此您更有可能需要使用调试器单步执行以准确找到出错的位置,但您无需付出大量额外的努力即可获得单元测试的主要好处(知道出了问题) 。