使用 Python Mock 类进行单元测试

Data type, model, or node -- these are just some of the roles a mock object might assume. But how does a mock fit inside a unit test setup?

Sometimes, you need "other" code resources for your test setup. But those resources may be unavailable, unstable, or just too unwieldy to use. You could try and find a replacement for the missing resource; or you could simulate it by creating what is known as a mock. Mocks let us simulate resources that are either unavailable or too unwieldy for unit testing.

Creating a mock in Python is done through the Mock module. You can build a mock one-attribute-at-a-time or wholesale with a dictionary object or a class interface. You can also define how a mock behaves and check its usage during a test. Let's explore.

Sample code presented here is written in Python 2.4, unless noted otherwise.

译者信息

数据类型、模型或节点——这些都只是mock对象可承担的角色。但mock在单元测试中扮演一个什么角色呢?

有时,你需要为单元测试的初始设置准备一些“其他”的代码资源。但这些资源兴许会不可用,不稳定,或者是使用起来太笨重。你可以试着找一些其他的资源替代;或者你可以通过创建一个被称为mock的东西来模拟它。Mocks能够让我们模拟那些在单元测试中不可用或太笨重的资源。

在Python中创建mock是通过Mock模块完成的。你可以通过每次一个属性(one-attribute-at-a-time)或一个健全的字典对象或是一个类接口来创建mock。你还可以定义mock的行为并且在测试过程中检查它的使用。让我们继续探讨。

除非单独说明,下面的示例代码都是用Python2.4写的。

The Test Setup

The typical test setup has at least two parts. First, there is the test subject (red), the focus of the test. It can be a method, module, or class. It may or may not return a result, but it can raise an error or exception depending on input data or internal state  (Figure 1).

Python
Figure 1.

Second is the test case (grey), which runs alone or as part of a suite. It prepares the test subject, as well as any data or resources needed by the subject. It runs one or more test routines and checks how the test subject behaves in each test. And it collects the test results and presents them in a concise, readable format.

Now, some test subjects need one or more resources (green) in order to function. These resources may be another class or module, or even an independent process. Whatever their nature, test resources are functional code. Their role is to support the test subject, but they are not the focus of the test.

译者信息

测试准备

典型的测试准备最少有两个部分。首先是测试对象(红色),这是测试的关注点。它可以是一个方法、模块或者类。它可以返回一个结果,也可以不返回结果,但是它可以根据数据数据或者内部状态产生错误或者异常(图1)。

Python

图1

第二测试用例(灰色),它可以单独运行也可以作为套件的一部分。它是为测试对象准备的,也可以是测试对象需要的任意数据或资源。运行一个或多个测试事务,在每个测试中检查测试对象的行为。收集测试结果并用一个简洁、易读的格式呈现测试结果。

现在,为了发挥作用,一些测试对象需要一个或多个资源(绿色)。这些资源可以是其他的类或者模块,甚至是一个非独立的进程。不论其性质,测试资源是功能性的代码。他们的角色是支持测试对象,但是他们不是测试的关注点。

Reasons to Mock

But there are times when a test resource is either unavailable or unsuitable. Perhaps the resource is being developed in parallel with the test subject. It may then be incomplete or too unstable to be reliable.

The resource may be too costly. If the resource is a third-party product, its high price tag can disqualify its use for testing. Setting up the resource might be complex, taking up hardware and time that could be used elsewhere. If the resource is a data source, setting up its data set, one that mimics real-world content, can get tedious.

The resource may simply be unpredictable. A good unit test has to be repeatable, allowing you to isolate and identify a fault. But the test resource might give out stochastic results, or it might have widely varying response times. And as a result, you end up glossing over a potential showstopper.

译者信息

使用Mock的理由

但是有些时候,测试资源不可用,或者不适合。也许这个资源正在和测试对象并行开发中,或者并不完整或者是太不稳定以至于不可靠。

测试资源太昂贵,如果测试资源是第三方的产品,其高昂的价格不适用于测试。测试资源的建立过于复杂,占用的硬件和时间可以用于别的地方。如果测试资源是一个数据源,建立它的数据集模仿真实世界是乏味的。

测试资源是不可预知的。一个好的单元测试是可重复的,允许你分离和识别故障。但是测试资源可能给出随机的结果,或者它会有不同的响应时间。而作为这样的结果,测试资源最终可能成为一个潜在的搅局者。

These are some of the reasons why you might want to use a mock in place of a test resource. A mock can present the same set of method interfaces as the resource to the test subject. But a mock is easier to setup, easier to manage. It can present the test subject the same method interfaces as the actual test resource. It can deliver deterministic results, and it can be customize to suit a particular test. And it can be easily updated to reflect changes in the actual resource.

Of course, mocks are not without issues. Designing an accurate mock is difficult, especially if you have no reliable information on the test resource. You can try and find an open-source match, or you can make a guesstimate on the resource's method interface. Whichever you choose, you can update the mock easily later on, should you get more detailed information on the preferred resource.

Too many mocks can complicate a test, making it harder for you to track down a failure. Best practice is to limit a test case to one or two mocks, or use separate test cases for each mock/subject pairing.

译者信息

 这些都是你可能想要用mock代替测试资源的原因。mock向测试对象提供一套和测试资源相同的方法接口。但是mock是更容易创建和管理。它能向测试对象提供和真实的测试资源相同的方法接口。它能提供确定的结果,并可以自定义以适用于特定的测试。能够容易的更新,以反映实际资源的变化。

当然,mocks不是没有问题的。设计一个精确的mock是困难的,特别是如果你没有测试资源的可靠信息。你可以尝试找到一个开源的接口,或者你能对测试资源的方法接口进行猜测。无论你如何选择,你都可以在以后轻松的更新mock,你可以在首选资源中得到更详细的信息。

太多的mock会使测试过于复杂,让你跟踪错误变得更困难。最好的实践是每个测试用例限制使用一到两个mock,或者为每个mock/对象对使用独立的测试用例。

Mocks versus Stubs versus Fakes

A mock is not the only way to mimic a test resource. Solutions like a stub or a fake are just as capable of the same service. So, how does a mock compare against these two? Why choose a mock over a stub or a fake?

Consider the stub: A stub presents a set of method interfaces to the test subject, the same set the subject would see in an actual test resource. When the test subject calls a stub method, the stub may respond with a predetermined set of results. It may raise an error or exception, also predetermined. A stub may track its interactions with the test subject, but it performs no tasks outside the narrow scope of its programming.

A fake also presents a set of method interfaces and tracks its interactions with the test subject. But unlike a stub, a fake actually processes input data from the test subject and produces results based on that data. In short, a fake is a functional, but non-production version of the actual test resource. It lacks the checks and balances found in resource, it uses simpler algorithms, and it seldom, if ever, stores or transports data.

With fakes and stubs, you can test if the test subject called the right method with the right input. You can test how the subject handles the result and how it reacts to an error or exception. These tests are known as state verification. But what if you want to know if the test subject called the same method twice? What if you want to know if it called several methods in the proper order? Such tests are known as behavior verification, and to do them, you need mocks.

译者信息

Mocks对Stubs对Fakes

Mock不是模仿测试资源的唯一方式。其他的解决方案如stub和fake也能提供相同的服务。因此,mock和其他两种解决方案怎样比较?为什么选择mock而不是选择stub或者fake?

认识stub:stub为测试对象提供了一套方法接口,和真实的测试资源提供给测试对象的接口是相同的。当测试对象调用stub方法时,stub响应预定的结果。也可以产生一个预定的错误或者异常。stub可以跟踪和测试对象的交互,但是它不处理输入的数据。

fake也提供了一套方法接口并且也可以跟踪和测试对象的交互。但是和stub不同,fake真正的处理了从测试对象输入的数据产生的结果是基于这些数据的。简而言之,fake是功能性的,它是真实测试资源的非生产版。它缺乏资源的相互制衡,使用了更简单的算法,而且它很少存储和传输数据。

使用fake和stub,你可以输入正确的数据调用了正确的方法对测试对象进行测试。你能测试对象是如何处理数据并产生结果,当出现错误或者异常时是怎样反应的。这些测试被称为状态验证。但是你是否想知道测试对象调用了两次相同的方法?你是否想知道测试对象是否按照正确的顺序调用了几个方法?这种测试被称为行为验证,而要做到这些,你需要mocks。

To Mock with Python

The Mock module is what you use to create and manage a mock object on Python. This module is the brainchild of Michael Foord, and it is a standard module on Python 3.0. For Python 2.4 to 2.7, however, you have to install the module yourself. You can get the latest version of the Mock module (1.0.1) from the Python Package Index website.

The Mock module provides a handful classes on which to base your mock object. There is even a patch mechanism for altering the mock on the fly. But for now, our focus is on one class: the Mock class.

Figure 2 shows the basic structure of the Mock class (green). It derives from two parent classes: NonCallableMock and CallableMixin (grey).NonCallableMock defines the routines needed by the mock object. It overrides several magic methods, giving them default behaviors. And it supplies the assert routines for tracking the mock's behavior. As forCallableMixin, it updates those magic methods that make a mock object callable. In turn, both parents derive from the Base class (red), which declares the properties needed by the mock object.

使用 Python Mock 类进行单元测试_第1张图片
Figure 2.

译者信息

使用Python Mock

在Python中Mock模块是用来创建和管理mock对象的。该模块是Michael Foord的心血结晶,它是Python3.0的标准模块。因此在Python2.4~2.7中,你不得不自己安装这个模块。你可以 Python Package Index website从获得Mock模块最新的版本。

基于你的mock对象,Mock模块提供了少量的类。为了改变运行中的mock甚至提供了补丁机制。但是现在,我们关注一个类:Mock类。

图2中显示了Mock类(绿色)的基本结构。它继承于两个父类:NonCallableMock和CallableMixin(灰色)。NonCallableMock定义了mock对象所需的例程。它重载了几个魔法方法,给他们定义了缺省的行为。为了跟踪mock的行为,它提供了断言例程。CallableMixin更新了mock对象回调的魔法方法。反过来,两个父类继承于Base类(红色),声明了mock对象所需的属性。

使用 Python Mock 类进行单元测试_第2张图片

图2

