刚开始进行TDD的人,一开始着手写测试时,经常不知从何入手。《测试驱动开发》里有提到断言优先,也就是可以先写assert那句话。但要测什么呢,其实就是在测试后函数后置条件,也就是执行完函数后会产生什么结果。
最简单的检测就类似add()这种计算类函数,通过返回值就可以判断了。但实际开发中,我们往往遇到更多的是没法通过返回值来检测后置条件的函数。寻找可测的后置条件成为了问题的焦点。但后置条件在哪里呢,还真不容易判断,同一个情况不同的人可能会找出不同的后置条件。
例如:游戏世界需要将玩家周围发生的事件发送给该玩家客户端。这里用观察者模式实现,也就是客户端A预先向游戏世界订阅玩家A周围的事件,之后每当玩家A周围的其他玩家发生行为变化时,都会通知给客户端A。简化成函数就是:
上面这个例子后置条件是什么?你可能会立即说需求里不是明摆写着,后置条件就是每次玩家A周围的其他玩家发生行为变化时,订阅者就会收到该事件。如果把这作为后置条件,我们来看看要做准备哪些东西,要为游戏世界添加玩家A和一个玩家B,玩家B在玩家A的身边,然后让玩家B做一个行为,再检测客户端A是否收到了这个事件。这里面又涉及东西就多了。要把玩家添加到游戏世界中;世界在刷新时才同步信息给客户端,你要去完成刷新函数中的同步代码;玩家B在玩家A身边怎么判断的,还需要计算一定距离内的玩家算为在玩家A身边。等等,老兄,我们现在才在写订阅,怎么一下子要去实现那么多功能。
那怎么办呢?要不就创造一个简单可以让外面检测的后置条件吧。由world提供一支函数getSubscribedListeners返回所有订阅者,通过判断订阅者列表里是否存在之前添加进去的那个listener来测试。
或着更直接点,让world提供一支hasListener(),判断某个listener是否存在,来进行测试。
这样做不就简单解决了?的确这样做单从添加订阅者的角度OK了,但依然存在一些不妥的地方。
首先需求里并不需要world提供getSubscribedListeners或hasListener,这种函数大多情况下最终只会在测试中调用到。由于要测试的逻辑很多,你很快会发现你写了一堆类似的检测函数,而且都要做成public,这样class的阅读者就不容易清晰的看出哪些是class的核心函数了,从可维护性的角度良好的设计一个评判的标准不就是让代码看起来更简洁吗。
另外,如果为了定位快速,保存listener由list变成是map,那么你就需要改变getSubscribedListeners了。也就是world类内部的实现细节影响到了要修改测试代码。换句话说就是测试不只是依赖于类的外部功能,还依赖于类的内部实现。如果测试依赖于内部实现,你就会发现重构起来,很多的测试都需要修改甚至重写。也许我举的例子不足于让你感觉到麻烦,因为这个例子太简单了。但我确实在项目中遇到了很多重构时重写测试的麻烦,特别是测试代码往往写得很长,一堆的mock类,看起来很费神。有时候干脆就把整个测试去掉。
这里其实涉及到一个问题,就是要从需求的角度去写测试,还是从实现的角度去写测试。前者往往没有明确的、直接可测的后置条件,测试代码不容易写。后者会导致测试依赖于内部实现造成测试代码不稳定经常要修改。我时常纠结于这个问题,不知道大家是否也有此困惑,一共来探讨吧!
最简单的检测就类似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类,看起来很费神。有时候干脆就把整个测试去掉。
这里其实涉及到一个问题,就是要从需求的角度去写测试,还是从实现的角度去写测试。前者往往没有明确的、直接可测的后置条件,测试代码不容易写。后者会导致测试依赖于内部实现造成测试代码不稳定经常要修改。我时常纠结于这个问题,不知道大家是否也有此困惑,一共来探讨吧!