研究:Python Mock Object教程

原文连接:http://www.xper.org/wiki/seminar/PythonMockObjectTutorial

===============

源文件可以在mockobject.py处下载(它使用到了PyUnit模块)。

在开始之前,我想先和大家确定一点:本教程不是Mock Objects的介绍;它只是一个如何在Python 中使用MockObject^模块的简短介绍。在这里我们会看到一些Mock Objects的基本用法。要了解Mock Objects更多的信息,可以参见:

第一篇文章是任何想使用Mock Objects的人的必读(囧——译者注)。

JuNe

假定你和你所在的团队在建立一个KMS。那么应该有一个数据库保存了所有的文档。团队决定创建一个函数来删除所有一年前发布的文章。根据文档名删除文档,以及取得所有文档名和发布日期的功能是已经有了的——由接口完成——但还没有实现,数据库也没有建立。你被分配来开发“删除老文档”的功 能,但它取决于数据库组件。你是不是应该等下去,直到别人完成了你的模块所依赖的所有功能后才进行开发呢?

让我们使用Mock Objects。 第一个版本将是这样的:

import unittest
def removeOldDocs(aDb):
    pass
class RemoveOldDocs(unittest.TestCase):
    def testRemoveNoneFromEmptyDb(self):
        removeOldDocs(db)
if __name__=='__main__':
    unittest.main(argv=('','-v'))

此时,我们想测试的情况是:如果数据库中没有任何文档,那么也就没有任何文档会被删除——这可能是最简单的情况了。这里有两重的问题。他们还没有实现数据库,而我们如何能测试removeOldDocs表现正常呢?让我们用一个mock object来进行“伪装”。

import unittest,mockobject
class MockDb(mockobject.MockObject):
    pass
def removeOldDocs(aDb):
    pass
class RemoveOldDocs(unittest.TestCase):
    def testRemoveNoneFromEmptyDb(self):
        db=MockDb()
        removeOldDocs(db)

现在正常了,但是测试代码并不完整。我们需要测试removeOldDocs的行为。在测试用例中增加以下两行:

class RemoveOldDocs(unittest.TestCase):
    def testRemoveNoneFromEmptyDb(self):
        db=MockDb()
        db._setExpectedRemoveCalls(0)
        removeOldDocs(db)
        db._verify()

当然,此时会出错——因为我们还没有定义_setExpectedRemovedCalls。还好,计算机告诉我们接下来做什么:定义这个方法。 我们在新定义的方法中要做些什么呢?我们应该设置删除调用应该进行的次数。因此,我们在MockDb__init__中创建一个Expectation对象。

class MockDb(mockobject.MockObject):
    def __init__(self):
        self._removeCalls=mockobject.ExpectationCounter(MockDb.removeCalls)
    def _setExpectedRemoveCalls(self,aCount):
        self._removeCalls.setExpected(aCount)

现在我们运行测试代码,代码测试通过。现在,让我们做一些有意义的事情。

    def testRemoveOne(self):
        db=MockDb()
        db._addDoc(doc1,1992/3/1)
        db._setExpectedRemoveCalls(1)
        removeOldDocs(db)
        db._verify()

等一下。当前时间的信息在哪里?我们是不是也可以“伪装”它?是的。你可以替换time模块(或函数)而改用MockTime类。但是,这不是一个很好的想法。为什么不用参数将它进行传递呢?哈,我们需要修改代码,以及测试用例。让我们暂时注释掉刚加入的测试用例,让所有的测试都得以通过。然后修改测试代码。

class RemoveOldDocs(unittest.TestCase):
    def testRemoveNoneFromEmptyDb(self):
        db=MockDb()
        db._setExpectedRemoveCalls(0)
        removeOldDocs(db,2002/3/1)
        db._verify()
##    def testRemoveOne(self):
##        db=MockDb()
##        db._addDoc(doc1,1992/3/1)
##        db._setExpectedRemoveCalls(1)
##        removeOldDocs(db)
##        db._verify()

如我们所愿,测试出错了。让我们使之通过:增加一个参数。

def removeOldDocs(aDb,aDate):
    pass

现在回到刚才被注释掉的测试用例。函数调用中增加一个参数并运行。这回,计算机需要定义一个新的方法。

class MockDb(mockobject.MockObject):
......
    def _addDoc(self,aName,aDate):
        pass

现在又出错了。需要调用一次删除操作,但是一次也没有调用。

class MockDb(mockobject.MockObject):
    def getAllDocumentDates(self):
        return ()
def removeOldDocs(aDb,aDate):
    for eachDoc,eachDate in aDb.getAllDocumentDates():
        if isOlderThanBy(eachDate,aDate,365):
            aDb.remove(eachDoc)

假定isOlderThanBy已经实现了。这行的意思是:如果eachDate早于aDate超过365天。

现在我们创建一个新的属性以便我们在mock db中保存测试用的文档。

class MockDb(mockobject.MockObject):
    def __init__(self):
        self._removeCalls=mockobject.ExpectationCounter(MockDb.removeCalls)
        self._docs={}
    def _setExpectedRemoveCalls(self,aCount):
        self._removeCalls.setExpected(aCount)
    def _addDoc(self,aName,aDate):
        self._docs[aName]=aDate
    def getAllDocumentDates(self):
        return self._docs.items()

是时候在mock db中加入remove方法了。

class MockDb(mockobject.MockObject):
    def remove(self,aName):
        self._removeCalls.inc()

现在所有都可以执行通过。我们可以再增加一个测试用例。

    def testRemoveSome(self):
        db=MockDb()
        db._addDoc(doc1,1992/2/15)
        db._addDoc(doc2,1992/3/15)
        db._addDoc(doc3,1992/4/15)
        db._setExpectedRemoveCalls(2)
        removeOldDocs(db,1993/4/1)
        db._verify()

不需要修改代码,该用例应该可以通过。

如果你想确定某些特定文档被删除,你可以使用ExpectationSet。例如:

class MockDb(mockobject.MockObject):
    def __init__(self):
        self._removeCalls=mockobject.ExpectationCounter(MockDb.removeCalls)
        self._removedDocs=mockobject.ExpectationSet(MockDb.removedDocs)
        self._docs={}
    def _addExpectedRemovedDoc(self,aName):
        self._removedDocs.addExpected(aName)
    def remove(self,aName):
        self._removeCalls.inc()
        self._removedDocs.addActual(aName)
        #you may add del self._docs[aName] here if you'll use getAllDocumentDates again
class RemoveOldDocs(unittest.TestCase):
    def testRemoveSome(self):
        db=MockDb()
        db._addDoc(doc1,1992/2/15)
        db._addDoc(doc2,1992/3/15)
        db._addDoc(doc3,1992/4/15)
        db._setExpectedRemoveCalls(2)
        db._addExpectedRemovedDoc(doc1)
        db._addExpectedRemovedDoc(doc2)
        removeOldDocs(db,1993/4/1)
        db._verify()

有很多不同类型的Expectation类,如ExpectationCounterExpectationListExpectationSet以及ExpectationMap等。

尽管我们在测试用例中创建了mock objects,但是使用setUp方法等将它们重构出去会更好。而且我们有很大的几率消除代码中的重复。代码会更短、更清晰,只要我们严格遵循OAOO。但可惜的是,本教程就会更加的冗长了。

Comments

3 responses to “研究:Python Mock Object教程”

  1. 令狐虫 Avatar

    很好,很强大

  2. simp Avatar
    simp

    emm.. that is close to my current project.. Mock XXX… 🙂

Leave a Reply to 令狐虫 Cancel reply

Your email address will not be published. Required fields are marked *