Preparing to Mock

The Mock class has four sets of methods (Figure 3). In the first set is the class constructor, which takes up to six optional and labeled arguments. The diagram shows the four arguments you will use most often.

使用 Python Mock 类进行单元测试_第3张图片
Figure 3.

The first constructor argument is the name argument, which gives a mock object  a unique name. Listing One shows how I create a mock object (mockFoo) with the name Foo. Notice the name appears next to the mock's unique ID when I print the mock object (lines 6-9).

Listing One

from mock import Mock

# create the mock object
mockFoo = Mock(name = "Foo")

print mockFoo
# returns: 
print repr(mockFoo)
# still returns: 

The next constructor argument is the spec argument. It sets the mock object's attributes — which may be a property or a method. The attributes can be a list of strings or  can come from another Python class.

To demonstrate, in Listing Two, I have a list object (fooSpec) with three items (line 4): the property attribute _fooValue, and the method attributes callFooand doFoo. When I pass fooSpec to the class constructor (line 7), mockFoogains three attributes, which I can access with the dot operator (lines 10-15). But if I access an undeclared attribute, mockFoo raises an AttributeErrorand names the "faulty" attribute (lines 21-24).

Listing Two

from mock import Mock

# prepare the spec list
fooSpec = ["_fooValue", "callFoo", "doFoo"]

# create the mock object
mockFoo = Mock(spec = fooSpec)

# accessing the mocked attributes
print mockFoo
# 
print mockFoo._fooValue
# returns 
print mockFoo.callFoo()
# returns: 

mockFoo.callFoo()
# nothing happens, which is fine

# accessing the missing attributes
print mockFoo._fooBar
# raises: AttributeError: Mock object has no attribute '_fooBar'
mockFoo.callFoobar()
# raises: AttributeError: Mock object has no attribute 'callFoobar'
译者信息

准备Mock

Mock类有四套方法(图3)。第一套方法是类的构造器,它有六个可选和已标记的参数。图中显示了4个经常用到的参数。

使用 Python Mock 类进行单元测试_第4张图片

图3

构造器的第一个参数是name,它定义了mock对象的唯一标示符。Listing one显示了怎么创建一个标示符为Foo的mock对象mockFoo。请注意当我打印mock对象(6-9行)时,标示符后紧跟的是mock对象的唯一ID。

Listing One

from mock import Mock

# create the mock object
mockFoo = Mock(name = "Foo")

print mockFoo
# returns: 
print repr(mockFoo)
# still returns: 

构造器的第二个参数是spec。它设置mock对象的属性,可以是property或者方法。属性可以是一个列表字符串或者是其他的Python类。

为了演示,在Listing Two中,我有一个带三个项目的列表对象fooSpec(第4行):property属性_fooValue,方法属性callFoo和doFoo。当我把fooSpec带入类构造器时(第7行),mockFoo获得了三个属性,我能用点操作符访问它们(10~15行)。当我访问了一个未声明的属性时,mockFoo引发AttributeError和"faulty"属性(21~14行)。

Listing Two

from mock import Mock

# prepare the spec list
fooSpec = ["_fooValue", "callFoo", "doFoo"]

# create the mock object
mockFoo = Mock(spec = fooSpec)

# accessing the mocked attributes
print mockFoo
# 
print mockFoo._fooValue
# returns 
print mockFoo.callFoo()
# returns: 

mockFoo.callFoo()
# nothing happens, which is fine

# accessing the missing attributes
print mockFoo._fooBar
# raises: AttributeError: Mock object has no attribute '_fooBar'
mockFoo.callFoobar()
# raises: AttributeError: Mock object has no attribute 'callFoobar'

Listing Three shows another use of the spec argument. This time, I have classFoo with the same three attributes (lines 4-12). I pass the class name to the constructor (line 15), which then produces a mock object with the same attributes as Foo (lines 18-23). Again, accessing an undeclared attribute raises an AttributeError (lines 29-32). Also, in both examples, the method attributes are non-functional. Calling a mocked method does nothing, even if said method has functional code (lines 25-26).

Listing Three

from mock import Mock

# The class interfaces
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue    

# create the mock object
mockFoo = Mock(spec = Foo)

# accessing the mocked attributes
print mockFoo
# returns 
print mockFoo._fooValue
# returns 
print mockFoo.callFoo()
# returns: 

mockFoo.callFoo()
# nothing happens, which is fine

# accessing the missing attributes
print mockFoo._fooBar
# raises: AttributeError: Mock object has no attribute '_fooBar'
mockFoo.callFoobar()
# raises: AttributeError: Mock object has no attribute 'callFoobar'

The next constructor argument is return_value. This one sets a mock object's response when it gets a direct call. I use this argument for simulating a factory call.

To demonstrate, in Listing Four, I have return_value set to 456 (line 4). When I call mockFoo, I get 456 for a result (line 9-11). In Listing Five, I pass an instance of class Foo (fooObj) for return_value (lines 15-19). Now, when I call mockFoo, I get fooObj instead (shown here as mockObj) (line 24). And unlike in Listings Two and Three, the methods in mockObj are functional.

Listing Four

from mock import Mock

# create the mock object
mockFoo = Mock(return_value = 456)

print mockFoo
# 

mockObj = mockFoo()
print mockObj
# returns: 456

Listing Five

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# creating the mock object
fooObj = Foo()
print fooObj
# returns: <__main__.Foo object at 0x68550>

mockFoo = Mock(return_value = fooObj)
print mockFoo
# returns: 

# creating an "instance"
mockObj = mockFoo()
print mockObj
# returns: <__main__.Foo object at 0x68550>

# working with the mocked instance
print mockObj._fooValue
# returns: 123
mockObj.callFoo()
# returns: Foo:callFoo_
mockObj.doFoo("narf")
# returns: Foo:doFoo:input =  narf

Counter to return_value is the side_effect argument. This one assigns the mock with an alternative result, one that overrides return_value. In short, a simulated factory call returns the side_effect value, not the return_value.

Listing Six demonstrates the effects of the side_effect argument. First, I create an instance of class Foo (fooObj) and pass it as a return_valueargument (line 17). The result is similar to Listing Five; mockFoo returnsfooObj when called (line 22). But then I repeat the same steps, passingStandardError for the side_effect argument (line 28). Now, calling mockFooraises StandardError, instead of returning fooObj (lines 29-30).

译者信息

Listing Three显示了spec参数的另一种用法。这次,有带三个属性的类Foo(4~12行)。把类名传入构造器中,这样就产生了一个和Foo有相同属性的mock对象(18~23行)。再次,访问一个未声明的属性引发了AttributeError(29~32行)。也就是说,在两个例子中,方法属性时没有功能的。甚至在方法有功能代码时,调用mock的方法也什么都不做。

Listing Three

from mock import Mock

# The class interfaces
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue    

# create the mock object
mockFoo = Mock(spec = Foo)

# accessing the mocked attributes
print mockFoo
# returns 
print mockFoo._fooValue
# returns 
print mockFoo.callFoo()
# returns: 

mockFoo.callFoo()
# nothing happens, which is fine

# accessing the missing attributes
print mockFoo._fooBar
# raises: AttributeError: Mock object has no attribute '_fooBar'
mockFoo.callFoobar()
# raises: AttributeError: Mock object has no attribute 'callFoobar'

下一个构造器参数是return_value。这将设置mock对象的响应当它被直接调用的时候。我用这个参数模拟一个工厂调用。 

为了演示,在Listing Four中,设置return_value为456(第4行)。当调用mockFoo时,将返回456的结果(9~11行)。在Listing Five中,我给return_value传入了一个类Foo的实例fooObj(15~19行)。现在,当我调用mockFoo时,我获得了fooObj的实例(显示为mockObj)(第24行)。和Listing Two和Three不一样,mockObj的方法是带有功能的。

Listing Four

from mock import Mock

# create the mock object
mockFoo = Mock(return_value = 456)

print mockFoo
# 

mockObj = mockFoo()
print mockObj
# returns: 456

Listing Five

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# creating the mock object
fooObj = Foo()
print fooObj
# returns: <__main__.Foo object at 0x68550>

mockFoo = Mock(return_value = fooObj)
print mockFoo
# returns: 

# creating an "instance"
mockObj = mockFoo()
print mockObj
# returns: <__main__.Foo object at 0x68550>

# working with the mocked instance
print mockObj._fooValue
# returns: 123
mockObj.callFoo()
# returns: Foo:callFoo_
mockObj.doFoo("narf")
# returns: Foo:doFoo:input =  narf

side_effect参数和return_value是相反的。它给mock分配了可替换的结果,覆盖了return_value。简单的说,一个模拟工厂调用将返回side_effect值,而不是return_value。 

Listing Six演示了side_effect参数的影响。首先,创建类Foo的实例fooObj,把它传入return_value参数(第17行)。这个结果和Listing Five是类似的。当它被调用的时候,mockFoo返回fooObj(第22行)。然后我重复同样的步骤,给side_effect参数传入StandardError(第28行),现在,调用mockFoo引发了StandardError,不再返回fooObj(29~30行)。

Listing Six

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# creating the mock object (without a side effect)
fooObj = Foo()

mockFoo = Mock(return_value = fooObj)
print mockFoo
# returns: 

# creating an "instance"
mockObj = mockFoo()
print mockObj
# returns: <__main__.Foo object at 0x2a88f0>

# creating a mock object (with a side effect)

mockFoo = Mock(return_value = fooObj, side_effect = StandardError)
mockObj = mockFoo()
# raises: StandardError

Listing Seven shows another effect. In this one, I pass a list object (fooList) to the class constructor (lines 17-18). Then, each time I call mockFoo, it returns a list item in succession (lines 20-30). Once mockFoo reaches the end of the list, another call will raise a StopIteration error (lines 32-34).

Listing Seven

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# creating the mock object (with a side effect)
fooObj = FooSpec()

fooList = [665, 666, 667]
mockFoo = Mock(return_value = fooObj, side_effect = fooList)

fooTest = mockFoo()
print fooTest
# returns 665

fooTest = mockFoo()
print fooTest
# returns 666

fooTest = mockFoo()
print fooTest
# returns 667

fooTest = mockFoo()
print fooTest
# raises: StopIteration

