Python模拟框架
项目描述
.. image:: https://secure.travis-ci.org/vmalloc/pyforge.png
这是什么?
===========
Forge是一个Python的模拟库。它的大部分灵感来自Mox(http://code.google.com/p/pymox)。它旨在简单但功能丰富,为使用模拟方法进行单元测试提供最大的灵活性。
运行Forge的验收测试套件
================================
Forge的所有验收测试都在根目录下的tests/目录中。它们需要unittest2或内置的unittest模块(2.7/3.2及以上)。
建议使用*nosetests*脚本来运行测试,但如果没有它,可以使用*run_tests*脚本。
安装
============
安装Forge与您已知的大多数包几乎相同
::
python setup.py install
使用方法
=====
基础
------
Forge主要创建模拟对象和函数存根,但具有多种风格。使用Forge始终从创建一个"模拟管理器"开始,使用*Forge*类:
>>> from forge import Forge
>>> forge_manager = Forge()
没有保留多个Forge管理器的真正理由。它通常用于创建模拟:
>>> class SomeClass(object)
... def f(self, a, b, c)
... pass
>>> mock = forge_manager.create_mock(SomeClass)
>>> mock
<Mock of 'SomeClass'>
模拟测试通常以记录-回放的方式运行。您记录您期望模拟执行的操作,然后回放它,而Forge跟踪发生的情况并确保它是正确的:
>>> forge_manager.is_recording()
正确
>>> mock.f(1, 2, 3) # doctest: +ELLIPSIS
<...>
>>> mock.f(3, 4, 5) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> forge_manager.is_recording()
错误
>>> forge_manager.is_replaying()
正确
>>> mock.f(1, 2, 3)
>>> mock.f(3, 4, 5)
>>> forge_manager.verify() # 此操作验证没有预期之外的其他调用
要重新开始从头开始工作,您可以始终执行:
>>> forge_manager.reset()
就像类可以产生模拟对象一样,通过使用 *Forge.create_function_stub*,普通函数也可以产生存根:
>>> def some_func(a, b, c)
... pass
>>> stub = forge_manager.create_function_stub(some_func)
随着方法和函数的记录,它们的签名将与其他记录的调用进行验证。在回放时,调用必须与原始调用匹配,因此您不必过分担心关于函数签名的意外。
为了提高代码的友好性,两个上下文管理器提供了语法糖,可以结构化测试代码
>>> with forge_manager.record_context()
... mock.f(1, 2, 3) # doctest: +ELLIPSIS
... mock.f(3, 4, 5) # doctest: +ELLIPSIS
<...>
>>> with forge_manager.verified_replay_context()
... mock.f(1, 2, 3) # doctest: +ELLIPSIS
... mock.f(3, 4, 5) # doctest: +ELLIPSIS
失败和意外事件
------------------------------
当发生未预期的事件时,将抛出异常,解释发生了什么:
>>> stub = forge_manager.create_function_stub(some_func)
>>> stub(1, 2, 3) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> stub(1, 2, 4) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last)
...
UnexpectedCall: 未预期的函数调用!(预期:+,得到:-)
- some_func(a=1, b=2, c=4)
? ^
+ some_func(a=1, b=2, c=3)
? ^
>>> forge_manager.reset()
在某些情况下这已经足够,但如果您想了解调用是在哪里记录和回放的,您可以打开调试信息:
>>> forge_manager.debug.enable()
>>> stub = forge_manager.create_function_stub(some_func)
>>> stub(1, 2, 3) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> stub(1, 2, 4) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last)
...
UnexpectedCall: 未预期的函数调用!(预期:+,得到:-)
记录来自...
回放来自...
- some_func(a=1, b=2, c=4)
? ^
+ some_func(a=1, b=2, c=3)
? ^
>>> forge_manager.reset()
>>> forge_manager.debug.disable()
由于这通常是一个非常常见的模式,您还可以通过设置环境变量来打开调试,通过在运行测试时将 FORGE_DEBUG 环境变量设置为任何内容来实现。
期望属性设置
---------------------------
只有在记录模式下才允许设置模拟对象的属性。默认情况下,在回放期间设置的属性将触发异常。
但是,在某些情况下,您希望 *期望* 在回放过程中的某个点设置属性。由于 Forge setattr/getattr 机制的hackish性质,通过 __forge__ 处理器使用专用API来完成此操作:
>>> mock = forge_manager.create_mock(SomeClass)
>>> mock.__forge__.expect_setattr("length", 20) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> mock.length = 20
>>> forge_manager.verify()
>>> forge_manager.reset()
您还可以将模拟对象设置为忽略属性设置(因此允许所有 setattrs,无论其性质如何)。这不被推荐,但在某些情况下可能有用:
>>> mock.__forge__.enable_setattr_during_replay()
>>> forge_manager.replay()
>>> mock.a = 2 # 有效!
>>> forge_manager.reset()
如果您想模拟一个 *模拟结构*,即具有其他对象作为属性的属性,您可以使用 *create_mock_with_attrs* API。如果创建一个快捷方式,这将特别简洁:
>>> class A(object): pass
>>> class B(object): pass
>>> class C(object): pass
>>> MOCK = forge_manager.create_mock_with_attrs
>>> result = MOCK(A, b=MOCK(B, c=MOCK(C)))
>>> result.b.c # doctest: +ELLIPSIS
<Mock of 'C'>
动作
-------
当期望在存根上调用时,您可以控制调用发生时的 *行为*。支持的情况有
- 控制返回值:
my_stub(1, 2, 3).and_return(666)
- 调用另一个函数(无参数):
my_stub(1, 2, 3).and_call(callback)
- 调用另一个函数(带有特定参数/关键字参数):
my_stub(1, 2, 3).and_call(callback, args=(100, 200), kwargs={'some_arg':20})
- 调用另一个函数(使用调用的参数):
my_stub(1, 2, 3).and_call_with_args(callback)
- 抛出异常(在所有回调执行后发生):
my_stub(1, 2, 3).and_raise(MyException())
比较器
-----------
如果您不知道函数参数将获取的确切值,有时您必须使用谓词来帮助您区分有效案例和无效案例。首先,我们提到模拟对象只与自己比较为“true”,所以您不必担心模拟比较中的任何奇怪行为。
为了完整起见,如果您想对记录的参数进行各种检查,您可以使用比较器。例如,以下代码不关心传递给“name”的参数是哪一个,只要它是字符串即可:
my_stub(name=IsA(basestring))
Forge中存在许多比较器
* ``Is(x)``:仅在参数为 *x* 时比较为真
* ``IsA(type)``:仅在参数为 *type* 类型时比较为真
* ``RegexpMatches(regexp, [flags])``:仅在参数为字符串且与 *regexp* 匹配时比较为真
* ``Func(f)``:仅在 *f* 对参数返回 True 时比较为真
* ``IsAlmost(value, [places])``:仅在参数几乎与 *value* 相同,且在小数点后有 *places* 位数字时比较为真
* ``Contains(element)``:仅在 *element* 存在于参数中时比较为真
* ``StrContains(substring)``:仅在 *substring* 存在于参数中且参数为字符串时比较为真
* ``HasKeyValue(key, value)``:仅在参数有 *key* 作为键,其值为 *value* 时比较为真
* ``HasAttributeValue(attr, value)``:与 HasKeyValue 相同,但用于属性
* ``Anything()``:始终比较为真
* ``And(...), Or(...), Not(c)``:其他比较器的与、或和否定
用存根替换方法和函数
------------------------------------------
Forge包括一种机制,用于安装(和稍后删除)存根以代替普通的方法和函数:
>>> import time
>>> forge_manager.replace(time, "time") # doctest: +ELLIPSIS
<...>
>>> time.time().and_return(2)
2
>>> forge_manager.replay()
>>> time.time()
2
>>> forge_manager.verify()
>>> forge_manager.restore_all_replacements()
>>> forge_manager.reset()
当然,这也适用于方法
>>> class MyClass(object)
... def f(self)
... self.g()
... def g(self)
... raise NotImplementedError()
>>> instance = MyClass()
>>> forge_manager.replace(instance, "g") # doctest: +ELLIPSIS
<...>
>>> instance.g() # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> instance.f()
>>> forge_manager.verify()
>>> forge_manager.restore_all_replacements()
>>> forge_manager.reset()
您还可以使用相同的安装机制来设置自定义值,并在所有存根随之后恢复:
>>> class SomeClass(object)
... x = 2
>>> forge_manager.replace_with(SomeClass, "x", 3)
3
>>> SomeClass.x
3
>>> forge_manager.restore_all_replacements()
>>> SomeClass.x
2
替换也支持在上下文中进行,在退出上下文时恢复安装的存根:
>>> with forge_manager.replacing_context(SomeClass, "x")
... pass
排序
--------
默认情况下,Forge会验证实际调用顺序与记录流中的顺序相同。
但是,您可以控制它并创建在组中顺序无关紧要的组:
>>> class SomeClass(object)
... def func(self, arg)
... pass
>>> mock = forge_manager.create_mock(SomeClass)
>>> mock.func(1) # doctest: +ELLIPSIS
<...>
>>> mock.func(2) # doctest: +ELLIPSIS
<...>
>>> mock.func(3) # doctest: +ELLIPSIS
... # 到目前为止,顺序必须保持不变
<...>
>>> with forge_manager.any_order(): # doctest: +ELLIPSIS
... mock.func(4)
... mock.func(5)
<...>
<...>
>>> mock.func(6) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> mock.func(1)
>>> mock.func(2)
>>> mock.func(3)
>>> mock.func(5) # ok!
>>> mock.func(4) # also ok!
>>> mock.func(6)
>>> forge_manager.verify()
>>> forge_manager.reset()
您可以使用 *ordered*、*any_order* 和 *interleaved_order*(见下文)来嵌套排序组:
>>> with forge_manager.any_order(): # doctest: +ELLIPSIS
... mock.func(4)
... with forge_manager.ordered()
... mock.func(5)
... mock.func(6)
... mock.func(7)
<...>
<...>
<...>
<...>
在上面的示例中,func(5)和func(6)将按特定顺序断言发生,但该组可以出现在func(4)和func(7)之间任何位置。
>>> forge_manager.replay()
>>> for i in (5, 6, 7, 4)
... _ = mock.func(i)
>>> forge_manager.verify()
>>> forge_manager.reset()
在嵌套排序组的上下文中,当处理协程/greenlets时,*交错*排序可能会很有用:
>>> class SomeClass(object)
... def foo(self, arg)
... pass
... def bar(self, arg)
... pass
>>> mock = forge_manager.create_mock(SomeClass)
>>> with forge_manager.interleaved_order(): # doctest: +ELLIPSIS
... with forge_manager.ordered()
... mock.foo(1)
... mock.foo(2)
... with forge_manager.ordered()
... mock.bar(1)
... mock.bar(2)
<...>
<...>
<...>
<...>
>>> forge_manager.replay()
>>> mock.foo(1)
>>> mock.bar(1)
>>> mock.foo(2)
>>> mock.bar(2)
>>> forge_manager.verify()
>>> forge_manager.reset()
上述预期也可以与以下序列一起工作
>>> with forge_manager.interleaved_order(): # doctest: +ELLIPSIS
... with forge_manager.ordered()
... mock.foo(1)
... mock.foo(2)
... with forge_manager.ordered()
... mock.bar(1)
... mock.bar(2)
<...>
<...>
<...>
<...>
>>> forge_manager.replay()
>>> mock.bar(1)
>>> mock.bar(2)
>>> mock.foo(1)
>>> mock.foo(2)
>>> forge_manager.verify()
>>> forge_manager.reset()
无论何时
--------
有时你希望一个函数被调用零次或多次,无论时机如何,并返回一个特定的值(或引发异常)。有几种不美观的方法可以做到这一点:
>>> class MyObj(object)
... def f(self)
... pass
>>> m = forge_manager.create_mock(MyObj)
>>> m.f = lambda: 2 # yuck!
当然,缺点是
* f存在的事实没有得到验证。此外,该方法未验证其签名。
* lambda很丑,并且当想要使用异常时,事情变得更糟。
*whenever()*拯救了 - 它是一个可以调用的预期方法的方法,导致调用被接受、签名检查并执行。然而,与常规录制不同,它期望调用0次或多次,在任何时间点 - 因此它达到了相同的效果:
>>> m = forge_manager.create_mock(MyObj)
>>> m.f().whenever().and_return(2)
2
>>> forge_manager.replay()
>>> m.f()
2
>>> m.f()
2
>>> forge_manager.verify()
>>> forge_manager.reset()
可以指定多个具有不同参数的*whenever()*录制,从而实现请求的调用的“模式匹配”(每个调用签名将产生不同的返回值)。
存在*whenever()*的替代语法,以便更容易阅读:
>>> class Obj(object)
... def f(self, value)
... pass
>>> m = forge_manager.create_mock(Obj)
>>> m.f.when(2).then_return(3)
3
>>> forge_manager.replay()
>>> m.f(2)
3
>>> forge_manager.verify()
>>> forge_manager.reset()
.. note:: whenever()调用始终适用于它们被记录的排序组。这意味着一旦一个顺序组被清除,该组中记录的所有*whenever*都将自动“忘记”,并且在重新播放时将不再被接受。
通配符Mock
--------------
尽管不推荐,但有时你只想在录制时接受任何内容,并仅验证你在重放时坚持使用它。这对于原型设计尚未存在的接口很有用。在Forge中,这是通过使用*通配符Mock*来完成的:
>>> mock = forge_manager.create_wildcard_mock()
>>> mock
<<Wildcard>>
>>> stub = forge_manager.create_wildcard_function_stub()
>>> stub
<Stub for '<<Wildcard>>'>
>>> mock.f() # doctest: +ELLIPSIS
<...>
>>> mock.g(1, 2, 3, d=4) # doctest: +ELLIPSIS
<...>
>>> stub() # doctest: +ELLIPSIS
<...>
>>> stub(1, 2, 3, d=4) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> mock.f()
>>> mock.g(1, 2, 3, d=4)
>>> stub()
>>> stub(1, 2, 3, d=4)
>>> forge_manager.reset()
类Mock
-----------
有时你想要模拟类的行为,而不是对象。Forge允许使用*create_class_mock* API来完成此操作:
>>> class MyClass(object)
... def __init__(self, a, b, c)
... pass
... def regular_method(self)
... pass
... @classmethod
... def some_class_method(cls)
... pass
... @staticmethod
... def some_static_method()
... pass
>>> class_mock = forge_manager.create_class_mock(MyClass)
>>> class_mock
<Class mock of 'MyClass'>
>>> class_mock.regular_method() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last)
SignatureException: ...
>>> class_mock.some_class_method() # doctest: +ELLIPSIS
<...>
>>> class_mock.some_static_method() # doctest: +ELLIPSIS
<...>
>>> fake_new_instance = forge_manager.create_mock(MyClass)
>>> class_mock(1, 2, 3).and_return(fake_new_instance) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> class_mock.some_class_method()
>>> class_mock.some_static_method()
>> 断言 class_mock(1, 2, 3) 是 fake_new_instance
>>> forge_manager.verify()
>>> forge_manager.reset()
混合模拟
------------
假设你有一个如下所示的类:
>> class File(object)
... def __init__(self, filename)
... self.f = open(filename, "rb")
... def read(self, size)
... raise NotImplementedError()
... def log(self, buffer)
... raise NotImplementedError()
... def read_and_log(self, size)
... data = self.read(size)
... self.log(data)
... return data
现在,假设你想为 read_and_log 编写测试,同时模拟 read() 和 log() 的行为。这很常见,因为有时类中的方法有很多副作用,在编写测试时很难连接。一个简单的方法是创建一个 File 对象,并用存根替换 read() 和 log()(见上文)。这很好,但问题是类构造函数打开了一个文件进行读取。
在某些情况下,构造函数(特别是在添加测试的旧代码中)做了很多难以模拟的事情,或者很可能会改变,从而破坏你可能安装的任何存根。对于这种情况,Forge 提供了混合模拟:
>> mock = forge_manager.create_hybrid_mock(File)
>> mock.read(20).and_return("data") # doctest: +ELLIPSIS
'...'
>> mock.log("data") # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>> assert mock.read_and_log(20) == "data"
>>> forge_manager.verify()
>>> forge_manager.reset()
混合模拟,顾名思义,是混合的。在记录期间,它们的行为与常规模拟相同,但在回放期间调用任何未记录的方法将调用模拟上的原始方法,从而在隔离环境中对其进行测试。
类等效也存在:
>>> class SomeClass(object)
... def __init__(self, parameter)
... raise NotImplementedError()
... @classmethod
... def constructor(cls)
... return cls(1)
>> mock = forge_manager.create_hybrid_class_mock(SomeClass)
>> expected_return_value = forge_manager.create_sentinel()
>> mock(1).and_return(expected_return_value) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>> got_return_value = mock.constructor()
>> got_return_value is expected_return_value
正确
这是什么?
===========
Forge是一个Python的模拟库。它的大部分灵感来自Mox(http://code.google.com/p/pymox)。它旨在简单但功能丰富,为使用模拟方法进行单元测试提供最大的灵活性。
运行Forge的验收测试套件
================================
Forge的所有验收测试都在根目录下的tests/目录中。它们需要unittest2或内置的unittest模块(2.7/3.2及以上)。
建议使用*nosetests*脚本来运行测试,但如果没有它,可以使用*run_tests*脚本。
安装
============
安装Forge与您已知的大多数包几乎相同
::
python setup.py install
使用方法
=====
基础
------
Forge主要创建模拟对象和函数存根,但具有多种风格。使用Forge始终从创建一个"模拟管理器"开始,使用*Forge*类:
>>> from forge import Forge
>>> forge_manager = Forge()
没有保留多个Forge管理器的真正理由。它通常用于创建模拟:
>>> class SomeClass(object)
... def f(self, a, b, c)
... pass
>>> mock = forge_manager.create_mock(SomeClass)
>>> mock
<Mock of 'SomeClass'>
模拟测试通常以记录-回放的方式运行。您记录您期望模拟执行的操作,然后回放它,而Forge跟踪发生的情况并确保它是正确的:
>>> forge_manager.is_recording()
正确
>>> mock.f(1, 2, 3) # doctest: +ELLIPSIS
<...>
>>> mock.f(3, 4, 5) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> forge_manager.is_recording()
错误
>>> forge_manager.is_replaying()
正确
>>> mock.f(1, 2, 3)
>>> mock.f(3, 4, 5)
>>> forge_manager.verify() # 此操作验证没有预期之外的其他调用
要重新开始从头开始工作,您可以始终执行:
>>> forge_manager.reset()
就像类可以产生模拟对象一样,通过使用 *Forge.create_function_stub*,普通函数也可以产生存根:
>>> def some_func(a, b, c)
... pass
>>> stub = forge_manager.create_function_stub(some_func)
随着方法和函数的记录,它们的签名将与其他记录的调用进行验证。在回放时,调用必须与原始调用匹配,因此您不必过分担心关于函数签名的意外。
为了提高代码的友好性,两个上下文管理器提供了语法糖,可以结构化测试代码
>>> with forge_manager.record_context()
... mock.f(1, 2, 3) # doctest: +ELLIPSIS
... mock.f(3, 4, 5) # doctest: +ELLIPSIS
<...>
>>> with forge_manager.verified_replay_context()
... mock.f(1, 2, 3) # doctest: +ELLIPSIS
... mock.f(3, 4, 5) # doctest: +ELLIPSIS
失败和意外事件
------------------------------
当发生未预期的事件时,将抛出异常,解释发生了什么:
>>> stub = forge_manager.create_function_stub(some_func)
>>> stub(1, 2, 3) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> stub(1, 2, 4) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last)
...
UnexpectedCall: 未预期的函数调用!(预期:+,得到:-)
- some_func(a=1, b=2, c=4)
? ^
+ some_func(a=1, b=2, c=3)
? ^
>>> forge_manager.reset()
在某些情况下这已经足够,但如果您想了解调用是在哪里记录和回放的,您可以打开调试信息:
>>> forge_manager.debug.enable()
>>> stub = forge_manager.create_function_stub(some_func)
>>> stub(1, 2, 3) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> stub(1, 2, 4) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last)
...
UnexpectedCall: 未预期的函数调用!(预期:+,得到:-)
记录来自...
回放来自...
- some_func(a=1, b=2, c=4)
? ^
+ some_func(a=1, b=2, c=3)
? ^
>>> forge_manager.reset()
>>> forge_manager.debug.disable()
由于这通常是一个非常常见的模式,您还可以通过设置环境变量来打开调试,通过在运行测试时将 FORGE_DEBUG 环境变量设置为任何内容来实现。
期望属性设置
---------------------------
只有在记录模式下才允许设置模拟对象的属性。默认情况下,在回放期间设置的属性将触发异常。
但是,在某些情况下,您希望 *期望* 在回放过程中的某个点设置属性。由于 Forge setattr/getattr 机制的hackish性质,通过 __forge__ 处理器使用专用API来完成此操作:
>>> mock = forge_manager.create_mock(SomeClass)
>>> mock.__forge__.expect_setattr("length", 20) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> mock.length = 20
>>> forge_manager.verify()
>>> forge_manager.reset()
您还可以将模拟对象设置为忽略属性设置(因此允许所有 setattrs,无论其性质如何)。这不被推荐,但在某些情况下可能有用:
>>> mock.__forge__.enable_setattr_during_replay()
>>> forge_manager.replay()
>>> mock.a = 2 # 有效!
>>> forge_manager.reset()
如果您想模拟一个 *模拟结构*,即具有其他对象作为属性的属性,您可以使用 *create_mock_with_attrs* API。如果创建一个快捷方式,这将特别简洁:
>>> class A(object): pass
>>> class B(object): pass
>>> class C(object): pass
>>> MOCK = forge_manager.create_mock_with_attrs
>>> result = MOCK(A, b=MOCK(B, c=MOCK(C)))
>>> result.b.c # doctest: +ELLIPSIS
<Mock of 'C'>
动作
-------
当期望在存根上调用时,您可以控制调用发生时的 *行为*。支持的情况有
- 控制返回值:
my_stub(1, 2, 3).and_return(666)
- 调用另一个函数(无参数):
my_stub(1, 2, 3).and_call(callback)
- 调用另一个函数(带有特定参数/关键字参数):
my_stub(1, 2, 3).and_call(callback, args=(100, 200), kwargs={'some_arg':20})
- 调用另一个函数(使用调用的参数):
my_stub(1, 2, 3).and_call_with_args(callback)
- 抛出异常(在所有回调执行后发生):
my_stub(1, 2, 3).and_raise(MyException())
比较器
-----------
如果您不知道函数参数将获取的确切值,有时您必须使用谓词来帮助您区分有效案例和无效案例。首先,我们提到模拟对象只与自己比较为“true”,所以您不必担心模拟比较中的任何奇怪行为。
为了完整起见,如果您想对记录的参数进行各种检查,您可以使用比较器。例如,以下代码不关心传递给“name”的参数是哪一个,只要它是字符串即可:
my_stub(name=IsA(basestring))
Forge中存在许多比较器
* ``Is(x)``:仅在参数为 *x* 时比较为真
* ``IsA(type)``:仅在参数为 *type* 类型时比较为真
* ``RegexpMatches(regexp, [flags])``:仅在参数为字符串且与 *regexp* 匹配时比较为真
* ``Func(f)``:仅在 *f* 对参数返回 True 时比较为真
* ``IsAlmost(value, [places])``:仅在参数几乎与 *value* 相同,且在小数点后有 *places* 位数字时比较为真
* ``Contains(element)``:仅在 *element* 存在于参数中时比较为真
* ``StrContains(substring)``:仅在 *substring* 存在于参数中且参数为字符串时比较为真
* ``HasKeyValue(key, value)``:仅在参数有 *key* 作为键,其值为 *value* 时比较为真
* ``HasAttributeValue(attr, value)``:与 HasKeyValue 相同,但用于属性
* ``Anything()``:始终比较为真
* ``And(...), Or(...), Not(c)``:其他比较器的与、或和否定
用存根替换方法和函数
------------------------------------------
Forge包括一种机制,用于安装(和稍后删除)存根以代替普通的方法和函数:
>>> import time
>>> forge_manager.replace(time, "time") # doctest: +ELLIPSIS
<...>
>>> time.time().and_return(2)
2
>>> forge_manager.replay()
>>> time.time()
2
>>> forge_manager.verify()
>>> forge_manager.restore_all_replacements()
>>> forge_manager.reset()
当然,这也适用于方法
>>> class MyClass(object)
... def f(self)
... self.g()
... def g(self)
... raise NotImplementedError()
>>> instance = MyClass()
>>> forge_manager.replace(instance, "g") # doctest: +ELLIPSIS
<...>
>>> instance.g() # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> instance.f()
>>> forge_manager.verify()
>>> forge_manager.restore_all_replacements()
>>> forge_manager.reset()
您还可以使用相同的安装机制来设置自定义值,并在所有存根随之后恢复:
>>> class SomeClass(object)
... x = 2
>>> forge_manager.replace_with(SomeClass, "x", 3)
3
>>> SomeClass.x
3
>>> forge_manager.restore_all_replacements()
>>> SomeClass.x
2
替换也支持在上下文中进行,在退出上下文时恢复安装的存根:
>>> with forge_manager.replacing_context(SomeClass, "x")
... pass
排序
--------
默认情况下,Forge会验证实际调用顺序与记录流中的顺序相同。
但是,您可以控制它并创建在组中顺序无关紧要的组:
>>> class SomeClass(object)
... def func(self, arg)
... pass
>>> mock = forge_manager.create_mock(SomeClass)
>>> mock.func(1) # doctest: +ELLIPSIS
<...>
>>> mock.func(2) # doctest: +ELLIPSIS
<...>
>>> mock.func(3) # doctest: +ELLIPSIS
... # 到目前为止,顺序必须保持不变
<...>
>>> with forge_manager.any_order(): # doctest: +ELLIPSIS
... mock.func(4)
... mock.func(5)
<...>
<...>
>>> mock.func(6) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> mock.func(1)
>>> mock.func(2)
>>> mock.func(3)
>>> mock.func(5) # ok!
>>> mock.func(4) # also ok!
>>> mock.func(6)
>>> forge_manager.verify()
>>> forge_manager.reset()
您可以使用 *ordered*、*any_order* 和 *interleaved_order*(见下文)来嵌套排序组:
>>> with forge_manager.any_order(): # doctest: +ELLIPSIS
... mock.func(4)
... with forge_manager.ordered()
... mock.func(5)
... mock.func(6)
... mock.func(7)
<...>
<...>
<...>
<...>
在上面的示例中,func(5)和func(6)将按特定顺序断言发生,但该组可以出现在func(4)和func(7)之间任何位置。
>>> forge_manager.replay()
>>> for i in (5, 6, 7, 4)
... _ = mock.func(i)
>>> forge_manager.verify()
>>> forge_manager.reset()
在嵌套排序组的上下文中,当处理协程/greenlets时,*交错*排序可能会很有用:
>>> class SomeClass(object)
... def foo(self, arg)
... pass
... def bar(self, arg)
... pass
>>> mock = forge_manager.create_mock(SomeClass)
>>> with forge_manager.interleaved_order(): # doctest: +ELLIPSIS
... with forge_manager.ordered()
... mock.foo(1)
... mock.foo(2)
... with forge_manager.ordered()
... mock.bar(1)
... mock.bar(2)
<...>
<...>
<...>
<...>
>>> forge_manager.replay()
>>> mock.foo(1)
>>> mock.bar(1)
>>> mock.foo(2)
>>> mock.bar(2)
>>> forge_manager.verify()
>>> forge_manager.reset()
上述预期也可以与以下序列一起工作
>>> with forge_manager.interleaved_order(): # doctest: +ELLIPSIS
... with forge_manager.ordered()
... mock.foo(1)
... mock.foo(2)
... with forge_manager.ordered()
... mock.bar(1)
... mock.bar(2)
<...>
<...>
<...>
<...>
>>> forge_manager.replay()
>>> mock.bar(1)
>>> mock.bar(2)
>>> mock.foo(1)
>>> mock.foo(2)
>>> forge_manager.verify()
>>> forge_manager.reset()
无论何时
--------
有时你希望一个函数被调用零次或多次,无论时机如何,并返回一个特定的值(或引发异常)。有几种不美观的方法可以做到这一点:
>>> class MyObj(object)
... def f(self)
... pass
>>> m = forge_manager.create_mock(MyObj)
>>> m.f = lambda: 2 # yuck!
当然,缺点是
* f存在的事实没有得到验证。此外,该方法未验证其签名。
* lambda很丑,并且当想要使用异常时,事情变得更糟。
*whenever()*拯救了 - 它是一个可以调用的预期方法的方法,导致调用被接受、签名检查并执行。然而,与常规录制不同,它期望调用0次或多次,在任何时间点 - 因此它达到了相同的效果:
>>> m = forge_manager.create_mock(MyObj)
>>> m.f().whenever().and_return(2)
2
>>> forge_manager.replay()
>>> m.f()
2
>>> m.f()
2
>>> forge_manager.verify()
>>> forge_manager.reset()
可以指定多个具有不同参数的*whenever()*录制,从而实现请求的调用的“模式匹配”(每个调用签名将产生不同的返回值)。
存在*whenever()*的替代语法,以便更容易阅读:
>>> class Obj(object)
... def f(self, value)
... pass
>>> m = forge_manager.create_mock(Obj)
>>> m.f.when(2).then_return(3)
3
>>> forge_manager.replay()
>>> m.f(2)
3
>>> forge_manager.verify()
>>> forge_manager.reset()
.. note:: whenever()调用始终适用于它们被记录的排序组。这意味着一旦一个顺序组被清除,该组中记录的所有*whenever*都将自动“忘记”,并且在重新播放时将不再被接受。
通配符Mock
--------------
尽管不推荐,但有时你只想在录制时接受任何内容,并仅验证你在重放时坚持使用它。这对于原型设计尚未存在的接口很有用。在Forge中,这是通过使用*通配符Mock*来完成的:
>>> mock = forge_manager.create_wildcard_mock()
>>> mock
<<Wildcard>>
>>> stub = forge_manager.create_wildcard_function_stub()
>>> stub
<Stub for '<<Wildcard>>'>
>>> mock.f() # doctest: +ELLIPSIS
<...>
>>> mock.g(1, 2, 3, d=4) # doctest: +ELLIPSIS
<...>
>>> stub() # doctest: +ELLIPSIS
<...>
>>> stub(1, 2, 3, d=4) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> mock.f()
>>> mock.g(1, 2, 3, d=4)
>>> stub()
>>> stub(1, 2, 3, d=4)
>>> forge_manager.reset()
类Mock
-----------
有时你想要模拟类的行为,而不是对象。Forge允许使用*create_class_mock* API来完成此操作:
>>> class MyClass(object)
... def __init__(self, a, b, c)
... pass
... def regular_method(self)
... pass
... @classmethod
... def some_class_method(cls)
... pass
... @staticmethod
... def some_static_method()
... pass
>>> class_mock = forge_manager.create_class_mock(MyClass)
>>> class_mock
<Class mock of 'MyClass'>
>>> class_mock.regular_method() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last)
SignatureException: ...
>>> class_mock.some_class_method() # doctest: +ELLIPSIS
<...>
>>> class_mock.some_static_method() # doctest: +ELLIPSIS
<...>
>>> fake_new_instance = forge_manager.create_mock(MyClass)
>>> class_mock(1, 2, 3).and_return(fake_new_instance) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>>> class_mock.some_class_method()
>>> class_mock.some_static_method()
>> 断言 class_mock(1, 2, 3) 是 fake_new_instance
>>> forge_manager.verify()
>>> forge_manager.reset()
混合模拟
------------
假设你有一个如下所示的类:
>> class File(object)
... def __init__(self, filename)
... self.f = open(filename, "rb")
... def read(self, size)
... raise NotImplementedError()
... def log(self, buffer)
... raise NotImplementedError()
... def read_and_log(self, size)
... data = self.read(size)
... self.log(data)
... return data
现在,假设你想为 read_and_log 编写测试,同时模拟 read() 和 log() 的行为。这很常见,因为有时类中的方法有很多副作用,在编写测试时很难连接。一个简单的方法是创建一个 File 对象,并用存根替换 read() 和 log()(见上文)。这很好,但问题是类构造函数打开了一个文件进行读取。
在某些情况下,构造函数(特别是在添加测试的旧代码中)做了很多难以模拟的事情,或者很可能会改变,从而破坏你可能安装的任何存根。对于这种情况,Forge 提供了混合模拟:
>> mock = forge_manager.create_hybrid_mock(File)
>> mock.read(20).and_return("data") # doctest: +ELLIPSIS
'...'
>> mock.log("data") # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>> assert mock.read_and_log(20) == "data"
>>> forge_manager.verify()
>>> forge_manager.reset()
混合模拟,顾名思义,是混合的。在记录期间,它们的行为与常规模拟相同,但在回放期间调用任何未记录的方法将调用模拟上的原始方法,从而在隔离环境中对其进行测试。
类等效也存在:
>>> class SomeClass(object)
... def __init__(self, parameter)
... raise NotImplementedError()
... @classmethod
... def constructor(cls)
... return cls(1)
>> mock = forge_manager.create_hybrid_class_mock(SomeClass)
>> expected_return_value = forge_manager.create_sentinel()
>> mock(1).and_return(expected_return_value) # doctest: +ELLIPSIS
<...>
>>> forge_manager.replay()
>> got_return_value = mock.constructor()
>> got_return_value is expected_return_value
正确
项目详情
关闭
pyforge-1.3.0.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | de97f900a6f1ffe5cfa87526946704254c4b70338638c4819469bea2716b7916 |
|
MD5 | 8148585982a221849e80da34ce68847e |
|
BLAKE2b-256 | 0228048f5829380506ed8066ad5b312c50a4771c29feede1a903dc4e6bdb79b4 |