我坚信使用验证完整程序的测试(例如收敛测试)的价值,包括一套自动化的回归测试。在阅读了一些编程书籍之后,我也感到I,我“应该”编写单元测试(即,用于验证单个函数正确性并且不等于运行整个代码来解决问题的测试)。 。但是,单元测试似乎并不总是与科学规范相适应,并且最终会感到虚假或浪费时间。
我们应该为研究代码编写单元测试吗?
我坚信使用验证完整程序的测试(例如收敛测试)的价值,包括一套自动化的回归测试。在阅读了一些编程书籍之后,我也感到I,我“应该”编写单元测试(即,用于验证单个函数正确性并且不等于运行整个代码来解决问题的测试)。 。但是,单元测试似乎并不总是与科学规范相适应,并且最终会感到虚假或浪费时间。
我们应该为研究代码编写单元测试吗?
Answers:
多年以来,我一直误以为我没有足够的时间为我的代码编写单元测试。当我确实编写测试时,它们是肿的,沉重的东西,这仅鼓励我认为我应该只在知道需要它们时才编写单元测试。
然后,我开始使用“ 测试驱动开发”,并发现它是一个完整的启示。我现在坚信,我没有时间不编写单元测试。
根据我的经验,通过考虑测试进行开发,最终会得到更简洁的界面,更集中的类和模块以及通常更多的SOLID可测试代码。
每当我使用没有单元测试的遗留代码而不得不手动测试某些东西时,我一直在想:“如果此代码已经具有单元测试,这将更快。” 每当我不得不尝试向具有高耦合性的代码中添加单元测试功能时,我总是在想:“如果以非耦合的方式编写,这将变得更加容易”。
比较和对比我支持的两个实验站。一个已经存在了一段时间,并且拥有大量的旧代码,而另一个则相对较新。
在向旧实验室中添加功能时,通常是下到实验室并花费大量时间来研究其所需功能的含义以及如何在不影响任何其他功能的情况下添加该功能的情况。根本没有将代码设置为允许离线测试,因此几乎所有内容都必须在线开发。如果我确实尝试离线开发,那么最终我将得到比合理数量更多的模拟对象。
在较新的实验室中,我通常可以通过以下方式添加功能:在我的办公桌上离线开发它,仅模拟那些立即需要的东西,然后只花很短的时间在实验室中,以解决所有未解决的问题。 -线。
为了清楚起见,由于@ naught101问...
我倾向于研究实验控制和数据采集软件,并进行一些临时的数据分析,因此TDD与版本控制的结合有助于记录基础实验硬件的变化以及数据收集要求随时间的变化。
但是,即使在开发探索性代码的情况下,我也可以从编纂假设的过程中看到巨大的好处,并且能够查看这些假设如何随着时间演变。
通常,由于问题的数学结构,与我从事的业务代码相比,科学代码往往具有连锁功能的星座。因此,我认为单个功能的单元测试不是非常有效。但是,我确实认为有一类有效的单元测试,并且与整个程序测试的不同之处还在于它们针对特定的功能。
我只是简单地定义这些测试的含义。当对代码进行更改时,回归测试会寻找现有行为的更改(以某种方式验证)。单元测试运行一段代码,并根据规范检查它是否提供了所需的输出。它们没有什么不同,因为原始回归测试是单元测试,因为我必须确定输出有效。
我最喜欢的数值单元测试示例是测试有限元实现的收敛速度。这绝对不简单,但是它采用了已知的PDE解决方案,在减小网格尺寸遇到了一些问题,然后将误差范数拟合到曲线,其中是收敛速度。我使用Python处理PETSc中的泊松问题。我不是在寻找像回归中的差异,而是为给定元素指定了特别的比率。C h r r r
来自PyLith的单元测试的另外两个示例是:点定位,这是一个易于生成综合结果的单一函数;以及在网格中创建零体积内聚单元,其中涉及多个函数,但解决了网格的一个外接部分。代码中的功能。
有许多此类测试,包括保存性和一致性测试。该操作与回归(您运行测试并根据标准检查输出)没有什么不同,但是标准输出来自规范而不是先前的运行。
自从我阅读第二版Code Complete中的测试驱动开发以来,就一直使用单元测试框架作为开发策略的一部分,由于我编写的各种测试都是诊断性的,因此它减少了调试时间,从而大大提高了我的生产率。附带的好处是,我对自己的科学结果更有信心,并多次使用单元测试来捍卫我的结果。如果单元测试中有错误,我通常可以很快找出原因。如果我的应用程序崩溃并且我的所有单元测试都通过了,我将进行代码覆盖率分析,以查看未执行代码的哪些部分,并使用调试器逐步遍历代码以查明错误源。然后,我编写了一个新测试以确保该错误保持不变。
我写的许多测试不是纯单元测试。严格定义,单元测试应该行使一项功能的功能。当我可以使用模拟数据轻松测试单个函数时,就可以这样做。有时,我无法轻松模拟需要编写测试以行使给定功能的功能的测试所需的数据,因此我将在集成测试中与其他功能一起测试该功能。整合测试一次测试多个功能的行为。正如Matt所指出的那样,科学代码通常是一系列互锁功能的集合,但通常情况下,某些功能是按顺序调用的,可以编写单元测试以在中间步骤测试输出。例如,如果我的生产代码按顺序调用了五个函数,那么我将编写五个测试。第一个测试只会调用第一个函数(因此它是一个单元测试)。然后第二个测试将调用第一个和第二个函数,第三个测试将调用前三个函数,依此类推。即使我可以为代码中的每个功能编写单元测试,也无论如何都要编写集成测试,因为将程序的各个模块组合在一起时可能会出现错误。最后,写完我认为需要的所有单元测试和集成测试后,将我的案例研究包装在单元测试中,并将其用于回归测试,因为我希望结果可以重复。如果它们不可重复,并且得到不同的结果,我想知道为什么。回归测试的失败可能不是一个真正的问题,但是它将迫使我弄清楚新结果是否至少与旧结果一样可信。
除单元测试外,还值得进行静态代码分析,内存调试器以及使用编译器警告标志进行编译以捕获简单错误和未使用的代码。
以我的经验,随着科学研究代码的复杂性增加,在编程中需要一种非常模块化的方法。这对于具有庞大而古老的代码(f77
有人吗?)可能会很痛苦,但是有必要继续前进。当围绕代码的特定方面构建模块时(对于CFD应用程序,请考虑边界条件或热力学),单元测试对于验证新实现,隔离问题和进一步的软件开发非常有价值。
这些单元测试应该比代码验证低一级(我可以恢复波动方程的解析解吗?),比代码验证低2级(我可以在湍流管道中预测正确的峰值RMS值),只需确保编程即可(参数是否正确传递,指针是否指向正确的东西?)和“数学”(此子例程计算摩擦系数。如果我输入一组数字并手动计算解,例程是否得出相同的结果结果?)是正确的。基本上比编译器可以发现的水平高出一层,即基本语法错误。
我肯定会为您的应用程序中的至少一些关键模块推荐它。但是,人们必须意识到它非常繁琐且耗时,因此除非您拥有无限的人力,否则我不建议您将它用于100%的复杂代码。
出于多种原因,科学代码的单元测试很有用。
特别是三个:
单元测试可帮助其他人理解您的代码约束。基本上,单元测试是一种文档形式。
单元测试检查以确保单个代码单元返回正确的结果,并检查以确保修改细节后程序的行为不会改变。
使用单元测试可以更轻松地模块化您的研究代码。如果您开始尝试将代码定位在新平台上,例如您有兴趣对其进行并行化或在GPGPU机器上运行它,则这尤其重要。
最重要的是,单元测试使您有信心使用代码生成的研究结果是有效且可验证的。
我注意到您在问题中提到了回归测试。在许多情况下,回归测试是通过自动,定期执行单元测试和/或集成测试来实现的(单元测试和/或集成测试可以测试代码段在组合时能否正常工作;在科学计算中,这通常是通过将输出与实验数据或测试结果进行比较来完成的)受信任的早期程序的结果)。听起来您已经在大型复杂组件级别成功使用集成测试或单元测试。
我要说的是,随着研究代码变得越来越复杂,并且依赖于其他人的代码和库,了解错误在何处发生非常重要。单元测试使错误更容易查明。
在我与人合着的《科学计算的最佳实践》的论文的第7节“错误计划”中,您可能会发现描述,证据和参考资料很有用-它还引入了防御性编程的补充概念。
在我deal.II类我教的软件,没有测试无法正常工作(并继续强调一点,我故意说:“ 不会无法正常工作”,而不是“ 可能无法正常工作)。
当然,我遵循这句口头禅-这就是处理方法.II每次提交都会运行2500个测试;-)
更严重的是,我认为Matt已经很好地定义了这两类测试。我们为较低级别的内容编写单元测试,并且自然而然地发展为针对较高级别的内容进行回归测试。我认为我无法画出一个清晰的边界来将我们的测试从一侧或另一侧分开,当然有很多人在有人看过输出并认为它在很大程度上是合理的(单元测试?)。而不考虑准确性的最后一点(回归测试?)。
是的,没有。当然,要对用来简化生活的基本工具集的基本例程进行单元测试,例如转换例程,字符串映射,基本物理和数学等。在计算类或函数时,它们通常需要较长的运行时间,并且您实际上可能更喜欢将它们作为功能测试而不是作为单元进行测试。同样,对那些级别和用途将发生很大变化(例如出于优化目的)或内部细节由于任何原因而将发生更改的类和实体进行大量的单元测试和压力测试。最典型的示例是包装从磁盘映射的巨大矩阵的类。
绝对!
什么,那还不够吗?
在科学编程中,我们比其他任何种类的开发都基于尝试匹配物理系统。除了测试之外,您怎么知道您是否还做了?在甚至开始编码之前,请决定如何使用代码并计算出一些示例运行。尝试抓住任何可能的边缘情况。以模块化方式进行操作-例如,对于神经网络,您可以对单个神经元进行一组测试,而对完整的神经网络进行一组测试。这样,当您开始编写代码时,可以确保在开始使用网络之前您的神经元能够正常工作。像这样分阶段进行工作意味着,当您遇到问题时,只有最新的“阶段”代码要测试,较早的阶段已经过测试。
另外,一旦完成测试,如果您需要用其他语言(例如转换为CUDA)重写代码,或者即使您只是对其进行更新,则已经有测试用例,可以使用它们来编写确保程序的两个版本都以相同的方式工作。
是。
无需单元测试就可以编写任何代码的想法令人厌恶。除非您证明代码正确,然后证明证明正确= P。
我会务实地而不是教条地解决这个问题。问自己一个问题:“函数X可能出什么问题?” 想象一下,当您在代码中引入一些典型的错误时,输出会发生什么:错误的前置因子,错误的索引等等,然后编写可能检测到这种错误的单元测试。如果对于给定的功能,如果不重复该功能本身的代码就无法编写此类测试,则不要-而是考虑下一个更高级别的测试。
科学代码中的单元测试(或实际上是任何测试)的一个更为重要的问题是如何处理浮点运算的不确定性。据我所知,目前还没有好的通用解决方案。
我为Tangurena感到遗憾-在这里,口头禅是“未经测试的代码是破损的代码”,它来自老板。我只想补充一些细节,而不是重复进行单元测试的所有好理由。
在开发化学求解器(用于复杂的地质领域)时,我使用了一种稍微不同的方法,即所谓的“ 通过复制和粘贴代码段进行单元测试”。
在这段时间内,为嵌入大型化学系统建模器中的原始代码构建测试工具是不可行的。
但是,我能够编写出越来越复杂的代码片段,以显示化学式的(Boost Spirit)解析器是如何工作的,作为针对不同表达式的单元测试。
最终的,最复杂的单元测试非常接近系统中所需的代码,而不必将代码更改为可模拟的。因此,我能够跨单元测试代码进行复制。
使得这不仅仅是学习练习和真正的回归套件的还有两个因素-单元测试保留在主要来源中,并作为该应用程序的其他测试的一部分运行(是的,它们确实从Boost中获得了副作用) 2年后改变了精神)-由于复制和粘贴的代码在实际应用程序中已进行了最小限度的修改,因此它可以包含注释,以回溯到单元测试,以帮助某人保持同步。
对于更大的代码库,高级内容的测试(不一定是单元测试)很有用。某些更简单算法的单元测试也很有用,以确保您的代码没有废话,因为您的辅助函数正在使用sin
而不是cos
。
但是对于整体研究代码而言,编写和维护测试非常困难。在没有有意义的中间结果的情况下,算法往往会很大,中间结果可能会进行明显的测试,并且往往需要很长时间才能运行出结果。当然,您可以针对具有良好结果的参考运行进行测试,但是就单元测试而言,这并不是一个很好的测试。
结果通常是真实解的近似值。虽然您可以测试简单的功能(如果它们在一定程度上精确到一定程度),但是很难验证某个结果网格是否正确,这是用户(您)之前通过目视检查所评估的。
在这种情况下,自动化测试通常具有太高的成本/收益比。我建议更好的方法:编写测试程序。例如,我编写了一个中等大小的python脚本来创建有关结果的数据,例如边缘大小和网格角度的直方图,最大和最小三角形的面积及其比例等。
我既可以用它在正常运行期间,以评估输入和输出网格和使用它之后,我改变了算法有一个全面的检查。当我更改算法时,我并不总是知道新的结果是否更好,因为通常没有绝对的量度哪个近似是最好的。但是通过生成这样的度量标准,我可以说出一些因素,比如“新版本最终具有更好的角度比,但收敛速度更差”会更好。