You could pass other iterable objects (set, tuple) to the side_effectargument. What you cannot pass is a primitive (integer, string, and so on) because these are not iterable. To make a primitive iterable, add it to a single-item list.

译者信息

Listing Six

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# creating the mock object (without a side effect)
fooObj = Foo()

mockFoo = Mock(return_value = fooObj)
print mockFoo
# returns: 

# creating an "instance"
mockObj = mockFoo()
print mockObj
# returns: <__main__.Foo object at 0x2a88f0>

# creating a mock object (with a side effect)

mockFoo = Mock(return_value = fooObj, side_effect = StandardError)
mockObj = mockFoo()
# raises: StandardError

Listing Seven显示了另一个影响。在这个例子中,传入一个列表对象fooList到类构造器中(17~18行)。然后,每次我调用mockFoo时,它连续的返回列表中的项(20~30行)。一旦mockFoo到达了列表的末尾,调用将引发StopIteration 错误(32~34行)

Listing Seven

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# creating the mock object (with a side effect)
fooObj = FooSpec()

fooList = [665, 666, 667]
mockFoo = Mock(return_value = fooObj, side_effect = fooList)

fooTest = mockFoo()
print fooTest
# returns 665

fooTest = mockFoo()
print fooTest
# returns 666

fooTest = mockFoo()
print fooTest
# returns 667

fooTest = mockFoo()
print fooTest
# raises: StopIteration

你可以传入其他的可迭代对象(集合,元组)到side_effct对象中。你不能传入一个简单对象(如整数、字符串等),因为这些对象是不能迭代的,为了让简单对象可迭代,需要将他们加入单一项的列表中。

Asserting with a Mock

The next set of methods from the Mock class are the asserts. These help track the method calls made to the mock by the test subject. They can work in conjunction with the asserts from the unittest module. They can be attached to the mock or to one of its method attributes. All but one take the same two optional arguments: a variable sequence, and a key/value sequence.

The first assert, assert_called_with(), checks if a mocked method gets the right arguments. It fires when at least one argument has the wrong value or type, when there is a wrong number of arguments, when the arguments are in the wrong order, or when the mocked method is not expecting any arguments at all. Listing Eight shows how you might use this assert. Here, I prepared a mock object with class Foo as its spec. I call the mocked methoddoFoo(), passing a string for input. With assert_called_with(), I check if the method gets the right input. The assert in line 20 passes becausedoFoo() got "narf" for input. But the assert in line 24 fails because doFoo()got "zort", which is wrong.

Listing Eight

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.doFoo("narf")
mockFoo.doFoo.assert_called_with("narf")
# assertion passes

mockFoo.doFoo("zort")
mockFoo.doFoo.assert_called_with("narf")
# AssertionError: Expected call: doFoo('narf')
# Actual call: doFoo('zort')
译者信息

Mock断言

Mock类的下一套方法是断言。它将帮助跟踪测试对象对mock方法的调用。他们能和unittest模块的断言一起工作。能连接到mock或者其方法属性之一。 有两个相同的可选参数:一个变量序列,一个键/值序列。

第一个断言assert_called_with(),检查mock方法是否获得了正确的参数。当至少一个参数有错误的值或者类型时,当参数的数量错误时,当参数的顺序错误时,或者当mock的方法根本不存在任何参数时,这个断言将引发错误。Listing Eight显示了可以怎样使用这个断言。那儿,我准备了一个mock对象,用类Foo作为它的spec参数。我调用了类的方法doFoo(),传入了一个字符串作为输入。使用assert_called_with(),我检查方法是否获得了正确的输入。第20行的断言通过了,因为doFoo()获得了"narf"的输入。但是在第24行的断言失败了因为doFoo()获得了"zort",这是错误的输入。

Listing Eight

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.doFoo("narf")
mockFoo.doFoo.assert_called_with("narf")
# assertion passes

mockFoo.doFoo("zort")
mockFoo.doFoo.assert_called_with("narf")
# AssertionError: Expected call: doFoo('narf')
# Actual call: doFoo('zort')

Listing Nine shows a slightly different use. In this one, I call the mocked method callFoo(), first without input, and then with the string "zort". The first assert passes (line 20), because callFoo() is not supposed to get any input. And the second assert fails (line 24) for obvious reasons.

Listing Nine

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.callFoo()
mockFoo.callFoo.assert_called_with()
# assertion passes

mockFoo.callFoo("zort")
mockFoo.callFoo.assert_called_with()
# AssertionError: Expected call: callFoo()
# Actual call: callFoo('zort')

The next assert is assert_called_once_with(). Like assert_called_with(), this assert checks if the test subject called a mocked method correctly. Butassert_called_once_with() will fire when the same method call happens more than once, whereas assert_called_with() will ignore multiple calls. Listing Ten shows how one might use the assert. Here, I make two calls to the mocked method callFoo(). On the first call (lines 19-20), the assert passes. But on the second call (lines 23-24), the assert fires, sending its error message to stdout.

Listing Ten

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.callFoo()
mockFoo.callFoo.assert_called_once_with()
# assertion passes

mockFoo.callFoo()
mockFoo.callFoo.assert_called_once_with()
# AssertionError: Expected to be called once. Called 2 times.

The assert assert_any_call() checks if the test subject called a mocked method at any point of the test routine. This is regardless of how many other calls were made between the mocked method and the assert. Compare this with the previous two asserts, both of which check only the most recent call.

Listing Eleven shows the assert_any_call() assert at work: still the same mock object, with class Foo as its spec. The first call is to the methodcallFoo() (line 18), the next two to doFoo() (lines 19-20). Notice thatdoFoo() gets two different inputs.

译者信息

Listing Nine显示了稍微不同的用法。在这个例子中,我调用了mock方法callFoo(),首先没有任何输入,然后输入了字符串“zort”。第一个断言通过了(第20行),因为callFoo()不支持获得任何输入。而第二个断言失败了(第24行)因为显而易见的原因。

Listing Nine

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.callFoo()
mockFoo.callFoo.assert_called_with()
# assertion passes

mockFoo.callFoo("zort")
mockFoo.callFoo.assert_called_with()
# AssertionError: Expected call: callFoo()
# Actual call: callFoo('zort')

先一个断言是assert_called_once_with()。像assert_called_with()一样,这个断言检查测试对象是否正确的调用了mock方法。但是当同样的方法调用超过一次时, assert_called_once_with()将引发错误,然而assert_called_with()会忽略多次调用。Listing Ten显示了怎样使用这个断言。那儿,我调用了mock方法callFoo()两次。第一次调用时(行19~20),断言通过。但是在第二次调用的时(行23~24),断言失败,发送了错误消息到stdout。

Listing Ten

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.callFoo()
mockFoo.callFoo.assert_called_once_with()
# assertion passes

mockFoo.callFoo()
mockFoo.callFoo.assert_called_once_with()
# AssertionError: Expected to be called once. Called 2 times.

断言assert_any_call(),检查测试对象在测试例程中是否调用了测试方法。它不管mock方法和断言之间有多少其他的调用。和前面两个断言相比较,前两个断言仅检查最近一次的调用。

Listing Eleven显示了assert_any_call()断言如何工作:仍然是同样的mock对象,spec参数是Foo类。第一个调用方法callFoo()(第18行),接下来调用两次doFoo()(行19~20)。注意doFoo()获得了两个不同的输入。

Listing Eleven



mockFoo.callFoo()
mockFoo.doFoo("narf")
mockFoo.doFoo("zort")

mockFoo.callFoo.assert_any_call()
# assert passes

mockFoo.callFoo()
mockFoo.doFoo("troz")

mockFoo.doFoo.assert_any_call("zort")
# assert passes

mockFoo.doFoo.assert_any_call("egad")
# raises: AssertionError: doFoo('egad') call not found

The first assert_any_call() (line 22) passes even though two doFoo() calls separate the assert and callFoo(). The second assert (line 28) also passes even though a callFoo() separates it from the doFoo() in question (line 20). On the other hand, the third assert (line 31) fires, because none of thedoFoo() calls used the string "egad" for input.

Finally, there is assert_has_calls(). This one looks at a sequence of method calls, checks if they are in the right order and with the right arguments. It takes two arguments: a list of expected method calls and an optional argument any_order. It fires when the test subject calls the wrong method, calls one method out of order, or gives a method the wrong input.

译者信息

Listing Eleven



mockFoo.callFoo()
mockFoo.doFoo("narf")
mockFoo.doFoo("zort")

mockFoo.callFoo.assert_any_call()
# assert passes

mockFoo.callFoo()
mockFoo.doFoo("troz")

mockFoo.doFoo.assert_any_call("zort")
# assert passes

mockFoo.doFoo.assert_any_call("egad")
# raises: AssertionError: doFoo('egad') call not found

第一个assert_any_call()(第22行)通过,虽然两次doFoo()调用隔开了断言和callFoo()。第二个断言(第28行)也通过了,虽然一个callFoo()隔开了我们提到的doFoo()(第20行)。另一方面,第三个断言(第31行)失败了,因为没有任何doFoo()的调用使用了"egad"的输入。

最后,还有assert_has_calls()。它查看方法调用的顺序,检查他们是否按正确的次序调用并带有正确的参数。它带有两个参数:期望调用方法的列表和一个可选悬殊any_order。当测试对象调用了错误的方法,调用了不在次序中的方法,或者方法获得了一个错误的输入,将生产断言错误。

Listing Twelve demonstrates the assert_has_calls() assert. In lines 18-20, I make three method calls, providing input to two. Then, I prepare a list of expected calls (fooCalls) and pass this list to assert_has_calls() (lines 22-23). Since the list matches the method calls, the assert passes.

Listing Twelve

from mock import Mock, call

# The mock specification
class Foo(object):
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.callFoo()
mockFoo.doFoo("narf")
mockFoo.doFoo("zort")

fooCalls = [call.callFoo(), call.doFoo("narf"), call.doFoo("zort")]
mockFoo.assert_has_calls(fooCalls)
# assert passes

