论坛首页 综合技术论坛

实践TDD的点滴——寻找可测的后置条件

浏览 3521 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (4) :: 隐藏帖 (0)
作者 正文
   发表时间:2010-02-27   最后修改:2010-02-28
    刚开始进行TDD的人,一开始着手写测试时,经常不知从何入手。《测试驱动开发》里有提到断言优先,也就是可以先写assert那句话。但要测什么呢,其实就是在测试后函数后置条件,也就是执行完函数后会产生什么结果。
   
    最简单的检测就类似add()这种计算类函数,通过返回值就可以判断了。但实际开发中,我们往往遇到更多的是没法通过返回值来检测后置条件的函数。寻找可测的后置条件成为了问题的焦点。但后置条件在哪里呢,还真不容易判断,同一个情况不同的人可能会找出不同的后置条件。

    例如:游戏世界需要将玩家周围发生的事件发送给该玩家客户端。这里用观察者模式实现,也就是客户端A预先向游戏世界订阅玩家A周围的事件,之后每当玩家A周围的其他玩家发生行为变化时,都会通知给客户端A。简化成函数就是:
class GameWorld
{
public:
    void subscribeSync(int playerId, GameWorld::Listener* listener)
    {
    }
};


    上面这个例子后置条件是什么?你可能会立即说需求里不是明摆写着,后置条件就是每次玩家A周围的其他玩家发生行为变化时,订阅者就会收到该事件。如果把这作为后置条件,我们来看看要做准备哪些东西,要为游戏世界添加玩家A和一个玩家B,玩家B在玩家A的身边,然后让玩家B做一个行为,再检测客户端A是否收到了这个事件。这里面又涉及东西就多了。要把玩家添加到游戏世界中;世界在刷新时才同步信息给客户端,你要去完成刷新函数中的同步代码;玩家B在玩家A身边怎么判断的,还需要计算一定距离内的玩家算为在玩家A身边。等等,老兄,我们现在才在写订阅,怎么一下子要去实现那么多功能。

    那怎么办呢?要不就创造一个简单可以让外面检测的后置条件吧。由world提供一支函数getSubscribedListeners返回所有订阅者,通过判断订阅者列表里是否存在之前添加进去的那个listener来测试。
class GameWorld
{
public:
    list<SubscribeListener> getSubscribedListeners()
    {
    }
};

或着更直接点,让world提供一支hasListener(),判断某个listener是否存在,来进行测试。
class GameWorld
{
public:
    virtual bool hasListener(GameWorld::Listener* listener)
    {
    }
};


    这样做不就简单解决了?的确这样做单从添加订阅者的角度OK了,但依然存在一些不妥的地方。
   
    首先需求里并不需要world提供getSubscribedListeners或hasListener,这种函数大多情况下最终只会在测试中调用到。由于要测试的逻辑很多,你很快会发现你写了一堆类似的检测函数,而且都要做成public,这样class的阅读者就不容易清晰的看出哪些是class的核心函数了,从可维护性的角度良好的设计一个评判的标准不就是让代码看起来更简洁吗。

    另外,如果为了定位快速,保存listener由list变成是map,那么你就需要改变getSubscribedListeners了。也就是world类内部的实现细节影响到了要修改测试代码。换句话说就是测试不只是依赖于类的外部功能,还依赖于类的内部实现。如果测试依赖于内部实现,你就会发现重构起来,很多的测试都需要修改甚至重写。也许我举的例子不足于让你感觉到麻烦,因为这个例子太简单了。但我确实在项目中遇到了很多重构时重写测试的麻烦,特别是测试代码往往写得很长,一堆的mock类,看起来很费神。有时候干脆就把整个测试去掉。

    这里其实涉及到一个问题,就是要从需求的角度去写测试,还是从实现的角度去写测试。前者往往没有明确的、直接可测的后置条件,测试代码不容易写。后者会导致测试依赖于内部实现造成测试代码不稳定经常要修改。我时常纠结于这个问题,不知道大家是否也有此困惑,一共来探讨吧!
   发表时间:2010-02-28   最后修改:2010-02-28
    感觉论坛里都在讨论TDD的概念,很少去讨论具体应用中遇到的问题,而且最近关于TDD的话题都很少了。我觉得TDD之所以用的人少,就是更多人只是观望,很少真正用到项目。或着稍涉足,遇到一堆的具体问题,不知如何是好,最后放弃。为什么大家不从一些具体实践遇到的细节问题展开探讨,逐步扫除实施上的障碍,最后才去领悟TDD的价值。
