TDD、DDD和封装性

24

经过几年的遵循工作中'架构师'传下来的不良实践,并认为一定有更好的方法,我最近一直在阅读TDD和DDD方面的知识,我认为这些原则和实践非常适合我们编写的复杂软件。

然而,我看到的许多TDD示例调用了域对象上的一个方法,然后测试对象的属性以确保行为执行正确。

另一方面,业内的一些知名人士(尤其是Greg Young在他关于CQRS的演讲中)主张通过删除所有“getters”来完全封装每个域对象。

因此,我的问题是:如果禁止检索域对象的状态,如何测试域对象的功能?

我认为我错过了一些基本的东西,请随意称呼我为白痴并启发我 - 任何指导将不胜感激。


1
哈哈,我在阅读完这篇文章后想更深入地了解“无 getter”原则...而这篇文章是谷歌搜索结果的第一篇。 - Frank Schwieterman
我想我一直在寻找一个模式的名称,这个模式在其他地方已经被讨论过了。最初,我观看了一个关于命令查询分离的视频,它提倡只写域和查询数据存储的替代路径。然后我又读了几篇文章,讨论了getter违反封装等问题。也许这些术语会产生更好的搜索结果? - Justin
从你的评论中,我认为你听到的可能是“no-setters”。很多人认为设置器违反了封装性。一般来说,我同意他们被过度使用,但在许多情况下仍然是有用和必要的。 - Phil Sandler
绝对不要使用getter。我在谈论CQRS风格系统中的DDD,其中领域模型将仅为写入,因此您将完全封装您的对象在领域模型中,以真正的OO意义上。正如您可以从我接受的答案以及稍后谈论领域事件的答案中看到的那样,已经有了允许测试具有这种封装级别的模型的测试模式。我将更改问题的标题以更正确地反映我的意思-一年后,即使是我也能看出,“无getter原则”是误导性的。 - Justin
8个回答

17
你所描述的是状态验证,其中你会在领域对象的状态上进行断言。有一种被称为行为验证的TDD分支利用模拟对象。
行为验证允许你指定应该调用哪些方法,以及如果需要,哪些方法不应该调用。
请查看Martin Fowler的这篇文章以获取更多细节:模拟不是存根

太棒了 - 这给了我很多关键词可以搜索,还有更多的阅读材料!感谢您提供如此出色和快速的答案。 - Justin
阅读马丁·福勒的相关内容 - 他在这个领域写了很多,并且是一个很好的资源。此外,我发现肯特·贝克所著的《测试驱动开发:实例》是入门TDD的好材料。 - Gavin Miller
我正在考虑肯特·贝克的书,但是我看到的一些评论(尤其是亚马逊上的评论)并不那么好。你确定要推荐这本书吗? - Justin
我发现这是一个很好的TDD入门资料。如果你坐下来,编写示例代码,认真“读懂这本书”,那么你将会受益匪浅。然而,如果你只是阅读它,那就不值得了。 - Gavin Miller
Fowler让人觉得这种行为验证非常脆弱,仅通过测试某些方法是否通过模拟对象调用,实际实现的重构变得痛苦。昨晚阅读了一些其他内容后,我对无getter和纯行为验证并不信服。我的意思是 - 即使您有getter,您仍然可以应用“告诉,不要问”,对于只读属性使用面向行为的方法来改变状态(例如,Order具有Submit或Cancel等方法的Status属性)。 - Justin
嘿,Justin,最近我也在考虑同样的问题,但我确信我错了。假设你已经接受了首先要有一个只写域的想法,那么如果你有任何getter,你就会遇到麻烦。只写域原则希望你从你的域对象中触发一个事件,或者从你的域对象编写的投影中读取,或者类似的操作。一旦你暴露getter,你就开始暴露对象的“形状”,正如Greg Young所说,“域对象具有行为,而不是形状”。 - Charlie Flowers

9

好的,这个答案晚了一年;-)

但是当您想要测试CQRS模型时,您可以对触发的领域事件进行断言,而不是对实体状态进行断言。

例如,如果您想要测试调用:customer.Rename("Foo")是否会产生正确的行为。

您不是测试customer.Name是否等于"foo",而是测试在您的待处理事件存储中是否有一个挂起的CustomerRename事件,其值为"Foo"。 (根据实现方式,在您的uow或实体事件列表中)


4
如果您真的要禁止状态检索,那么您只能限于行为测试,可能需要使用像TypeMock这样的模拟框架来跟踪对象的行为。如果您能够进行纯BDD(行为驱动开发),那么理论上您可以通过系统的行为方式断言整个系统的正确性。
实际上,在许多情况下,我发现BDD比有状态的测试更加脆弱。虽然有些人可能会呼吁采用某种理论,但只有在它适合您的情况下才有效。基于状态的测试仍然占我们编写的所有单元测试的90%,并且我们的团队非常了解BDD。
做最适合您的事情。

2

嘿,贾斯汀,和你一样,我最近考虑为了单元测试而给我的只写域对象添加getter,但现在我确信我错了。假设你已经接受了只写域的想法,那么如果你有getter,你就会遇到麻烦。只写域原则希望你从域对象中触发事件,或者从域对象写入的投影中读取,或者类似这样的操作。一旦你暴露getter,你就开始暴露对象的“形状”,正如Greg Young所说,“域对象具有行为,而不是形状”。

话虽如此,我也在思考与你相同的问题......如何对只写域对象进行单元测试?这是我的当前计划:我想让我的域对象触发一个域事件,表示“这些属性已更改”,在我的单元测试中,我将在发送“EditCommand”之前注册它。请查看Udi Dahan关于域事件的文章here,还可以参考Eric Evans关于域事件的说法


2

有几点需要注意。

首先,当你使用TDD来使你的代码可测试时,你会得到更小的类。如果你有一个有很多私有属性的类,你无法检查它们,那么将其拆分成多个类并使其更加可测试是一个不错的选择。

其次,老式的面向对象架构试图通过使用语言保护措施来防止访问某些内容来使软件更加安全。而TDD架构通过编写验证代码实际执行情况的测试来使软件更加强健,从而减少了使用语言结构来确保程序不会做什么的重视程度。

最后,检查属性并不是验证代码是否按照预期执行的唯一方式。书籍xUnit Design Patterns在这里记录了其他方法:http://xunitpatterns.com/Result%20Verification%20Patterns.html


非常感谢您的快速回复 - 这个链接太棒了,我开始明白了。 - Justin
如果你点击链接,你会喜欢这本书的(至少我是这样的)。 - Frank Schwieterman

2

2
你提到的是状态测试。还有行为测试。用于此的技术包括依赖注入、控制反转和模拟:
你的类的所有副作用都是其“依赖项”上的方法调用--即从外部提供的对象,通常在构造函数中。然后,在你的单元测试中,你提供一个假对象而不是真实对象。假对象可以记住它是否被调用,这就是你在测试中断言的内容。
存在许多模拟框架,通过动态生成实现给定接口的类来自动创建模拟对象。最流行的是Rhino.Mocks和Moq。

对于简洁的回答点个赞。您能否提供一个示例或参考示例来解释IoC如何用于跟踪对象的方法调用?不清楚注入的对象是否会跟踪其他对象上特定的外部方法调用,还是关于跟踪同一对象上的内部方法调用,或者两者都有。 - jpierson

1

链接已经失效。这是第二个项目的PDF文件:http://www.jmock.org/oopsla2004.pdf,这是第一个项目的大部分链接:http://msdn.microsoft.com/en-us/magazine/dd882516.aspx。 - sivabudh
第二个链接对我有效,如果你把mockobjects.com改成www.mockobjects.com,第一个链接也能用。 - SCFrench

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接