fooCalls = [call.callFoo(), call.doFoo("zort"), call.doFoo("narf")]
mockFoo.assert_has_calls(fooCalls)
# AssertionError: Calls not found.
# Expected: [call.callFoo(), call.doFoo('zort'), call.doFoo('narf')]
# Actual: [call.callFoo(), call.doFoo('narf'), call.doFoo('zort')]

fooCalls = [call.callFoo(), call.doFoo("zort"), call.doFoo("narf")]
mockFoo.assert_has_calls(fooCalls, any_order = True)
# assert passes

In line 26, I swapped the two doFoo() calls around. The first doFoo() gets  "zort" for input, the second gets "narf". If I pass this fooCalls toassert_has_calls() (line 27), the assert fires. But if I pass a True to theany_order argument, the assert passes. This is because the assert now ignores the order in which the method calls were made.

译者信息

Listing Twelve演示了assert_has_calls()断言。在18~20行,我调用了三个方法,提供了两个输入。然后,我准备了一个期望调用的列表(fooCalls)并把这个列表传入assert_has_calls()(22~23行)。由于列表匹配了方法的调用,断言通过。

Listing Twelve

from mock import Mock, call

# The mock specification
class Foo(object):
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.callFoo()
mockFoo.doFoo("narf")
mockFoo.doFoo("zort")

fooCalls = [call.callFoo(), call.doFoo("narf"), call.doFoo("zort")]
mockFoo.assert_has_calls(fooCalls)
# assert passes

fooCalls = [call.callFoo(), call.doFoo("zort"), call.doFoo("narf")]
mockFoo.assert_has_calls(fooCalls)
# AssertionError: Calls not found.
# Expected: [call.callFoo(), call.doFoo('zort'), call.doFoo('narf')]
# Actual: [call.callFoo(), call.doFoo('narf'), call.doFoo('zort')]

fooCalls = [call.callFoo(), call.doFoo("zort"), call.doFoo("narf")]
mockFoo.assert_has_calls(fooCalls, any_order = True)
# assert passes

在第26行,我交换了两个doFoo()调用的顺序。第一个doFoo()获得"zort"的输入,第二个获得了"narf"。如果我传入这个fooCalls到assert_has_calls()(第27行)中,断言失败。但是如果我给参数any_order传入参数True,断言通过。这是因为断言将忽略方法调用的顺序。

Listing Thirteen demonstrates another use. To the list fooCalls, I added a nonexistent method dooFoo() (line 22). Then I passed fooCalls toassert_has_calls() (line 24). The assert fires, informing me that the expected call sequence did not match what actually happened. If I pass aTrue to the any_order argument (line 30), the assert names dooFoo() as the offending method call.

Listing Thirteen

from mock import Mock, call

# The mock specification
class Foo(object):
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.callFoo()
mockFoo.doFoo("narf")
mockFoo.doFoo("zort")

fooCalls = [call.callFoo(), call.dooFoo("narf"), call.doFoo("zort")]

mockFoo.assert_has_calls(fooCalls)
# AssertionError: Calls not found.
# Expected: [call.callFoo(), call.dooFoo('narf'), call.doFoo('zort')]
# Actual: [call.callFoo(), call.doFoo('narf'), call.doFoo('zort')]

fooCalls = [call.callFoo(), call.dooFoo("narf"), call.doFoo("zort")]
mockFoo.assert_has_calls(fooCalls, any_order = True)
# AssertionError: (call.dooFoo('narf'),) not all found in call list

In both examples for assert_has_calls(), note the call keyword that appears before each method name. This keyword is for a helper object, one that marks out a method attribute in the mock object. To use the callkeyword, make sure to import the helper from the mock module as follows:

from mock import Mock, call
译者信息

Listing Thirteen演示了其他的用法。在fooCalls列表中,我添加了不存在的方法dooFoo()(第22行)。然后我传入fooCalls到assert_has_calls()中(第24行)。断言失败,通知我期望调用的顺序和真实发生的顺序不匹配。如果我给any_order赋值为True(第30行),断言名称dooFoo()作为违规的方法调用。

Listing Thirteen

from mock import Mock, call

# The mock specification
class Foo(object):
    _fooValue = 123
    
    def callFoo(self):
        pass
    
    def doFoo(self, argValue):
        pass

# create the mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

mockFoo.callFoo()
mockFoo.doFoo("narf")
mockFoo.doFoo("zort")

fooCalls = [call.callFoo(), call.dooFoo("narf"), call.doFoo("zort")]

mockFoo.assert_has_calls(fooCalls)
# AssertionError: Calls not found.
# Expected: [call.callFoo(), call.dooFoo('narf'), call.doFoo('zort')]
# Actual: [call.callFoo(), call.doFoo('narf'), call.doFoo('zort')]

fooCalls = [call.callFoo(), call.dooFoo("narf"), call.doFoo("zort")]
mockFoo.assert_has_calls(fooCalls, any_order = True)
# AssertionError: (call.dooFoo('narf'),) not all found in call list

在assert_has_calls()的两个例子中,注意到关键字call是出现在每个方法的前面。这个关键字是一个helper对象,标记出mock对象的方法属性。为了使用call关键字,请确保使用如下的方法从mocke模块导入helper: 

from mock import Mock, call

Managing a Mock

A third set of methods from the Mock class allow you to control and manage your mock object. You can change how the mock behaves, alter some of its attributes, or restore the mock to its pre-test state. You can even change the response values for each mocked method or for the mock itself.

The method attach_mock() lets you add a second mock object to your mock. This method takes two arguments: the second mock object (aMock) and an attribute name (aName).

Listing Fourteen demonstrates how this is done. Here, I create two mock objects, mockFoo and mockBar, each one with a different spec (lines 25, 30). To mockFoo, I add mockBar using  attach_mock() and  the name "fooBar" (line 35). Once that is done, I can access the second mock and its attributes via the property fooBar (lines 46-53). And I can still access the attributes for the first mock, mockFoo (lines 40-43).

Listing Fourteen

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

class Bar(object):
    # instance properties
    _barValue = 456
    
    def callBar(self):
        pass
    
    def doBar(self, argValue):
        pass

# create the first mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

# create the second mock object
mockBar = Mock(spec = Bar)
print mockBar
# returns: 

# attach the second mock to the first
mockFoo.attach_mock(mockBar, 'fooBar')

# access the first mock's attributes
print mockFoo
# returns: 
print mockFoo._fooValue
# returns: 
print mockFoo.callFoo()
# returns: 

# access the second mock and its attributes
print mockFoo.fooBar
# returns: 
print mockFoo.fooBar._barValue
# returns: 
print mockFoo.fooBar.callBar()
# returns: 
print mockFoo.fooBar.doBar("narf")
# returns: 

The method configure_mock() lets you make wholesale changes to the mock object. Its sole argument is a sequence of key/value pairs, each key being the attribute you want changed. If the mock does not have the specified attribute, configure_mock() will add the attribute to the mock.

译者信息

管理Mock

Mock类的第三套方法允许你控制和管理mock对象。你可以更改mock的行为,改变它的属性或者将mock恢复到测试前的状态。你甚至可以更改每个mock方法或者mock本身的响应值。attach_mock()方法让你在mock中添加第二个mock对象。这个方法带有两个参数:第二个mock对象(aMock)和一个属性名称(aName)。

Listing Fourteen 样式了attach_mock()方法的使用。那儿,我创建了两个mock对象mockFoo和mockBar,他们有不同spec参数(第25行和第30行)。我用attach_mock()方法将mockBar添加到mockFoo中,命名为fooBar(第35行)。一旦添加成功,我就能通过property fooBar访问第二mock对象和它的属性(46~53行)。并且我仍然可以访问第一个mock对象mockFoo的属性。

Listing Fourteen

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

class Bar(object):
    # instance properties
    _barValue = 456
    
    def callBar(self):
        pass
    
    def doBar(self, argValue):
        pass

# create the first mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

# create the second mock object
mockBar = Mock(spec = Bar)
print mockBar
# returns: 

# attach the second mock to the first
mockFoo.attach_mock(mockBar, 'fooBar')

# access the first mock's attributes
print mockFoo
# returns: 
print mockFoo._fooValue
# returns: 
print mockFoo.callFoo()
# returns: 

# access the second mock and its attributes
print mockFoo.fooBar
# returns: 
print mockFoo.fooBar._barValue
# returns: 
print mockFoo.fooBar.callBar()
# returns: 
print mockFoo.fooBar.doBar("narf")
# returns: 

configure_mock()方法让你批量的更改mock对象。它唯一的参数是一个键值对序列,每个键就是你想要修改的属性。如果你的对象没有指定的属性,configure_mock()将在mock中添加属性。 

Listing Fifteen shows the configure_mock() method in action. Once again, I have a mock object (mockFoo) with class Foo for a spec and 555 for areturn_value (line 13). Then with configure_mock(), I changed thereturn_value property to 999 (line 17). When I call mockFoo directly, I get 999for a result, instead of the original 555.

Listing Fifteen

from mock import Mock

class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

mockFoo = Mock(spec = Foo, return_value = 555)
print mockFoo()
# returns: 555

mockFoo.configure_mock(return_value = 999)
print mockFoo()
# returns: 999

fooSpec = {'callFoo.return_value':"narf", 'doFoo.return_value':"zort", 'doFoo.side_effect':StandardError}
mockFoo.configure_mock(**fooSpec)

print mockFoo.callFoo()
# returns: narf
print mockFoo.doFoo("narf")
# raises: StandardError

fooSpec = {'doFoo.side_effect':None}
mockFoo.configure_mock(**fooSpec)
print mockFoo.doFoo("narf")
# returns: zort

Next, I prepare a dictionary object (fooSpec) into which I set the return values for two mocked methods and the side effect for doFoo() (line 21). I pass fooSpec into configure_mock(), taking care to prefix fooSpec with '**'(line 22). Invoking callFoo() now returns "narf" as a result; and invokingdoFoo(), regardless of input, raises a StandardError signal (lines 24-27). If I alter fooSpec, setting the side-effect value for doFoo() to None, I get a result of "zort" when invoking doFoo() (lines 29-32).

译者信息

Listing fifteen显示了configure_mock()方法的运用。再次,我定义了一个spec为类Foo和return_value为555的mock对象mockFoo(第13行)。然后使用configure_mock()方法更改return_value为999(第17行)。当我直接调用mockFoo时,获得的结果为999,替换了原来的555。

Listing Fifteen

from mock import Mock

class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

mockFoo = Mock(spec = Foo, return_value = 555)
print mockFoo()
# returns: 555

mockFoo.configure_mock(return_value = 999)
print mockFoo()
# returns: 999

fooSpec = {'callFoo.return_value':"narf", 'doFoo.return_value':"zort", 'doFoo.side_effect':StandardError}
mockFoo.configure_mock(**fooSpec)

print mockFoo.callFoo()
# returns: narf
print mockFoo.doFoo("narf")
# raises: StandardError

fooSpec = {'doFoo.side_effect':None}
mockFoo.configure_mock(**fooSpec)
print mockFoo.doFoo("narf")
# returns: zort

接着,我准备了一个字段对象(fooSpec),对两个mock方法设置了返回值,为doFoo()设置了side_effect(第21行)。我将fooSpec传入configure_mock(),注意fooSpec带有前缀'**'(第22行)。现在调用callFoo()结果返回“narf”。调用doFoo(),无论输入什么,引发StandardError 信号(行24~27)。如果我修改了fooSpec,设置doFoo()的side_effect的值为None,当我调用doFoo()时,将得到结果“zort”(29~32行)。

The next method, mock_add_spec(), lets you add new attributes to the mock object. Its function is similar to the constructor argument spec, exceptmock_add_spec() works on an existing object, and it "erases" those attributes set by the constructor. The method takes two arguments: the attribute spec (aSpec) and a spec_set flag (aFlag). Again, the spec may be a list of strings or it may be a class. The added attributes are read-only by default, but passing a True to the spec_set flag make those same attributes writable.

Listing Sixteen demonstrates mock_add_spec() in action. The mock object (mockFoo) starts with attributes coming from class Foo (line 25). When I access two of the attributes (_fooValue and callFoo()), I get a result confirming their presence (lines 29-32)

Listing Sixteen

from mock import Mock

# The class interfaces
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

class Bar(object):
    # instance properties
    _barValue = 456
    
    def callBar(self):
        pass
    
    def doBar(self, argValue):
        pass
    
# create the mock object
mockFoo = Mock(spec = Foo)

print mockFoo
# returns 
print mockFoo._fooValue
# returns 
print mockFoo.callFoo()
# returns: 

# add a new spec attributes
mockFoo.mock_add_spec(Bar)

print mockFoo
# returns: 
print mockFoo._barValue
# returns: 
print mockFoo.callBar()
# returns: 

print mockFoo._fooValue
# raises: AttributeError: Mock object has no attribute '_fooValue'
print mockFoo.callFoo()
# raises: AttributeError: Mock object has no attribute 'callFoo'

Then, I use mock_add_spec() to add class Bar to mockFoo (line 35). The mock object now assumes the attributes declared in class Bar (lines 39-42). If I access any Foo attribute, the mock object raises an AttributeError to signal their absence (lines 44-47).

译者信息

下一个方法mock_add_spec()让你向mock对象添加新的属性。除了mock_add_spec()工作在一个已存在的对象上之外,它的功能类似于构造器的spec参数。它擦除了一些构造器设置的属性。这个方法带有两个参数:spec属性(aSpec)和spc_set标志(aFlag)。再次,spce可以是字符串列表或者是类。已添加的属性缺省状态是只读的,但是通过设置spec_set标志为True,可以让属性可写。

Listing Sixteen演示了mock_add_spec()的运用。mock对象mockFoo开始的属性来自于类Foo(第25行)。当我访问两个属性(_fooValue和callFoo())时,我得到结果确认他们是存在的(29~32行)。

Listing Sixteen

from mock import Mock

# The class interfaces
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

class Bar(object):
    # instance properties
    _barValue = 456
    
    def callBar(self):
        pass
    
    def doBar(self, argValue):
        pass
    
# create the mock object
mockFoo = Mock(spec = Foo)

print mockFoo
# returns 
print mockFoo._fooValue
# returns 
print mockFoo.callFoo()
# returns: 

# add a new spec attributes
mockFoo.mock_add_spec(Bar)

print mockFoo
# returns: 
print mockFoo._barValue
# returns: 
print mockFoo.callBar()
# returns: 

print mockFoo._fooValue
# raises: AttributeError: Mock object has no attribute '_fooValue'
print mockFoo.callFoo()
# raises: AttributeError: Mock object has no attribute 'callFoo'

然后,我使用mock_add_spec()方法添加类Bar到mockFoo(第35行)。mock对象现在的属性已声明在类Bar中(39~42行)。如果我访问任何Foo属性,mock对象将引发AttributeError 信号,表示他们不存在(44~47行)。

The last method, resetMock(), puts the mock object back to its pre-test state. It clears the mock's call statistics and asserts. It does not clear thereturn_value and side_effect properties for both mock and its method attributes. Do this to reuse the mock and avoid the overhead of creating another mock.

Finally, you can assign a return value or side-effect to each method attribute. This you do through the accessors return_value and side_effect. For example, to make the method callFoo() return a value of "narf", use thereturn_value accessor as follows:

mockFoo.callFoo.return_value = "narf"

To give callFoo() the side-effect of TypeError, use the side_effectaccessor as follows:

mockFoo.callFoo.side_effect = TypeError

To clear the side-effect, pass None to the accessor:

mockFoo.callFoo.side_effect = None

You can also use the same two accessors to change how the mock object responds to a factory call.

译者信息

最后一个方法resetMock(),恢复mock对象到测试前的状态。它清除了mock对象的调用统计和断言。它不会清除mock对象的return_value和side_effect属性和它的方法属性。这样做是为了重新使用mock对象避免重新创建mock的开销。

最后,你能给每个方法属性分配返回值或者side-effect。你能通过return_value和side_effect访问器做到这些。例如,按如下的语句通过return_value访问器设置方法callFoo()的返回值为"narf":

mockFoo.callFoo.return_value = "narf"

按如下的语句通过side_effect访问器 设置方法callFoo()的side-ffect为TypeError

mockFoo.callFoo.side_effect = TypeError

传入None清除side-effect

mockFoo.callFoo.side_effect = None

你也可以用这个两个相同的访问器改变mock对象对工厂调用的响应值。 

Statistics with a Mock

The last set of methods consists of accessors that track any calls made to a mock object. The accessor called returns a True when the mock gets a factory call, False  otherwise. Consider the code in Listing Seventeen. After I create mockFoo, the called accessor returns a False result (lines 19-20). If I do a factory call, it returns a True result (lines 22-23). But what if I create a second mock object, then invoke a mocked method (callFoo(), line 30)? In that case, the called accessor will only give a False result (lines 31-32).

Listing Seventeen

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

print mockFoo.called
# returns: False

mockFoo()
print mockFoo.called
# returns: True

mockFoo = Mock(spec = Foo)
print mockFoo.called
# returns: False

mockFoo.callFoo()
print mockFoo.called
# returns: False

The accessor call_count gives the number of times a mock object gets a factory call. Consider the code in Listing Eighteen. After I create mockFoo,call_count gives the expected result of 0 (lines 19-20). When I make a factory call to mockFoocall_count increases by 1 (lines 22-24). When I invoke the mocked method callFoo()call_count remains unchanged (lines 26-28). If I do a second factory call, call_count should increase by 1 more.

Listing Eighteen

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

print mockFoo.call_count
# returns: 0

mockFoo()
print mockFoo.call_count
# returns: 1

mockFoo.callFoo()
print mockFoo.call_count
# returns: 1
译者信息

Mock统计

最后一套方法包含跟踪mock对象所做的任意调用的访问器。当mock对象获得工厂调用时,访问器called返回True,否则返回False。查看Listing Seventeen中的代码,我创建了mockFoo之后,called访问器返回了结果False(19~20行)。如果我做了一个工厂调用,它将返回结果True(22~23行)。但是如果我创建了第二个mock对象,然后调用了mock方法callFoo()(第30行)?在这个例子中,called访问器仅仅放回了False结果(31~32行)。

Listing Seventeen

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

print mockFoo.called
# returns: False

mockFoo()
print mockFoo.called
# returns: True

mockFoo = Mock(spec = Foo)
print mockFoo.called
# returns: False

mockFoo.callFoo()
print mockFoo.called
# returns: False

访问器call_count给出了mock对象被工厂调用的次数。查看Listing Eighteen中的代码。我创建mockFoo之后,call_count给出的期望结果为0(19~20行)。当我对mockFoo做了一个工厂调用时,call_count增加1(22~24行)。当我调用mock方法callFoo()时,call_count没有改变(26~28行)。如果我做了第二次工厂调用call_count将再增加1。

Listing Eighteen

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo)
print mockFoo
# returns 

print mockFoo.call_count
# returns: 0

mockFoo()
print mockFoo.call_count
# returns: 1

mockFoo.callFoo()
print mockFoo.call_count
# returns: 1

The accessor call_args returns the arguments used in a factory call. Listing Nineteen demonstrates its action. For a newly created mock object (mockFoo), the call_args accessor gives a result of None (lines 17-21). If I make a factory call, passing "zort" for input, call_args reports it as"call('zort')" (line 23-25). Note the call keyword in the result. For a second factory call, without input, call_args returns "call()" (lines 27-29). A third factory call, with "troz" for input, gives the result "call('troz')"from call_args (lines 31-33). But when I invoke the mocked methodcallFoo(), the call_args accessor still returns "call('troz')" (lines 35-37).

Listing Nineteen

#!/usr/bin/python

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo, return_value = "narf")
print mockFoo
# returns 
print mockFoo.call_args
# returns: None

mockFoo("zort")
print mockFoo.call_args
# returns: call('zort')

mockFoo()
print mockFoo.call_args
# returns: call()

mockFoo("troz")
print mockFoo.call_args
# returns: call('troz')

mockFoo.callFoo()
print mockFoo.call_args
# returns: call('troz')

The accessor call_args_list also reports the arguments used in a factory call. But while call_args returns the most recent arguments,call_args_list returns a list, with the first item being the earliest argument. Listing Twenty shows the accessor in action, using the same code in Listing Nineteen.

译者信息

