如何将TDD应用于读/写功能?


10

好像是鸡和鸡蛋的问题。

您可以使写入功能写入某些数据存储,但是在没有经过测试的读取功能的情况下,永远不会知道您是否正确保存了它。

您可以使读取功能从数据存储中读取,但是如何在没有经过测试的写入功能的情况下将东西放入该数据存储中以进行读取呢?

编辑:

我正在连接到SQL数据库并与之进行事务,以保存和加载要使用的对象。测试数据库提供的访问功能毫无意义,但是我包装了这些数据库功能以对对象进行序列化/反序列化。我想确保我正在正确地从数据库中写入和读取正确的内容。

它不像@snowman提到的添加/删除。我想知道我写的内容是正确的,但这需要经过良好测试的读取功能。当我阅读时,我想确保我的阅读正确地创建了一个与所写内容相等的对象。但这需要经过良好测试的写入功能。


您是在编写自己的数据存储还是使用现有的数据存储?如果您使用的是现有的,请假定它已经可以使用。如果您是自己编写的,则其工作方式与使用TDD编写任何其他软件的方式相同。
罗伯特·哈维


1
虽然密切相关,但我认为这不是该特定问题的重复。欺骗对象正在谈论添加/删除,这是读/写。区别在于,读/写依赖关系可能依赖于正在读/写的对象的内容,而简单的添加/删除测试可能会更简单:对象是否存在

2
您可以暂存一个完全没有数据且没有数据的数据库来测试读取功能。
JeffO

Answers:


7

从读取功能开始。

  • 在测试设置中:创建数据库并添加测试数据。通过迁移脚本或从备份。由于这不是您的代码,因此不需要在TDD中进行测试

  • 在测试中:实例化您的存储库,指向您的测试数据库,然后调用Read方法。检查是否返回测试数据。

现在,您具有经过全面测试的读取功能,可以转到写入功能,该功能可以使用现有的读取来验证其自身的结果


我想您可以在内存中创建一个数据库来加快处理速度,但这可能太复杂了。为什么不在单元测试中使用模拟呢?
BЈовић

1
这里有点恶魔的拥护者,但是您如何测试数据库是否正确创建?如OP所述,鸡肉和鸡蛋。
user949300

1
@Ewan-我绝对同意您不应该测试他们的数据库代码。但是,怎么知道您的数据库设置代码没有忘记在某处插入INSERT或在列中输入错误的值呢?
user949300

1
从纯TDD方法开始,测试就是要求。因此,从逻辑上讲它不可能是错误的。在现实世界中观察,您必须加以注意
Ewan

1
Quis custodiet ipsos custodes?或者,“谁来测试测试?” :-)我同意您的看法,在纯净的TDD世界中,这将是非常繁琐且容易出错的方法(特别是如果它是具有8个JOINS的复杂多表结构)。已投票。
user949300

6

我经常只做写然后读。例如(伪代码)

Foo foo1 = setup some object to write
File tempfile = create a tempfile, possibly in memory 
writeFoo(foo1, tempfile) 
Foo foo2 = readFoo(tempfile) 
assertEquals(foo1, foo2); 
clean-up goes here

以后添加

除了该解决方案“实用”和“足够好”之外,还可以说其他解决方案测试了错误的东西。测试字符串或SQL语句是否匹配不是一个糟糕的主意,我自己做过,但是它正在测试副作用,而且很脆弱。如果您更改大小写,添加字段或更新数据中的版本号该怎么办?如果您的SQL驱动程序切换了调用顺序以提高效率,或者更新的XML序列化器增加了额外的空间或更改了架构版本,该怎么办?

现在,如果您必须严格遵守某些官方规范,那么我同意检查详细信息是适当的。


1
因为它是90%的真正伪代码?不确定。也许突出显示文本并减少代码的噪音?
RubberDuck

1
是的,@ Ewan。狂热者对此不以为然,但是务实的程序员会说“足够好”并继续前进。
RubberDuck

1
我有点把问题读为..“假设我像狂热分子一样遵循TDD ...”
Ewan

1
我对OP和TDD的解释是,您的测试应首先编写,除非同时在其他地方进行了测试,否则不要同时使用读写。
伊万

2
您正在测试,“读取应返回我写的内容”,但要求“读取应从db返回数据”和“写入应将数据写入db”
Ewan

4

别。不要对I / O进行单元测试。这是浪费时间。

单元测试逻辑。如果要在I / O代码中测试很多逻辑,则应重构代码,以将I / O操作方式和I / O操作逻辑与实际的I / O业务分开。 (几乎无法测试)。

