I code it

Code and Life

测试驱动开发为什么很重要?

为了对代码的修改内容进行追踪,防止对代码的误删除等操作,我在本地搭建了一个svn的服务器,并创建一个working-copy来进行开发。但是有一次,我不小心将一个working-copy删除了,并且糟糕的是,这个working-copy中有很多未进入版本库的内容。当我再次checkout之后,很多程序都编译不了了。

在和历史版本对比中发现,我对其中的一些代码进行了重构,将部分代码分离出来作为独立的模块。而在svn的HEAD版本中,仅保留了对独立出来模块的访问路径。于是我只能将之前做过的重构工作再做一次。在重做的过程中发现,如果我保留了当时的测试用例,那么一切都会很轻松。

我正在开发一个基于wxStyledEdit的小型编辑器,其中有一段负责加载外部的api文件,并生成calltip列表的代码。重构时,我将其移动到了一个HelperUtil模块中,这个模块并没有被添加到svn中,我只能靠手工来再做一次:

首先,我从编辑器代码中找到了这个类及其实例的所有引用:

   1: class MyEditor(wxStyledEditor):
   2:     def __init__(self, ...):
   3:         ...
   4:         self.helper = HelperUtil.CommonHelper([])
   5:         ...
   6:         
   7:  
   8:     def initUI(self, ...):
   9:         keywords = self.helper.GetKeywords()
  10:         ...
  11:         self.SetKeyWords(0, " ".join(keywords))
  12:         ...
  13:  
  14:     def tryShowCallTip(self, currentPos, word):
  15:         #print word, fdmap[word]
  16:         fdmap = self.helper.GetFunctionMap()
  17:         if word in fdmap.keys():
  18:             self.CallTipShow(currentPos, fdmap[word])
  19:         else:
  20:             return
  21:             
  22:             
  23:     def someMethod(self, ...):
  24:         keywords = self.helper.GetKeywords()
  25:         udkeys = self.helper.GetUserKeywords()

根据这些信息,我可以知道HelperUtil模块中有一个CommonHelper的类,初始化时需要传递一个list给它,然后这个类的实例中有以下几个方法:

  • GetKeywords(), 返回一个list
  • GetFunctionMap(),返回一个dict
  • GetUserKeywords(),返回一个list

这样我很容易从历史版本中找出对应的代码,并将其复原:

   1: # -*- coding: utf-8 -*-
   2:  
   3: class CommonHelper(object):
   4:     # function declareation map
   5:     fdmap = {}
   6:     # user-define keywords
   7:     udkeys = []
   8:     #pre-defined keywords
   9:     keywords = []
  10:  
  11:     def __init__(self, ext):
  12:         self.ext = ext
  13:         self.loadKeywords()
  14:         self.loadTips()
  15:  
  16:     def loadFileToList(self, file, list):
  17:         f = open(file, "rb")
  18:         if not f:
  19:             return
  20:         line = f.readline()
  21:         while line:
  22:             list.append(line.strip())
  23:             line = f.readline()
  24:  
  25:         f.close()
  26:  
  27:     def loadKeywords(self):
  28:         self.loadFileToList("extra\c.kw", self.keywords)
  29:         self.loadFileToList("extra\oracle.kw", self.keywords)
  30:         self.keywords.sort()
  31:  
  32:     def loadTips(self):
  33:         f = open("extra\idp.api", "rb")
  34:         if not f:
  35:             return
  36:  
  37:         line = f.readline()
  38:         while line:
  39:             pos = line.find('(')
  40:             
  41:             if pos == -1:
  42:                 self.udkeys.append(line)
  43:                 line = f.readline()
  44:                 continue
  45:             else:
  46:                 key = line[:pos]
  47:                 value = line
  48:                 self.fdmap[key] = line
  49:                 self.udkeys.append(key)
  50:             line = f.readline()
  51:  
  52:         f.close()
  53:         return
  54:  
  55:     def GetKeywords(self):
  56:         return self.keywords
  57:  
  58:     def GetFunctionMap(self):
  59:         return self.fdmap
  60:  
  61:     def GetUserKeywords(self):
  62:         return self.udkeys
  63:  
  64: if __name__ == "__main__":
  65:     helper = CommonHelper([])
  66:     print helper.GetKeywords()
  67:     print helper.GetFunctionMap()
  68:     print helper.GetUserKeywords()
  69:  

当然,这个例子非常小,引用到Helper的地方也不多,很容易通过阅读代码来还原,但是如果Helper被多个地方引用,而代码量又非常大的话,可能就需要更多的时间来操作,并且很容易出错。如果有针对每个功能/模块的测试用例,那么这个工作就非常平坦了。

测试驱动开发的好处远不至此:

  • 测试本身就是示例
  • 测试本身就是与代码高度一致的文档
  • 测试帮助我们快速实现需要的功能,而不是所有的功能
  • 测试帮助我们降低模块间的耦合度
  • 测试使得重构代码时,不会有后顾之忧

Comments