访问器call_args返回工厂调用已用的参数。Listing Nineteen演示了它的运用。对于新创建的mock对象(mockFoo),call_args访问器返回结果为None(17~21行)。如果我做了一个工厂调用,在输入中传入"zort",call_args报告的结果为call('zort')(23~25行)。注意结果中的call关键字。对于第二个没有输入的工厂调用,call_args返回call()(27~29行)。第三个工厂调用,输入“troz”,call_args给出结果为call('troz')(31~33行)。但是当我调用mock方法callFoo()时,call_args访问器仍然返回call('troz')(35~37行)。

Listing Nineteen

#!/usr/bin/python

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo, return_value = "narf")
print mockFoo
# returns 
print mockFoo.call_args
# returns: None

mockFoo("zort")
print mockFoo.call_args
# returns: call('zort')

mockFoo()
print mockFoo.call_args
# returns: call()

mockFoo("troz")
print mockFoo.call_args
# returns: call('troz')

mockFoo.callFoo()
print mockFoo.call_args
# returns: call('troz')

访问器call_args_list 也报告了工厂调用中已使用的参数。但是call_args返回最近使用的参数,而call_args_list返回一个列表,第一项为最早的参数。Listing Twenty显示了这个访问的的运用,使用了和Listing Nineteen相同的代码。


Listing Twenty

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo, return_value = "narf")
print mockFoo
# returns 

mockFoo("zort")
print mockFoo.call_args_list
# returns: [call('zort')]

mockFoo()
print mockFoo.call_args_list
# returns: [call('zort'), call()]

mockFoo("troz")
print mockFoo.call_args_list
# returns: [call('zort'), call(), call('troz')]

mockFoo.callFoo()
print mockFoo.call_args_list
# returns: [call('zort'), call(), call('troz')]

The accessor method_calls reports the mocked method calls made by the test subject. Its result is a list object, each item showing the method name and its arguments.

Listing Twenty-one demonstrates method_calls in action. With a newly created mockFoomethod_calls returns an empty list (lines 15-19). The same also happens when I do a factory call (lines 21-23). When I invoke the mocked method callFoo()method_calls returns a list object with one entry (lines 25-27). When I invoke doFoo(), passing "narf" for input, method_callsreturns a list with two items (lines 29-31). Notice how each method name appears in the order of its invocation.

Listing Twenty-one

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo, return_value = "poink")
print mockFoo
# returns 
print mockFoo.method_calls
# returns []

mockFoo()
print mockFoo.method_calls
# returns []

mockFoo.callFoo()
print mockFoo.method_calls
# returns: [call.callFoo()]

mockFoo.doFoo("narf")
print mockFoo.method_calls
# returns: [call.callFoo(), call.doFoo('narf')]

mockFoo()
print mockFoo.method_calls
# returns: [call.callFoo(), call.doFoo('narf')]

The last accessor mock_calls reports all calls made by the test subject to the mock object. The result is again a list, but now showing both factory and method calls. Listing Twenty-two demonstrates the accessor in action, using the same code in Listing Twenty-one.

Listing Twenty-two

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo, return_value = "poink")
print mockFoo
# returns 

print mockFoo.mock_calls
# returns []

mockFoo()
print mockFoo.mock_calls
# returns [call()]

mockFoo.callFoo()>
print mockFoo.mock_calls
# returns: [call(), call.callFoo()]

mockFoo.doFoo("narf")
print mockFoo.mock_calls
# returns: [call(), call.callFoo(), call.doFoo('narf')]

mockFoo()
print mockFoo.mock_calls
# returns: [call(), call.callFoo(), call.doFoo('narf'), call()]
译者信息

Listing Twenty

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo, return_value = "narf")
print mockFoo
# returns 

mockFoo("zort")
print mockFoo.call_args_list
# returns: [call('zort')]

mockFoo()
print mockFoo.call_args_list
# returns: [call('zort'), call()]

mockFoo("troz")
print mockFoo.call_args_list
# returns: [call('zort'), call(), call('troz')]

mockFoo.callFoo()
print mockFoo.call_args_list
# returns: [call('zort'), call(), call('troz')]

访问器mothod_calls报告了测试对象所做的mock方法的调用。它的结果是一个列表对象,每一项显示了方法的名称和它的参数。

Listing Twenty-one演示了method_calls的运用。对新创建的mockFoo,method_calls返回了空列表(15~19行)。当做了工厂调用时,同样返回空列表(21~23行)。当我调用了mock方法callFoo()时,method_calls返回一个带一项数据的列表对象(25~27行)。当我调用doFoo(),并传入"narf"参数时,method_calls返回带有两项数据的列表(29~31行)。注意每个方法名称是按照它调用的顺序显示的。

Listing Twenty-one

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo, return_value = "poink")
print mockFoo
# returns 
print mockFoo.method_calls
# returns []

mockFoo()
print mockFoo.method_calls
# returns []

mockFoo.callFoo()
print mockFoo.method_calls
# returns: [call.callFoo()]

mockFoo.doFoo("narf")
print mockFoo.method_calls
# returns: [call.callFoo(), call.doFoo('narf')]

mockFoo()
print mockFoo.method_calls
# returns: [call.callFoo(), call.doFoo('narf')]

最后一个访问器mock_calls报告了测试对象对mock对象所有的调用。结果是一个列表,但是工厂调用和方法调用都显示了。Listing Twenty-two演示这个访问器的运用,使用了和Listing Twenty-one相同的代码

Listing Twenty-two

from mock import Mock

# The mock object
class Foo(object):
    # instance properties
    _fooValue = 123
    
    def callFoo(self):
        print "Foo:callFoo_"
    
    def doFoo(self, argValue):
        print "Foo:doFoo:input = ", argValue

# create the first mock object
mockFoo = Mock(spec = Foo, return_value = "poink")
print mockFoo
# returns 

print mockFoo.mock_calls
# returns []

mockFoo()
print mockFoo.mock_calls
# returns [call()]

mockFoo.callFoo()>
print mockFoo.mock_calls
# returns: [call(), call.callFoo()]

mockFoo.doFoo("narf")
print mockFoo.mock_calls
# returns: [call(), call.callFoo(), call.doFoo('narf')]

mockFoo()
print mockFoo.mock_calls
# returns: [call(), call.callFoo(), call.doFoo('narf'), call()]

Testing with a Mock

Data type, model, or node — these are just some of the roles a mock object might assume. But how does a mock fit inside a unit test setup? Let us take a look, using a simplified setup taken from the Martin Fowler articleMocks Aren't Stubs.

In the test setup are three classes (Figure 4). The Order class is the test subject. It models a single item purchase order, which it fills from a data source. The Warehouse class is the test resource. It contains a sequence of key/value pairs, the key being the item name, the value being the available quantity. And the OrderTest class is the test case itself.

使用 Python Mock 类进行单元测试_第5张图片
Figure 4.

Listing Twenty-three describes the Order class. The class declares three properties: the item name (_orderItem), the requested quantity (_orderAmount) and the filled quantity (_orderFilled). Its constructor takes two arguments (lines 8-18), which it uses to populate the properties_orderItem and _orderAmount. Its __repr__() method returns a summary of the purchase order (lines 21-24).

Listing Twenty-three

class Order(object):
    # instance properties
    _orderItem = "None"
    _orderAmount = 0
    _orderFilled = -1
    
    # Constructor
    def __init__(self, argItem, argAmount):
        print "Order:__init__"
        
        # set the order item
        if (isinstance(argItem, str)):
            if (len(argItem) > 0):
                self._orderItem = argItem
        
        # set the order amount
        if (argAmount > 0):
            self._orderAmount = argAmount
        
    # Magic methods
    def __repr__(self):
       # assemble the dictionary
        locOrder = {'item':self._orderItem, 'amount':self._orderAmount}
        return repr(locOrder)
    
    # Instance methods
    # attempt to fill the order
    def fill(self, argSrc):
        print "Order:fill_"
        
        try:
            # does the warehouse has the item in stock?
            if (argSrc is not None):
                if (argSrc.hasInventory(self._orderItem)):
                    # get the item
                    locCount =    argSrc.getInventory(self._orderItem, self._orderAmount)
                
                    # update the following property
                    self._orderFilled = locCount
                else:
                    print "Inventory item not available"
            else:
                print "Warehouse not available"
        except TypeError:
            print "Invalid warehouse"
    
    # check if the order has been filled
    def isFilled(self):
        print "Order:isFilled_"
        return (self._orderAmount == self._orderFilled)

The Order class defines two instance methods. The fill() method gets a data source (argSrc) for an argument. It checks if the source is valid and if it has the item in question (line 33-34). It submits a withdrawal request and updates _orderFilled with the actual quantity withdrawn (lines 36-39). TheisFilled() method returns a True when both _orderAmount and_orderFilled have the same values (lines 48-50).

译者信息

在测试中使用MOCK

数据类型,模型或者节点,这些是mock对象可能被假定的一些角色。但是mock对象怎样适合单元测试呢?让我们一起来看看,来自Martin Fowler的文章Mocks Aren't Stubs采取了简化的设置。

在这个测试中,设置了三个类(图4)。Order类是测试对象。它模拟了单一项目的采购订单,订单来源于一个数据源。Warehouse类是测试资源。它包含了键值对的序列,键是项目的名称,值是可用的数量。OrderTest类是测试用例本身。

使用 Python Mock 类进行单元测试_第6张图片

图4

Listing Twenty-three描述了Order。Order类声明了三个属性:项目名称(_orderItem),要求的数量(_orderAmount)和已填写的数量(_orderFilled)。它的构造器带有两个参数(8~18行),填入的属性是_orderItem和_orderAmount。它的__repr__()方法返回了购买清单的摘要(21~24行)。

Listing Twenty-three