0 请登录后投票
   发表时间:2010-04-13  
我这两天正在搞单元测试的东东,想整理一下哪些方法是可测试哪些是不需要测试的,可以方便测试代码的编写,同时起到编程规范的作用。目前我使用Jtest工具,一直在网上找不到专业一点的理论知识,要自己总结太郁闷了。
0 请登录后投票
   发表时间:2010-04-13  
这个确实纠结,很难两全,
但是我宁愿选择去写啰嗦的黑盒单元测试,可以带来更多的安全感,而且测试本身不易碎。
0 请登录后投票
   发表时间:2010-04-14  
bingo1018 写道
我这两天正在搞单元测试的东东,想整理一下哪些方法是可测试哪些是不需要测试的,可以方便测试代码的编写,同时起到编程规范的作用。目前我使用Jtest工具,一直在网上找不到专业一点的理论知识,要自己总结太郁闷了。


这是个风险驱动的东西。
简单的说,你觉得很放心的地方可以不测,你觉得总有点心神不宁那就要测。
0 请登录后投票
   发表时间:2010-04-14   最后修改:2010-04-20
"游戏世界需要将玩家周围发生的事件发送给该玩家客户端。这里用观察者模式实现,也就是客户端A预先向游戏世界订阅玩家A周围的事件,之后每当玩家A周围的其他玩家发生行为变化时,都会通知给客户端A。"

这个场景我会做一个桩玩家. 该桩可以知道发送给玩家A的事件. 用这个桩替换玩家A.
操作玩家B发生一个动作, 然后检测桩是否收到事件.

我觉得测试代码写的白盒(注:误. 应为黑盒)一点,易于重构代码. 即通过public的接口来assert. 这样重构实现的时候不影响测试代码, 测试代码才能够成为一张自动保护你的代码行为的网.
0 请登录后投票
   发表时间:2010-04-19  
强强爱妍妍 写道
这个场景我会做一个桩玩家. 该桩可以知道发送给玩家A的事件. 用这个桩替换玩家A.
操作玩家B发生一个动作, 然后检测桩是否收到事件.


是呀,如果是这样的写法,比较扣住需求,不依赖于实现细节。但问题是,我现在只是写订阅,至于同步于玩家B的事件给A,并还没开始开发呀,同步的逻辑更加的复杂。TDD不是一直要我们把想到的事做放入TODO,然后继续朝着我们的目标前进吗?因此没法通过后续的真正后置条件——同步玩家周围的事件来测,我只能以是否添加成功来测试。

强强爱妍妍 写道
我觉得测试代码写的白盒一点,易于重构代码. 即通过public的接口来assert. 这样重构实现的时候不影响测试代码, 测试代码才能够成为一张自动保护你的代码行为的网.

“黑盒一点”是否是笔误?应该黑盒一点在重构时才不影响代码吧
0 请登录后投票
   发表时间:2010-04-19  
mock1234 写道
TDD测试不是传统单元测试(虽然工具一样,但是思想是反叛的)。

传统的单元测试,其实完全可以用程序代码中的断言来实现,而没有必要使用单元测试工具(单元测试工具只是运行一些模拟使用功能的用例好让那些断言可以跑起来)。

而TDD是设计。你写的每一行代码,回想一下是先写手工文档然后直接代码还是用TDD代码驱动出来的,这才是TDD要的结果。而将传统的单元测试过多地移植到TDD中,可能移植了过多目的不明确的东西。


同意你的观点,但TDD总是建立在一个个的单元测试基础上的,既然是测试,免不了要考虑测什么,怎么测
0 请登录后投票
   发表时间:2010-12-31  
windflawlyq 写道
mock1234 写道
TDD测试不是传统单元测试(虽然工具一样,但是思想是反叛的)。

传统的单元测试,其实完全可以用程序代码中的断言来实现,而没有必要使用单元测试工具(单元测试工具只是运行一些模拟使用功能的用例好让那些断言可以跑起来)。

而TDD是设计。你写的每一行代码,回想一下是先写手工文档然后直接代码还是用TDD代码驱动出来的,这才是TDD要的结果。而将传统的单元测试过多地移植到TDD中,可能移植了过多目的不明确的东西。


同意你的观点,但TDD总是建立在一个个的单元测试基础上的,既然是测试,免不了要考虑测什么,怎么测

如何单元测试是难点。
0 请登录后投票
论坛首页 综合技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics