5

我必须使用一些指向数据库的遗留 Delphi 代码,并使其支持具有完全不同模式的新的、更好的数据库。更新后的数据库具有相同的数据。它结合了存储过程和嵌入式 SQL。

有没有一种好的测试驱动开发技术可以帮助确保我不会破坏任何东西?这段代码几乎没有单元测试,我需要对很多硬编码的 SQL 进行更改。

在每次更改后运行听起来容易出错且耗时。我喜欢做 TDD 或 BDD 的想法,只是不知道该怎么做。

4

3 回答 3

13

你想进入单元测试是件好事,但我想提醒你不要过度热心地接受它。

将单元测试添加到遗留代码是一项重大任务,而仅仅为了添加测试用例而停止其他工作几乎总是完全不可行的。此外,除非您已经有 TDD 方面的经验,否则学习曲线本身可能会成为一个难以克服的障碍。

但是,如果你坚持不懈,一步一步地做事,你的努力最终会得到回报。

您可能会遇到的问题:

  • 遗留应用程序通常很难“改造”测试用例。这是因为编写代码时没有考虑到可测试性。
    • 许多例程做的事情太多,因此测试必须考虑大量的副作用。
    • 代码不是完全独立的,因此为测试设置前置条件需要做很多工作。
    • 测试/检查行为的入口点经常丢失,因为生产代码不需要它们;因此一开始就没有添加。
    • 代码通常依赖于某处的全局状态。直接,或通过 Singleton 的。这种全局状态(无论它位于何处)会对您的测试用例造成严重破坏。
  • 数据库的单元测试本质上比其他类型的单元测试更困难。原因是测试用例不喜欢全局状态——而数据库实际上是全局状态的巨大容器。问题以多种方式表现出来:
    • 如果您使用 IDENTITY 列、Auto Inc 或任何形式的数字生成器:这些会导致每次测试运行之间存在特定差异,或者您需要一种方法来在测试之间重置这些数字。
    • 数据库很慢。一旦你建立了大量的测试用例,在每次更改之间运行所有测试是不切实际的。(我的一个 Db 测试套件需要将近 10 分钟才能运行。)
    • 如果您的数据库生成日期/时间值,这些也会使测试复杂化。特别是如果数据库在不同的机器上运行。
    • 数据库测试由于数据库有两个方面而变得复杂:它的模式和它的数据。因此,如果您希望测试一个新的/更改的存储过程(架构的一部分),它需要对数据进行适当的更改,并可能对架构的其他方面(例如表/视图)进行适当的更改。
  • 即使没有上述额外的复杂情况,您也必须处理“正常问题”。
    • 全局状态经常在一些尴尬的地方意外出现。考虑Now()哪个返回 TDateTime。它使用全局状态:当前日期时间。如果您的系统中有基于时间/日期的规则,则这些规则可能会根据您的测试运行时间返回不同的结果。除非您找到应对这一挑战的有效方法,否则您将拥有许多“不稳定”的测试用例。
    • 编写测试用例是一种与大多数开发人员习惯的完全不同的编程范式。打破旧习惯可能非常困难。测试用例代码的风格几乎是声明式的:鉴于这个我这样做时,我希望这已经发生。测试用例需要简单明了地说明他们想要实现的目标。
    • 学习曲线可能很棘手。最初,如果不熟悉测试用例,您可能会发现自己编写代码的时间是原来的 3 倍。即使它最终会有所改善(甚至可能达到你比以前使用非结构化和随意的测试更快的程度) - 你周围的其他人可能会表达沮丧。(如果是你的老板,那就不酷了。)

希望我没有气馁,我确实有一些实用的建议:

俗话说“嚼不烂”。
准备慢慢开始。暂时,以你熟悉的方式继续你的大部分工作。但是强迫自己每天写一两个测试用例。随着您变得更加舒适,您可以增加这个数字。

Try 坚持“久经考验的原则”
TDD 的工作流程是:首先编写测试并确保测试失败。我知道很难坚持这个习惯,但这个原则有一个非常重要的目的。这是您的测试用例证明错误/缺失功能的确认级别。我经常看到测试用例代码在有/没有生产更改的情况下都会通过 - 使测试有些无用。

对于您的数据库测试,您需要建立一个适合您的框架。
首先,您需要一种使数据库进入“基本状态”的机制。您的所有测试都应该能够通过的一种——无论它们运行什么顺序或多少次。通常这将涉及测试之间的某种重置(但它需要非常快)。其次,您需要一种简单的方法来将数据库的模式更新为生产代码所期望的。

最初,您只想测试新功能或错误修复。
避免测试一切的诱惑。随着时间的推移,您的测试用例覆盖率将会增加。一旦你的框架和模式建立起来,你就有机会开始添加测试来增加覆盖率。

重构现有代码。
随着您对测试的熟悉,您将了解使测试变得更加困难的编码习惯。您可能会在遗留代码中发现许多此类问题。这样的代码将无法按原样进行测试。你可能需要在测试之前重构你的代码。显然这并不理想,因为您宁愿让测试始终通过以证明您的更改没有破坏任何东西。一本关于重构的好书会给你一些你可以使用的技术,这些技术将改变你的代码结构而不改变它的行为。

测试现有代码。
在为现有例程编写测试时,请查看代码并确定可能影响不同行为的每个输入。例如,当有if语句时,某些情况会导致条件评估为 True,而其他情况则为 False。至少,您需要对每个排列进行测试。

于 2013-09-05T20:24:43.290 回答
2

在您的位置,我将使用DUnit创建一个单元测试项目。对于每个实体,我会编写运行新旧句子的测试方法,然后编写方法来比较结果。

我会编写一个名为 TTestCase 的类,比如说TMyTestCase,并向其中添加一些辅助方法,然后将我的新测试类创建为TMyTestCase的子类。

祖先类的想法是提供通用功能,使编写测试(例如比较方法)更容易,以提高生产力和舒适度。

于 2013-09-05T12:59:46.660 回答
0

您可以开始构建数据库模拟器。连接它而不是旧的,看看它需要做什么。虽然工作量很大

于 2013-09-29T16:55:35.243 回答