class Order(object):
    # instance properties
    _orderItem = "None"
    _orderAmount = 0
    _orderFilled = -1
    
    # Constructor
    def __init__(self, argItem, argAmount):
        print "Order:__init__"
        
        # set the order item
        if (isinstance(argItem, str)):
            if (len(argItem) > 0):
                self._orderItem = argItem
        
        # set the order amount
        if (argAmount > 0):
            self._orderAmount = argAmount
        
    # Magic methods
    def __repr__(self):
       # assemble the dictionary
        locOrder = {'item':self._orderItem, 'amount':self._orderAmount}
        return repr(locOrder)
    
    # Instance methods
    # attempt to fill the order
    def fill(self, argSrc):
        print "Order:fill_"
        
        try:
            # does the warehouse has the item in stock?
            if (argSrc is not None):
                if (argSrc.hasInventory(self._orderItem)):
                    # get the item
                    locCount =    argSrc.getInventory(self._orderItem, self._orderAmount)
                
                    # update the following property
                    self._orderFilled = locCount
                else:
                    print "Inventory item not available"
            else:
                print "Warehouse not available"
        except TypeError:
            print "Invalid warehouse"
    
    # check if the order has been filled
    def isFilled(self):
        print "Order:isFilled_"
        return (self._orderAmount == self._orderFilled)

Order类定义了两个实例方法。fill()方法从参数(argSrc)中获取数据源。它检查数据源是否可用,数据源的项目是否存在问题(33~34行)。它提交了一个申请并用实际返回的数量更新_orderFilled(36~39行)。当_orderAmount和_orderFilled有相同的值时,isFilled()方法返回True(48~50行)。

Listing Twenty-four describes the Warehouse class. It is an abstract class, declaring properties and method interfaces, but not defining the methods themselves. The property _houseName holds the warehouse's name, while_houseList holds its inventory. Accessors exists for these two properties.

Listing Twenty-four

class Warehouse(object):    
    # private properties
    _houseName = None
    _houseList = None
        
    # accessors
    def warehouseName(self):
        return (self._houseName)
    
    def inventory(self):
        return (self._houseList)
    
    
    # -- INVENTORY ACTIONS
    # set up the warehouse
    def setup(self, argName, argList):
    	pass
    
    # check for an inventory item
    def hasInventory(self, argItem):
        pass
    
    # retrieve an inventory item
    def getInventory(self, argItem, argCount):
        pass
        
    # add an inventory item
    def addInventory(self, argItem, argCount):
        pass

The class declares four method interfaces. The method setup() takes two arguments, which are meant to update the two properties. The methodhasInventory() gets an item name and returns a True if said item is in the inventory. The method getInventory() gets an item name and quantity. It tries to deduct that quantity from the inventory, and returns what was deducted successfully. The method addInventory() also gets an item name and quantity. It is suppose to update _houseList with those two arguments.

In Listing Twenty-five is the test case itself, OrderTest. Its one property,fooSource, holds the mock object needed by the Order class. The setUp()method identifies the test routine to be executed (lines 14-16), then creates and configures the mock object (lines 21-34). And the tearDown() method prints an empty line to stdout.

Listing Twenty-five

import unittest
from mock import Mock, call

class OrderTest(unittest.TestCase):
    # declare the test resource
    fooSource = None
    
    # preparing to test
    def setUp(self):
        """ Setting up for the test """
        print "OrderTest:setUp_:begin"
        
        # identify the test routine
        testName = self.id().split(".")
        testName = testName[2]
        print testName
        
        # prepare and configure the test resource
        if (testName == "testA_newOrder"):
            print "OrderTest:setup_:testA_newOrder:RESERVED"
        elif (testName == "testB_nilInventory"):
            self.fooSource = Mock(spec = Warehouse, return_value = None)
        elif (testName == "testC_orderCheck"):
            self.fooSource = Mock(spec = Warehouse)
            self.fooSource.hasInventory.return_value = True
            self.fooSource.getInventory.return_value = 0
        elif (testName == "testD_orderFilled"):
            self.fooSource = Mock(spec = Warehouse)
            self.fooSource.hasInventory.return_value = True
            self.fooSource.getInventory.return_value = 10
        elif (testName == "testE_orderIncomplete"):
            self.fooSource = Mock(spec = Warehouse)
            self.fooSource.hasInventory.return_value = True
            self.fooSource.getInventory.return_value = 5
        else:
            print "UNSUPPORTED TEST ROUTINE"
    
    # ending the test
    def tearDown(self):
        """Cleaning up after the test"""
        print "OrderTest:tearDown_:begin"
        print ""
    
    # test: new order
    # objective: creating an order
    def testA_newOrder(self):
        # creating a new order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # test for a nil object
        self.assertIsNotNone(testOrder, "Order object is a nil.")
        
        # test for a valid item name
        testName = testOrder._orderItem
        self.assertEqual(testName, "mushrooms", "Invalid item name")
        
        # test for a valid item amount
        testAmount = testOrder._orderAmount
        self.assertGreater(testAmount, 0, "Invalid item amount")
    
    # test: nil inventory
    # objective: how the order object handles a nil inventory
    def testB_nilInventory(self):
        """Test routine B"""
        # creating a new order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # fill the order
        testSource = self.fooSource()
        testOrder.fill(testSource)
        
        # print the mocked calls
        print self.fooSource.mock_calls
        
        # check the call history
        testCalls = [call()]
        self.fooSource.assert_has_calls(testCalls)
    
    # ... continued in the next listing

The OrderTest class has five test routines. All five start by creating an instance of the Order class. The routine testA_newOrder() tests if the Orderobject is valid and if it holds the right data (lines 46-60). The routinetestB_nilWarehouse() creates a null mock and passes that to the Orderobject's fill() method (lines 64-79). It checks the mock's call history, making sure only a factory call has occurred.

译者信息

Listing Twenty-four描述了Warehouse类。它是一个抽象类,声明了属性和方法接口,但是没有定义方法本身。属性_houseName是仓库的名字,而_houseList是它持有的库存。还有这两个属性的访问器。

Listing Twenty-four

class Warehouse(object):    
    # private properties
    _houseName = None
    _houseList = None
        
    # accessors
    def warehouseName(self):
        return (self._houseName)
    
    def inventory(self):
        return (self._houseList)
    
    
    # -- INVENTORY ACTIONS
    # set up the warehouse
    def setup(self, argName, argList):
    	pass
    
    # check for an inventory item
    def hasInventory(self, argItem):
        pass
    
    # retrieve an inventory item
    def getInventory(self, argItem, argCount):
        pass
        
    # add an inventory item
    def addInventory(self, argItem, argCount):
        pass

Warehouse类声明了四个方法接口。方法setup()带有两个参数,是为了更新这两个属性。方法hasInventory()参数是项目的名称,如果项目在库存中则返回True。方法getInventory()的参数是项目的名称和数量。它尝试着从库存中扣除数量,返回哪些是成功的扣除。方法addInventory()的参数也是项目名称和数量。它将用这两个参数更新_houseList。

Listing Twenty-five是测试用例本身,orderTest类。他有一个属性fooSource是Order类所需的mock对象。setUp()方法识别执行的测试例程(14~16行),然后创建和配置mock对象(21~34行)。tearDown()方法向stdout打印一个空行。

Listing Twenty-five

import unittest
from mock import Mock, call

class OrderTest(unittest.TestCase):
    # declare the test resource
    fooSource = None
    
    # preparing to test
    def setUp(self):
        """ Setting up for the test """
        print "OrderTest:setUp_:begin"
        
        # identify the test routine
        testName = self.id().split(".")
        testName = testName[2]
        print testName
        
        # prepare and configure the test resource
        if (testName == "testA_newOrder"):
            print "OrderTest:setup_:testA_newOrder:RESERVED"
        elif (testName == "testB_nilInventory"):
            self.fooSource = Mock(spec = Warehouse, return_value = None)
        elif (testName == "testC_orderCheck"):
            self.fooSource = Mock(spec = Warehouse)
            self.fooSource.hasInventory.return_value = True
            self.fooSource.getInventory.return_value = 0
        elif (testName == "testD_orderFilled"):
            self.fooSource = Mock(spec = Warehouse)
            self.fooSource.hasInventory.return_value = True
            self.fooSource.getInventory.return_value = 10
        elif (testName == "testE_orderIncomplete"):
            self.fooSource = Mock(spec = Warehouse)
            self.fooSource.hasInventory.return_value = True
            self.fooSource.getInventory.return_value = 5
        else:
            print "UNSUPPORTED TEST ROUTINE"
    
    # ending the test
    def tearDown(self):
        """Cleaning up after the test"""
        print "OrderTest:tearDown_:begin"
        print ""
    
    # test: new order
    # objective: creating an order
    def testA_newOrder(self):
        # creating a new order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # test for a nil object
        self.assertIsNotNone(testOrder, "Order object is a nil.")
        
        # test for a valid item name
        testName = testOrder._orderItem
        self.assertEqual(testName, "mushrooms", "Invalid item name")
        
        # test for a valid item amount
        testAmount = testOrder._orderAmount
        self.assertGreater(testAmount, 0, "Invalid item amount")
    
    # test: nil inventory
    # objective: how the order object handles a nil inventory
    def testB_nilInventory(self):
        """Test routine B"""
        # creating a new order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # fill the order
        testSource = self.fooSource()
        testOrder.fill(testSource)
        
        # print the mocked calls
        print self.fooSource.mock_calls
        
        # check the call history
        testCalls = [call()]
        self.fooSource.assert_has_calls(testCalls)
    
    # ... continued in the next listing

OrderTest类有五个测试例程。所有五个测试例程在开始的时候都创建了一个Order类的实例。例程testA_newOrder()测试Order对象是否可用是否有正确的数据(46~60行)。例程testB_nilWarehouse()创建了一个空的mock并传入Order对象的fill()方法(64~79行)。它检查了mock的调用历史,确保仅仅发生了工厂调用。

The routine testC_orderCheck() (Listing Twenty-six) tests how the Orderobject reacts to a missing inventory item. Initially, fooSource responds with aTrue with its hasInventory() method and a 0 with getinventory(). The test routine checks if the order is not fulfilled and if the right mocked method is called (lines 16-19). Then the routine creates a new Order object, this time for a different item. The mock fooSource is set to respond with a False with its hasInventory() method (line 27). Again, the routine checks if the order is not filled and if the right mocked method is called (lines 34-37). Note the use of the reset_mock() method to restore fooSource to its pre-test state (line 28).

Listing Twenty-six