为了详细说明,如果要测试HTTP服务器,则应通过两种类型的测试进行测试:集成测试和单元测试。单元测试应该完全不与I / O交互。这很慢,并且引入了许多错误条件,这些错误条件与代码的正确性无关。单元测试不应受网络状态的限制!

您的代码应分开:

  • 确定发送什么信息的逻辑
  • 确定发送哪个字节以发送特定信息位(我如何将响应等编码为原始字节)的逻辑,以及
  • 实际上将这些字节写入套接字的机制。

前两个涉及逻辑和决策,需要进行单元测试。最后一个不涉及做出许多决定(如果有的话),并且可以使用集成测试进行出色的测试。

实际上,通常这只是一个好的设计,但其原因之一是它使测试更容易。


这里有些例子:

  • 如果要编写从关系数据库获取数据的代码,则可以进行单元测试,以将关系查询返回的数据映射到应用程序模型。
  • 如果您正在编写将数据写入关系数据库的代码,则可以在不实际测试使用的特定SQL查询的情况下,对要写入数据库的数据进行单元测试。例如,您可以在内存中保留应用程序状态的两个副本:一个代表数据库外观的副本和一个工作副本。当您想同步到数据库时,您需要比较它们并将差异写入数据库。您可以轻松地对该差异代码进行单元测试。
  • 如果要编写从配置文件中读取内容的代码,则要测试配置文件格式解析器,但要使用测试源文件中的字符串而不是磁盘中的字符串。

2

我不知道这是否是标准做法,但对我来说很好。

在我的非数据库读写方法实现我用我自己的类型特异性toString()fromString()方法的实施详情。

这些可以很容易地单独测试:

 assertEquals("<xml><car type='porsche'>....", new Car("porsche").toString());

对于实际的读写方法,我有一个集成测试,可以在一个测试中进行物理读写

顺便说一句:拥有一个同时测试可读写的测试有什么问题吗?


它可能不会感觉良好或“纯净”,但这是务实的解决方案。
RubberDuck

我也很喜欢一起测试读写的想法。您的toString()是一个不错的实用折衷方案。
user949300

1

已知数据必须以已知方式格式化。实现此目的最简单的方法是使用常量字符串并比较结果,如@ k3b所述。

但是,您不仅限于常量。您可以使用其他类型的解析器来提取书面数据的许多属性,例如正则表达式,甚至是寻找数据特征的临时探针。

至于读取或写入数据,拥有一个内存文件系统可能会很有用,该文件系统允许您运行测试而不会受到系统其他部分的干扰。如果您无权访问良好的内存文件系统,请使用临时目录树。


1

使用依赖注入和模拟。

您不想测试您的SQL驱动程序,也不想测试您的SQL数据库是否在线并正确设置。那将是集成测试或系统测试的一部分。您想测试代码是否发送了应该发送的SQL语句,以及是否以应有的方式解释了响应。

因此,当您有一个应该对数据库执行某些操作的方法/类时,请不要让它本身获得该数据库连接。对其进行更改,以便将代表数据库连接的对象传递给它。

在生产代码中,传递实际的数据库对象。

在单元测试中,传递一个模拟对象,该对象的行为就像实际的数据库实际上没有联系数据库服务器。只需检查它是否接收到了应该接收的SQL语句,然后以硬编码响应进行响应即可。

这样,您甚至不需要实际的数据库就可以测试数据库抽象层。


魔鬼的代言人:您如何知道它“应该接收”哪个SQL语句?如果数据库驱动程序根据代码中出现的内容优化顺序,该怎么办?
user949300

@ user949300数据库模拟对象通常替换数据库驱动程序。
菲利普

在测试存储库时,没有必要注入模拟的数据库客户端。您必须测试您的代码是否运行了可在数据库上运行的sql。否则,您将只测试您的模拟游戏
Ewan

@Ewan并不是单元测试的目的。单元测试测试与世界其他地方隔离的一个代码单元。您不是要测试组件之间的交互,例如代码和数据库。这就是集成测试的目的。
菲利普

是。林说没有点单元测试数据库存储库。集成测试是唯一值得做的事情
Ewan

0

如果您使用的是对象关系映射器,则通常会有一个关联的库,该库可用于通过创建聚合,将其持久化并从新的会话中重新加载它,然后针对原始对象。

NHibernate提供持久性规范测试。可以将其配置为针对内存存储进行工作,以进行快速的单元测试。

如果遵循“存储库”和“工作单元”模式的最简单版本,并测试所有映射,则可以依靠几乎可以正常工作的东西。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.