class OrderTest(unittest.TestCase):
    # ... see previous listing
    
    # test: checking the inventory
    # objective: does the order object check for inventory?
    def testC_orderCheck(self):
        """Test routine C"""
        # creating a test order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # perform the test
        testOrder.fill(self.fooSource)
        
        # perform the checks
        self.assertFalse(testOrder.isFilled())
        self.assertEqual(testOrder._orderFilled, 0)
        
        self.fooSource.hasInventory.assert_called_once_with("mushrooms")
        print self.fooSource.mock_calls
        
        # creating another order
        testOrder = Order("cabbage", 10)
        print repr(testOrder)
        
        # reconfigure the test resource
        self.fooSource.hasInventory.return_value = False
        self.fooSource.reset_mock()
        
        # perform the test
        testOrder.fill(self.fooSource)
        
        # perform the checks
        self.assertFalse(testOrder.isFilled())
        self.assertEqual(testOrder._orderFilled, -1)
        
        self.fooSource.hasInventory.assert_called_once_with("cabbage")
        print self.fooSource.mock_calls
    
    # ... continued in the next listing

Test routine testD_orderFilled() (Listing Twenty-seven) simulates a successful order transaction. Here, fooSource responds with a True with itshasInventory() method and returns 10 with getinventory(). The routine passes the mock to the fill() method, then checks if the order has been filled (lines 17-18). It also checks whether the right mocked methods were called in the right order and with the right arguments (lines 20-24).

Listing Twenty-seven

class OrderTest(unittest.TestCase):
    # ... see previous listing
    
    # test: fulfilling an order
    # objective: how does the order object behave with a successful transaction
    def testD_orderFilled(self):
        """Test routine D"""
        # creating a test order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # perform the test
        testOrder.fill(self.fooSource)
        print testOrder.isFilled()
        
        # perform the checks
        self.assertTrue(testOrder.isFilled())
        self.assertNotEqual(testOrder._orderFilled, -1)
        
        self.fooSource.hasInventory.assert_called_once_with("mushrooms")
        self.fooSource.getInventory.assert_called_with("mushrooms", 10)
        
        testCalls = [call.hasInventory("mushrooms"), call.getInventory("mushrooms", 10)]
        self.fooSource.assert_has_calls(testCalls)
    
    # ... continued in the next listing

Test routine testE_orderIncomplete() (Listing Twenty-eight) simulates an incomplete transaction. In this one, fooSource responds with a True with itshasInventory(), but with a 5 with getinventory(). The routine passes the mock to the fill() method, then checks for an incomplete order (lines 17-18). And it also checks if the right mocked methods were called in the right order and with the right arguments (lines 20-25).

Listing Twenty-eight

class OrderTest(unittest.TestCase):
    # ... see previous listing
    
    # test: fulfilling an order
    # objective: how does the order object behave with an incomplete transaction
    def testE_orderIncomplete(self):
        """Test routine E"""
        # creating a test order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # perform the test
        testOrder.fill(self.fooSource)
        print testOrder.isFilled()
        
        # perform the checks
        self.assertFalse(testOrder.isFilled())
        self.assertNotEqual(testOrder._orderFilled, testOrder._orderAmount)
        
        self.fooSource.hasInventory.assert_called_once_with("mushrooms")
        self.fooSource.getInventory.assert_called_with("mushrooms", 10)
        print self.fooSource.mock_calls
        
        testCalls = [call.hasInventory("mushrooms"), call.getInventory("mushrooms", 10)]
        self.fooSource.assert_has_calls(testCalls)
译者信息

例程testC_orderCheck()(Listing Twenty-six)测试了Order对象在库存不足时的反应。最初,fooSource的hasInventory()方法响应True,getinventory()方法返回0。测试例程检查是否订单未达成,是否正确的mock方法被带调用(16~19行)。然后测试例程创建了一个新的Order对象,这次是一个不同的项目。mock(fooSource)的方法hasInventory()的响应设置为False(第27行)。再次,例程检查是否订单未达成,是否调用了正确的mock方法(34~37行)。注意使用reset_mock()方法将fooSource恢复到测试前的状态(第28行)。

Listing Twenty-six

class OrderTest(unittest.TestCase):
    # ... see previous listing
    
    # test: checking the inventory
    # objective: does the order object check for inventory?
    def testC_orderCheck(self):
        """Test routine C"""
        # creating a test order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # perform the test
        testOrder.fill(self.fooSource)
        
        # perform the checks
        self.assertFalse(testOrder.isFilled())
        self.assertEqual(testOrder._orderFilled, 0)
        
        self.fooSource.hasInventory.assert_called_once_with("mushrooms")
        print self.fooSource.mock_calls
        
        # creating another order
        testOrder = Order("cabbage", 10)
        print repr(testOrder)
        
        # reconfigure the test resource
        self.fooSource.hasInventory.return_value = False
        self.fooSource.reset_mock()
        
        # perform the test
        testOrder.fill(self.fooSource)
        
        # perform the checks
        self.assertFalse(testOrder.isFilled())
        self.assertEqual(testOrder._orderFilled, -1)
        
        self.fooSource.hasInventory.assert_called_once_with("cabbage")
        print self.fooSource.mock_calls
    
    # ... continued in the next listing

测试例程testD_orderFilled()(Listing Twenty-seven)模拟了一个成功的订单事务。fooSource的hasInventory()方法响应True,getinventory()方法返回10。例程调用fill()方法传入mock对象,然后检查订单是否已完成(17~18行)。它也检查了是否采用正确的顺序和正确的参数调用了 正确的mock方法(20~24行)。

Listing Twenty-seven

class OrderTest(unittest.TestCase):
    # ... see previous listing
    
    # test: fulfilling an order
    # objective: how does the order object behave with a successful transaction
    def testD_orderFilled(self):
        """Test routine D"""
        # creating a test order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # perform the test
        testOrder.fill(self.fooSource)
        print testOrder.isFilled()
        
        # perform the checks
        self.assertTrue(testOrder.isFilled())
        self.assertNotEqual(testOrder._orderFilled, -1)
        
        self.fooSource.hasInventory.assert_called_once_with("mushrooms")
        self.fooSource.getInventory.assert_called_with("mushrooms", 10)
        
        testCalls = [call.hasInventory("mushrooms"), call.getInventory("mushrooms", 10)]
        self.fooSource.assert_has_calls(testCalls)
    
    # ... continued in the next listing

测试例程testE_orderIncomplete()(Listing Twenty-eight)模拟了一个未完成的事务。在这个测试中,fooSource的方法hasInventory()响应True,但是getinventory()返回5。例程调用fill()方法传入mock对象,然后检查未完成的订单(17~18行)。 它也检查了是否采用正确的顺序和正确的参数调用了正确的mock方法(20~25行)。

Listing Twenty-eight

class OrderTest(unittest.TestCase):
    # ... see previous listing
    
    # test: fulfilling an order
    # objective: how does the order object behave with an incomplete transaction
    def testE_orderIncomplete(self):
        """Test routine E"""
        # creating a test order
        testOrder = Order("mushrooms", 10)
        print repr(testOrder)
        
        # perform the test
        testOrder.fill(self.fooSource)
        print testOrder.isFilled()
        
        # perform the checks
        self.assertFalse(testOrder.isFilled())
        self.assertNotEqual(testOrder._orderFilled, testOrder._orderAmount)
        
        self.fooSource.hasInventory.assert_called_once_with("mushrooms")
        self.fooSource.getInventory.assert_called_with("mushrooms", 10)
        print self.fooSource.mock_calls
        
        testCalls = [call.hasInventory("mushrooms"), call.getInventory("mushrooms", 10)]
        self.fooSource.assert_has_calls(testCalls)

Conclusion

Mocks let us simulate resources that are either unavailable or too unwieldy for unit testing. We can configure a mock on the fly, change how it behaves or responds in a particular test, or get it to throw errors and exceptions on cue.

In this article, we looked at how a unit test setup benefits from a mock. We learned how a mock differs from a fake or a stub. We learned how to create a mock on Python, how to manage it and how to track its behavior with asserts. And we examined a simple mock at work in a basic test case.Feel free to try out this test setup and observe its results. Feel free to tweak the test routines presented here, and experiment with how those tweaks affect the test.

References

Alex Marandon. Python Mock Gotchas.

Grig Gheorghiu. (2009). Python Mock Testing Techniques and Tools. Python Magazine

Insomihack. Python Unit Testing with Mock.

Matt Cottingham. Python: Injecting Mock Objects for Powerful Testing.

Michael Foord. (2012). Mock Documentation, Release 1.0.1. [PDF]

Martin Fowler. Mocks Aren't Stubs.

Naftuli Tzvi Kay. An Introduction to Mocking in Python.

Steve Freeman, Tim Mackinnon, et al. (2004). Mock Roles, Not Objects [PDF].OOPLSA 2004

译者信息

结束语

Mocks让我们为单元测试模拟了那些不可用或者是太庞大的资源。我们可以在运行中配置mock,在特定的测试中改变它的行为或响应,或者让它在恰当的时候抛出错误和异常。

在这篇文章中,我们看到了在单元测试中设置mock的好处。我们了解了mock和fake或者stub的区别。我们了解了怎样在Python中创建mock,怎样管理它和用断言跟踪它的行为。我们研究了简单的mock在基础测试用例中的工作。随意尝试了这个测试设置并观察它的结构。随意的调整已提供的测试例程并观察这些调整对测试的影响。

引用

Alex Marandon. Python Mock Gotchas.

Grig Gheorghiu. (2009). Python Mock Testing Techniques and Tools. Python Magazine

Insomihack. Python Unit Testing with Mock.

Matt Cottingham. Python: Injecting Mock Objects for Powerful Testing.

Michael Foord. (2012). Mock Documentation, Release 1.0.1. [PDF]

Martin Fowler. Mocks Aren't Stubs.

Naftuli Tzvi Kay. An Introduction to Mocking in Python.

Steve Freeman, Tim Mackinnon, et al. (2004). Mock Roles, Not Objects [PDF]. OOPLSA 2004



http://www.oschina.net/translate/unit-testing-with-the-python-mock-class?cmp&p=3#
http://www.drdobbs.com/testing/unit-testing-with-the-python-mock-class/240168251

你可能感兴趣的:(python,python,mock)