├── requirements.txt ├── tests ├── __init__.py ├── comparator_py2.py ├── functional_test.py ├── samples.py ├── mock_test.py ├── chai_test.py ├── comparator_test.py ├── expectation_test.py ├── sample_test.py └── stub_test.py ├── examples ├── __init__.py └── examples.py ├── docs ├── source │ ├── tutorial │ │ ├── mocking.rst │ │ ├── stubbing.rst │ │ ├── comparators.rst │ │ ├── expectations.rst │ │ └── index.rst │ ├── future.rst │ ├── news.rst │ ├── intro.rst │ ├── index.rst │ └── conf.py ├── Makefile └── make.bat ├── .gitignore ├── chai ├── python2.py ├── __init__.py ├── _termcolor.py ├── spy.py ├── exception.py ├── mock.py ├── comparators.py ├── chai.py ├── expectation.py └── stub.py ├── .travis.yml ├── MANIFEST.in ├── LICENSE.txt ├── setup.py ├── CHANGELOG └── README.rst /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/tutorial/mocking.rst: -------------------------------------------------------------------------------- 1 | Mocking 2 | ======= -------------------------------------------------------------------------------- /docs/source/tutorial/stubbing.rst: -------------------------------------------------------------------------------- 1 | Stubbing 2 | ========= -------------------------------------------------------------------------------- /docs/source/tutorial/comparators.rst: -------------------------------------------------------------------------------- 1 | Comparators 2 | =========== -------------------------------------------------------------------------------- /docs/source/tutorial/expectations.rst: -------------------------------------------------------------------------------- 1 | Expectations 2 | ============ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.egg-info 4 | .venv 5 | docs/build 6 | -------------------------------------------------------------------------------- /docs/source/future.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | 3 | Future 4 | ================ 5 | 6 | -------------------------------------------------------------------------------- /chai/python2.py: -------------------------------------------------------------------------------- 1 | 2 | def reraise(exc, msg, traceback): 3 | raise exc, msg, traceback 4 | -------------------------------------------------------------------------------- /docs/source/news.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | 3 | News / Changelog 4 | ================ 5 | 6 | 0.1.8 7 | ----- 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.3" 5 | - "2.7" 6 | - "pypy" 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | 3 | Intro 4 | ================================ 5 | 6 | 1. Install 7 | 2. Write TestCase 8 | 3. Profit 9 | 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include LICENSE.txt 3 | include requirements.txt 4 | recursive-include scripts * 5 | recursive-include examples * 6 | recursive-include tests * 7 | -------------------------------------------------------------------------------- /docs/source/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Tutorial 3 | ================================ 4 | 5 | A Tutorial about using chai 6 | 7 | Index: 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | stubbing 13 | mocking 14 | comparators 15 | expectations 16 | -------------------------------------------------------------------------------- /chai/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2011-2017, Agora Games, LLC All rights reserved. 3 | 4 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 5 | """ 6 | 7 | from __future__ import absolute_import 8 | __version__ = '1.1.2' 9 | from .chai import Chai 10 | -------------------------------------------------------------------------------- /chai/_termcolor.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (c) 2011-2017, Agora Games, LLC All rights reserved. 3 | 4 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 5 | ''' 6 | 7 | try: 8 | from termcolor import colored 9 | except ImportError: 10 | def colored(s, *args, **kargs): 11 | return s 12 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Chai, a mocking/stubbing framework for python. 2 | ================================================================ 3 | 4 | Chai offers a very simple and intuitive api for mocking, stubbing and expectation management. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | news 10 | intro 11 | tutorial/index 12 | future -------------------------------------------------------------------------------- /tests/comparator_py2.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import sys 4 | 5 | from chai.comparators import * 6 | 7 | class ComparatorsPy2Test(unittest.TestCase): 8 | 9 | # because this code can't be run in py3 10 | def test_is_a_unicode_test(self): 11 | if sys.version_info.major > 2: 12 | return 13 | comp = IsA(str) 14 | self.assertTrue( comp.test('foo') ) 15 | self.assertFalse( comp.test(u'foo') ) 16 | self.assertFalse( comp.test(bytearray('foo')) ) 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2017, Agora Games, LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of Agora Games nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | requirements = [r for r in map(str.strip, open('requirements.txt').readlines())] 7 | exec([v for v in open('chai/__init__.py') if '__version__' in v][0]) 8 | 9 | setup( 10 | name='chai', 11 | version=__version__, 12 | author='Vitaly Babiy, Aaron Westendorf', 13 | author_email="vbabiy@agoragames.com, aaron@agoragames.com", 14 | packages=['chai'], 15 | url='https://github.com/agoragames/chai', 16 | license='LICENSE.txt', 17 | description="Easy to use mocking, stubbing and spying framework.", 18 | long_description=open('README.rst').read(), 19 | keywords=['python', 'test', 'mock'], 20 | install_requires=requirements, 21 | classifiers=[ 22 | 'Development Status :: 6 - Mature', 23 | 'License :: OSI Approved :: BSD License', 24 | "Intended Audience :: Developers", 25 | "Operating System :: POSIX", 26 | 'Topic :: Software Development :: Libraries', 27 | "Topic :: Software Development :: Libraries :: Python Modules", 28 | "Topic :: Software Development :: Testing", 29 | 'Programming Language :: Python', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.0', 33 | 'Programming Language :: Python :: 3.1', 34 | 'Programming Language :: Python :: 3.2', 35 | ], 36 | test_suite="tests", 37 | ) 38 | -------------------------------------------------------------------------------- /examples/examples.py: -------------------------------------------------------------------------------- 1 | from chai import Chai 2 | import socket 3 | import datetime 4 | 5 | ###################################### 6 | ## Mocking sockets 7 | ###################################### 8 | def connect(): 9 | sock = socket.socket() 10 | sock.bind(('127.0.0.1', 10000)) 11 | return sock.recv(1024) 12 | 13 | def get_host(): 14 | return socket.gethostname() 15 | 16 | class SocketTestCase(Chai): 17 | 18 | def test_socket(self): 19 | mock_socket = self.mock() 20 | self.mock(socket, 'socket') 21 | self.expect(socket.socket.__call__).returns(mock_socket) 22 | 23 | self.expect(mock_socket.bind).args(('127.0.0.1' , 10000)) 24 | self.expect(mock_socket.recv).args(1024).returns("HELLO WORLD") 25 | 26 | self.assert_equals(connect(), "HELLO WORLD") 27 | 28 | def test_get_host(self): 29 | self.mock(socket, 'gethostname') 30 | self.expect(socket.gethostname.__call__).returns("my_host") 31 | self.assert_equals(get_host(), "my_host") 32 | 33 | 34 | ###################################### 35 | ## Mocking datetime.now 36 | ###################################### 37 | def now(): 38 | return datetime.datetime.now() 39 | 40 | class DatetimeTestCase(Chai): 41 | 42 | def test_now(self): 43 | current_now = datetime.datetime.now() # Save for later 44 | 45 | # NOTE: In c python you are not allow to mock built types so we have to mock 46 | # the entire module.moc 47 | mock_datetime = self.mock(datetime, 'datetime') 48 | self.expect(mock_datetime.now).returns(current_now) 49 | 50 | self.assert_equals(now(), current_now) 51 | 52 | if __name__ == '__main__': 53 | import unittest2 54 | unittest2.main() 55 | 56 | -------------------------------------------------------------------------------- /chai/spy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Copyright (c) 2011-2013, Agora Games, LLC All rights reserved. 4 | 5 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 6 | ''' 7 | 8 | from .exception import UnsupportedModifier 9 | from .expectation import Expectation 10 | 11 | 12 | class Spy(Expectation): 13 | 14 | def __init__(self, *args, **kwargs): 15 | super(Spy, self).__init__(*args, **kwargs) 16 | self._side_effect = self._call_spy 17 | 18 | # To support side effects within spies 19 | self._spy_side_effect = False 20 | self._spy_side_effect_args = None 21 | self._spy_side_effect_kwargs = None 22 | self._spy_return = False 23 | 24 | def _call_spy(self, *args, **kwargs): 25 | ''' 26 | Wrapper to call the spied-on function. Operates similar to 27 | Expectation.test. 28 | ''' 29 | if self._spy_side_effect: 30 | if self._spy_side_effect_args or self._spy_side_effect_kwargs: 31 | self._spy_side_effect( 32 | *self._spy_side_effect_args, 33 | **self._spy_side_effect_kwargs) 34 | else: 35 | self._spy_side_effect(*args, **kwargs) 36 | 37 | return_value = self._stub.call_orig(*args, **kwargs) 38 | if self._spy_return: 39 | self._spy_return(return_value) 40 | 41 | return return_value 42 | 43 | def side_effect(self, func, *args, **kwargs): 44 | ''' 45 | Wrap side effects for spies. 46 | ''' 47 | self._spy_side_effect = func 48 | self._spy_side_effect_args = args 49 | self._spy_side_effect_kwargs = kwargs 50 | return self 51 | 52 | def spy_return(self, func): 53 | ''' 54 | Allow spies to react to return values. 55 | ''' 56 | self._spy_return = func 57 | return self 58 | 59 | def returns(self, *args): 60 | ''' 61 | Disable returns for spies. 62 | ''' 63 | raise UnsupportedModifier("Can't use returns on spies") 64 | 65 | def raises(self, *args): 66 | ''' 67 | Disable raises for spies. 68 | ''' 69 | raise UnsupportedModifier("Can't use raises on spies") 70 | -------------------------------------------------------------------------------- /tests/functional_test.py: -------------------------------------------------------------------------------- 1 | # Simple functional tests 2 | import sys 3 | import unittest 4 | 5 | from chai import Chai 6 | from chai.exception import UnexpectedCall 7 | 8 | try: 9 | IS_PYPY = sys.subversion[0]=='PyPy' 10 | except AttributeError: 11 | IS_PYPY = False 12 | 13 | class FunctionalTest(Chai): 14 | 15 | def test_properties_using_attr_name_on_an_instance_only(self): 16 | class Foo(object): 17 | @property 18 | def prop(self): return 3 19 | 20 | foo = Foo() 21 | 22 | expect(foo, 'prop').returns('foo').times(1) 23 | assert_equals( 'foo', foo.prop ) 24 | 25 | expect( stub(foo,'prop').setter ).args( 42 ) 26 | foo.prop = 42 27 | expect( stub(foo,'prop').deleter ) 28 | del foo.prop 29 | 30 | with assert_raises( UnexpectedCall ): 31 | foo.prop 32 | 33 | def test_properties_using_obj_ref_on_a_class_and_using_get_first(self): 34 | class Foo(object): 35 | @property 36 | def prop(self): return 3 37 | 38 | expect(Foo.prop).returns('foo').times(1) 39 | expect(Foo.prop.setter).args(42) 40 | expect(Foo.prop.deleter) 41 | 42 | assert_equals( 'foo', Foo().prop ) 43 | Foo().prop = 42 44 | del Foo().prop 45 | with assert_raises( UnexpectedCall ): 46 | Foo().prop 47 | 48 | @unittest.skipIf(IS_PYPY, "can't stub property setter in PyPy") 49 | def test_properties_using_obj_ref_on_a_class_and_using_set_first(self): 50 | class Foo(object): 51 | @property 52 | def prop(self): return 3 53 | 54 | expect(Foo.prop.setter).args(42) 55 | expect(Foo.prop).returns('foo') 56 | expect(Foo.prop.deleter) 57 | 58 | Foo().prop = 42 59 | assert_equals( 'foo', Foo().prop ) 60 | del Foo().prop 61 | 62 | def test_iterative_expectations(self): 63 | class Foo(object): 64 | def bar(self, x): 65 | return x 66 | f = Foo() 67 | 68 | assert_equals( 3, f.bar(3) ) 69 | 70 | expect( f.bar ) 71 | assert_equals( None, f.bar() ) 72 | 73 | expect( f.bar ).returns( 4 ) 74 | assert_equals( 4, f.bar() ) 75 | assert_equals( 4, f.bar() ) 76 | 77 | expect( f.bar ).returns( 5 ).times(1) 78 | assert_equals( 5, f.bar() ) 79 | with assert_raises( UnexpectedCall ): 80 | f.bar() 81 | 82 | expect( f.bar ).args( 6 ).returns( 7 ) 83 | assert_equals( 7, f.bar(6) ) 84 | 85 | expect( f.bar ).returns( 8 ) 86 | expect( f.bar ).returns( 9 ) 87 | assert_equals( 8, f.bar() ) 88 | assert_equals( 9, f.bar() ) 89 | assert_equals( 9, f.bar() ) 90 | -------------------------------------------------------------------------------- /tests/samples.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains sample classes and situations that we want to test. 3 | """ 4 | 5 | # Can test module import 6 | from collections import deque 7 | 8 | 9 | def mod_func_1(*args, **kwargs): 10 | pass 11 | 12 | 13 | def mod_func_2(*args, **kwargs): 14 | mod_func_1(*args, **kwargs) 15 | 16 | 17 | def mod_func_3(val): 18 | return 3 * val 19 | 20 | 21 | def mod_func_4(val): 22 | return 3 * mod_func_3(val) 23 | 24 | 25 | # For testing when the module has an instance's classmethod bound 26 | class ModInstance(object): 27 | @classmethod 28 | def foo(cls): 29 | pass 30 | 31 | mod_instance = ModInstance() 32 | mod_instance_foo = mod_instance.foo 33 | 34 | 35 | class SampleBase(object): 36 | 37 | a_class_value = 'sample in a jar' 38 | 39 | # Can test __init__ 40 | # Can test variable args 41 | # Can test changing module import 42 | def __init__(self, *args, **kwargs): 43 | self._args = args 44 | self._kwargs = kwargs 45 | self._prop_value = 5 46 | self._deque = deque() 47 | 48 | # Can test a simple property getter 49 | @property 50 | def prop(self): 51 | return self._prop_value 52 | 53 | # Can test property setter 54 | @prop.setter 55 | def set_property(self, val): 56 | self._prop_value = val 57 | 58 | # Can test property deleter 59 | @prop.deleter 60 | def del_property(self): 61 | self._prop_value = None 62 | 63 | @staticmethod 64 | def a_staticmethod(arg): 65 | return str(arg) 66 | 67 | # Can test a class method 68 | @classmethod 69 | def a_classmethod(cls): 70 | return cls.a_class_value 71 | 72 | def add_to_list(self, value): 73 | self._deque.append(value) 74 | return self._deque 75 | 76 | # Can test a bound method 77 | def bound_method(self, arg1, arg2='two'): 78 | self._arg1 = arg1 79 | self._arg2 = arg2 80 | 81 | # Can test that we call another method 82 | def callback_source(self): 83 | self.callback_target() 84 | 85 | def callback_target(self): 86 | self._cb_target = 'called' 87 | 88 | 89 | class SampleChild(SampleBase): 90 | 91 | # Can test calling super 92 | # Can test when bound method is overloaded 93 | def bound_method(self, arg1, arg2='two', arg3='three'): 94 | super(SampleBase, self).bound_method(arg1, arg2) 95 | self._arg3 = arg3 96 | 97 | # Can test overloading classmethod 98 | @classmethod 99 | def a_classmethod(cls): 100 | return 'fixed value' 101 | 102 | # Can test overloading a property 103 | @property 104 | def prop(self): 105 | return 'child property' 106 | -------------------------------------------------------------------------------- /chai/exception.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (c) 2011-2017, Agora Games, LLC All rights reserved. 3 | 4 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 5 | ''' 6 | from __future__ import absolute_import 7 | 8 | import sys 9 | import traceback 10 | 11 | from ._termcolor import colored 12 | 13 | # Refactored from ArgumentsExpectationRule 14 | 15 | 16 | def pretty_format_args(*args, **kwargs): 17 | """ 18 | Take the args, and kwargs that are passed them and format in a 19 | prototype style. 20 | """ 21 | args = list([repr(a) for a in args]) 22 | for key, value in kwargs.items(): 23 | args.append("%s=%s" % (key, repr(value))) 24 | return "(%s)" % ", ".join([a for a in args]) 25 | 26 | 27 | class ChaiException(RuntimeError): 28 | ''' 29 | Base class for an actual error in chai. 30 | ''' 31 | 32 | class UnsupportedStub(ChaiException): 33 | ''' 34 | Can't stub the requested object or attribute. 35 | ''' 36 | 37 | class UnsupportedModifier(ChaiException): 38 | ''' 39 | Can't use the requested modifier. 40 | ''' 41 | 42 | class ChaiAssertion(AssertionError): 43 | ''' 44 | Base class for all assertion errors. 45 | ''' 46 | 47 | 48 | class UnexpectedCall(BaseException): 49 | ''' 50 | Raised when a unexpected call occurs to a stub. 51 | ''' 52 | 53 | def __init__(self, msg=None, prefix=None, suffix=None, call=None, 54 | args=None, kwargs=None, expected_args=None, 55 | expected_kwargs=None): 56 | if msg: 57 | msg = colored('\n\n' + msg.strip(), 'red') 58 | else: 59 | msg = '' 60 | 61 | if prefix: 62 | msg = '\n\n' + prefix.strip() + msg 63 | 64 | if call: 65 | msg += colored('\n\nNo expectation in place for\n', 66 | 'white', attrs=['bold']) 67 | msg += colored(call, 'red') 68 | if args or kwargs: 69 | msg += colored(pretty_format_args(*(args or ()), 70 | **(kwargs or {})), 'red') 71 | if expected_args or expected_kwargs: 72 | msg += colored('\n\nExpected\n', 'white', attrs=['bold']) 73 | msg += colored(call, 'red') 74 | msg += colored(pretty_format_args( 75 | *(expected_args or ()), 76 | **(expected_kwargs or {})), 'red') 77 | 78 | # If handling an exception, add printing of it here. 79 | if sys.exc_info()[0]: 80 | msg += colored('\n\nWhile handling\n', 'white', attrs=['bold']) 81 | msg += colored(''.join( 82 | traceback.format_exception(*sys.exc_info())), 83 | 'red') 84 | 85 | if suffix: 86 | msg = msg + '\n\n' + suffix.strip() 87 | 88 | super(UnexpectedCall, self).__init__(msg) 89 | 90 | 91 | class ExpectationNotSatisfied(ChaiAssertion): 92 | 93 | ''' 94 | Raised when all expectations are not met 95 | ''' 96 | 97 | def __init__(self, *expectations): 98 | self._expectations = expectations 99 | 100 | def __str__(self): 101 | return str("\n".join([str(e) for e in self._expectations])) 102 | -------------------------------------------------------------------------------- /chai/mock.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (c) 2011-2017, Agora Games, LLC All rights reserved. 3 | 4 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 5 | ''' 6 | from .stub import Stub 7 | from .exception import UnexpectedCall 8 | 9 | 10 | class Mock(object): 11 | 12 | ''' 13 | A class where all calls are stubbed. 14 | ''' 15 | 16 | def __init__(self, **kwargs): 17 | for name, value in kwargs.items(): 18 | setattr(self, name, value) 19 | self._name = 'mock' 20 | 21 | # For whatever reason, new-style objects require this method defined before 22 | # any instance is created. Defining it through __getattr__ is not enough. 23 | # This appears to be a bug/feature in new classes where special members, 24 | # or at least __call__, have to defined when the instance is created. 25 | # Also, if it's already defined on the instance, getattr() will return 26 | # the stub but the original method will always be called. Anyway, it's 27 | # all crazy, but that's why the implementation of __call__ is so weird. 28 | def __call__(self, *args, **kwargs): 29 | if isinstance(getattr(self, '__call__'), Stub): 30 | return getattr(self, '__call__')(*args, **kwargs) 31 | 32 | raise UnexpectedCall(call=self._name, args=args, kwargs=kwargs) 33 | 34 | def __getattr__(self, name): 35 | rval = self.__dict__.get(name) 36 | 37 | if not rval or not isinstance(rval, (Stub, Mock)): 38 | rval = Mock() 39 | rval._name = '%s.%s' % (self._name, name) 40 | setattr(self, name, rval) 41 | 42 | return rval 43 | 44 | ### 45 | # Define nonzero so that basic "if :" stanzas will work. 46 | ### 47 | def __nonzero__(self): 48 | if isinstance(getattr(self, '__nonzero__'), Stub): 49 | return getattr(self, '__nonzero__')() 50 | return True 51 | 52 | ### 53 | # Emulate container types, the 99% of cases where we want to mock the 54 | # special methods. They all raise UnexpectedCall unless they're mocked out 55 | # http://docs.python.org/reference/datamodel.html#emulating-container-types 56 | ### 57 | # HACK: it would be nice to abstract this lookup-stub behavior in a 58 | # decorator but that gets in the way of stubbing. Would like to figure 59 | # that out @AW 60 | def __len__(self): 61 | if isinstance(getattr(self, '__len__'), Stub): 62 | return getattr(self, '__len__')() 63 | raise UnexpectedCall(call=self._name + '.__len__') 64 | 65 | def __getitem__(self, key): 66 | if isinstance(getattr(self, '__getitem__'), Stub): 67 | return getattr(self, '__getitem__')(key) 68 | raise UnexpectedCall(call=self._name + '.__getitem__', args=(key,)) 69 | 70 | def __setitem__(self, key, value): 71 | if isinstance(getattr(self, '__setitem__'), Stub): 72 | return getattr(self, '__setitem__')(key, value) 73 | raise UnexpectedCall( 74 | call=self._name + '.__setitem__', args=(key, value)) 75 | 76 | def __delitem__(self, key): 77 | if isinstance(getattr(self, '__delitem__'), Stub): 78 | return getattr(self, '__delitem__')(key) 79 | raise UnexpectedCall(call=self._name + '.__delitem__', args=(key,)) 80 | 81 | def __iter__(self): 82 | if isinstance(getattr(self, '__iter__'), Stub): 83 | return getattr(self, '__iter__')() 84 | raise UnexpectedCall(call=self._name + '.__iter__') 85 | 86 | def __reversed__(self): 87 | if isinstance(getattr(self, '__reversed__'), Stub): 88 | return getattr(self, '__reversed__')() 89 | raise UnexpectedCall(call=self._name + '.__reversed__') 90 | 91 | def __contains__(self, item): 92 | if isinstance(getattr(self, '__contains__'), Stub): 93 | return getattr(self, '__contains__')(item) 94 | raise UnexpectedCall(call=self._name + '.__contains__', args=(item,)) 95 | 96 | ### 97 | # Emulate context managers 98 | # http://docs.python.org/reference/datamodel.html#with-statement-context-managers 99 | ### 100 | def __enter__(self): 101 | if isinstance(getattr(self, '__enter__'), Stub): 102 | return getattr(self, '__enter__')() 103 | raise UnexpectedCall(call=self._name + '.__enter__') 104 | 105 | def __exit__(self, exc_type, exc_value, traceback): 106 | if isinstance(getattr(self, '__exit__'), Stub): 107 | return getattr(self, '__exit__')(exc_type, exc_value, traceback) 108 | raise UnexpectedCall( 109 | call=self._name + '.__exit__', 110 | args=(exc_type, exc_value, traceback)) 111 | -------------------------------------------------------------------------------- /tests/mock_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Tests for the mock object 3 | ''' 4 | 5 | import unittest 6 | import types 7 | 8 | from chai.comparators import * 9 | from chai.mock import Mock 10 | from chai.exception import UnexpectedCall 11 | from chai.stub import stub 12 | 13 | class MockTest(unittest.TestCase): 14 | 15 | # def test_get_attribute_creates_a_new_method(self): 16 | # m = Mock() 17 | # self.assertTrue( isinstance(m.foo, types.MethodType) ) 18 | def test_get_attribute_creates_a_mock_method(self): 19 | m = Mock() 20 | self.assertEquals( 'mock', m._name ) 21 | self.assertTrue( isinstance(m.foo, Mock) ) 22 | self.assertFalse( m.foo is m ) 23 | self.assertEquals( 'mock.foo', m.foo._name ) 24 | 25 | def test_get_attribute_caches_auto_methods(self): 26 | m = Mock() 27 | self.assertTrue( m.foo is m.foo ) 28 | 29 | def test_get_attribute_supports_multiple_depths(self): 30 | m = Mock() 31 | self.assertTrue( isinstance(m.foo.bar, Mock) ) 32 | 33 | def test_get_attribute_auto_method_raises_unexpectedcall_when_unstubbed(self): 34 | m = Mock() 35 | self.assertRaises( UnexpectedCall, m.foo ) 36 | 37 | def test_get_attribute_auto_method_raises_unexpectedcall_when_stubbed(self): 38 | m = Mock() 39 | stub( m.foo ) 40 | self.assertRaises( UnexpectedCall, m.foo ) 41 | 42 | def test_get_attribute_auto_method_not_raises_unexpectedcall_when_stubbed(self): 43 | m = Mock() 44 | stub(m.foo).expect().returns('success') 45 | self.assertEquals( 'success', m.foo() ) 46 | 47 | def test_get_attribute_auto_method_not_raises_unexpectedcall_multiple_depths(self): 48 | m = Mock() 49 | stub(m.foo.bar).expect().returns('success') 50 | self.assertEquals( 'success', m.foo.bar() ) 51 | 52 | def test_get_attribute_does_not_overwrite_existing_attr(self): 53 | m = Mock() 54 | m.foo = 42 55 | self.assertEquals( 42, m.foo ) 56 | 57 | def test_call_raises_unexpectedcall_when_unstubbed(self): 58 | m = Mock() 59 | self.assertRaises( UnexpectedCall, m ) 60 | 61 | def test_call_not_raises_unexpectedcall_when_stubbed(self): 62 | m = Mock() 63 | stub(m).expect().returns('success') 64 | self.assertEquals( 'success', m() ) 65 | 66 | def test_nonzero_returns_true_when_unstubbed(self): 67 | m = Mock() 68 | self.assertTrue( m.__nonzero__() ) 69 | 70 | def test_nonzero_when_stubbed(self): 71 | m = Mock() 72 | stub(m.__nonzero__).expect().returns(False) 73 | self.assertFalse( m.__nonzero__() ) 74 | 75 | def test_container_interface_when_unstubbed(self): 76 | m = Mock() 77 | self.assertRaises( UnexpectedCall, len, m ) 78 | self.assertRaises( UnexpectedCall, m.__getitem__, 'foo' ) 79 | self.assertRaises( UnexpectedCall, m.__setitem__, 'foo', 'bar' ) 80 | self.assertRaises( UnexpectedCall, m.__delitem__, 'foo' ) 81 | self.assertRaises( UnexpectedCall, iter, m ) 82 | self.assertRaises( UnexpectedCall, reversed, m ) 83 | self.assertRaises( UnexpectedCall, m.__contains__, 'foo' ) 84 | self.assertRaises( UnexpectedCall, m.__getslice__, 'i', 'j') 85 | self.assertRaises( UnexpectedCall, m.__setslice__, 'i', 'j', 'foo') 86 | self.assertRaises( UnexpectedCall, m.__delslice__, 'i', 'j') 87 | 88 | def test_container_interface_when_stubbed(self): 89 | m = Mock() 90 | i = iter([1,2,3]) 91 | 92 | stub( m.__len__ ).expect().returns( 42 ) 93 | stub( m.__getitem__ ).expect().args('foo').returns('getitem') 94 | stub( m.__setitem__ ).expect().args('foo', 'bar') 95 | stub( m.__delitem__ ).expect().args('foo') 96 | stub( m.__iter__ ).expect().returns( i ) 97 | stub( m.__reversed__ ).expect().returns( 'backwards' ) 98 | stub( m.__contains__ ).expect().args('foo').returns( True ) 99 | 100 | self.assertEquals( 42, len(m) ) 101 | self.assertEquals( 'getitem', m['foo'] ) 102 | m['foo']='bar' 103 | del m['foo'] 104 | self.assertEquals( i, iter(m) ) 105 | self.assertEquals( 'backwards', reversed(m) ) 106 | self.assertEquals( True, 'foo' in m ) 107 | 108 | def test_context_manager_interface_when_unstubbed(self): 109 | m = Mock() 110 | 111 | def cm(): 112 | with m: 113 | pass 114 | 115 | self.assertRaises( UnexpectedCall, cm ) 116 | 117 | def test_context_manager_interface_when_stubbed(self): 118 | m = Mock() 119 | exc = Exception('fail') 120 | 121 | def cm_noraise(): 122 | with m: pass 123 | 124 | def cm_raise(): 125 | with m: 126 | raise exc 127 | 128 | stub( m.__enter__ ).expect().times(2) 129 | stub( m.__exit__ ).expect().args( None, None, None ) 130 | stub( m.__exit__ ).expect().args( Is(Exception), exc, types.TracebackType ) 131 | 132 | cm_noraise() 133 | self.assertRaises( Exception, cm_raise ) 134 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Chai.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Chai.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Chai" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Chai" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Chai.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Chai.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /tests/chai_test.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | from collections import deque 4 | 5 | from chai import Chai 6 | from chai.chai import ChaiTestType 7 | from chai.mock import Mock 8 | from chai.stub import Stub 9 | from chai.exception import * 10 | from chai.comparators import Comparator 11 | 12 | class CupOf(Chai): 13 | ''' 14 | An example of a subclass on which we can test certain features. 15 | ''' 16 | 17 | class msg_equals(Comparator): 18 | ''' 19 | A Comparator used for check message equality 20 | ''' 21 | #def __init__(self, (key,value)): 22 | def __init__(self, pair): 23 | self._key = pair[0] 24 | self._value = pair[1] 25 | 26 | def test(self, value): 27 | if isinstance(value,dict): 28 | return self._value==value.get(self._key) 29 | return False 30 | 31 | def assert_msg_sent(self, msg): 32 | ''' 33 | Assert that a message was sent and marked that it was handled. 34 | ''' 35 | return msg.get('sent_at') and msg.get('received') 36 | 37 | def test_local_definitions_work_and_are_global(self): 38 | class Foo(object): 39 | def _save_data(self, msg): 40 | pass #dosomethingtottalyawesomewiththismessage 41 | 42 | def do_it(self, msg): 43 | self._save_data(msg) 44 | msg['sent_at'] = 'now' 45 | msg['received'] = 'yes' 46 | 47 | f = Foo() 48 | expect( f._save_data ).args( msg_equals(('target','bob')) ) 49 | 50 | msg = {'target':'bob'} 51 | f.do_it( msg ) 52 | assert_msg_sent( msg ) 53 | 54 | def test_something(self): pass 55 | def runTest(self, *args, **kwargs): pass 56 | 57 | class ChaiTest(unittest.TestCase): 58 | 59 | def test_setup(self): 60 | case = CupOf() 61 | case.setup() 62 | self.assertEquals( deque(), case._stubs ) 63 | self.assertEquals( deque(), case._mocks ) 64 | 65 | def test_teardown_closes_out_stubs_and_mocks(self): 66 | class Stub(object): 67 | calls = 0 68 | def teardown(self): self.calls += 1 69 | 70 | obj = type('test',(object,),{})() 71 | setattr(obj, 'mock1', 'foo') 72 | setattr(obj, 'mock2', 'bar') 73 | 74 | case = CupOf() 75 | stub = Stub() 76 | case._stubs = deque([stub]) 77 | case._mocks = deque([(obj,'mock1','fee'), (obj,'mock2')]) 78 | case.teardown() 79 | self.assertEquals( 1, stub.calls ) 80 | self.assertEquals( 'fee', obj.mock1 ) 81 | self.assertFalse( hasattr(obj, 'mock2') ) 82 | 83 | def test_stub(self): 84 | class Milk(object): 85 | def pour(self): pass 86 | 87 | case = CupOf() 88 | milk = Milk() 89 | case.setup() 90 | self.assertEquals( deque(), case._stubs ) 91 | case.stub( milk.pour ) 92 | self.assertTrue( isinstance(milk.pour, Stub) ) 93 | self.assertEquals( deque([milk.pour]), case._stubs ) 94 | 95 | # Test it's only added once 96 | case.stub( milk, 'pour' ) 97 | self.assertEquals( deque([milk.pour]), case._stubs ) 98 | 99 | def test_expect(self): 100 | class Milk(object): 101 | def pour(self): pass 102 | 103 | case = CupOf() 104 | milk = Milk() 105 | case.setup() 106 | self.assertEquals( deque(), case._stubs ) 107 | case.expect( milk.pour ) 108 | self.assertEquals( deque([milk.pour]), case._stubs ) 109 | 110 | # Test it's only added once 111 | case.expect( milk, 'pour' ) 112 | self.assertEquals( deque([milk.pour]), case._stubs ) 113 | 114 | self.assertEquals( 2, len(milk.pour._expectations) ) 115 | 116 | def test_mock_no_binding(self): 117 | case = CupOf() 118 | case.setup() 119 | 120 | self.assertEquals( deque(), case._mocks ) 121 | mock1 = case.mock() 122 | self.assertTrue( isinstance(mock1, Mock) ) 123 | self.assertEquals( deque(), case._mocks ) 124 | mock2 = case.mock() 125 | self.assertTrue( isinstance(mock2, Mock) ) 126 | self.assertEquals( deque(), case._mocks ) 127 | self.assertNotEqual( mock1, mock2 ) 128 | 129 | def test_mock_with_attr_binding(self): 130 | class Milk(object): 131 | def __init__(self): self._case = [] 132 | def pour(self): return self._case.pop(0) 133 | 134 | case = CupOf() 135 | case.setup() 136 | milk = Milk() 137 | orig_pour = milk.pour 138 | 139 | self.assertEquals( deque(), case._mocks ) 140 | mock1 = case.mock( milk, 'pour' ) 141 | self.assertTrue( isinstance(mock1, Mock) ) 142 | self.assertEquals( deque([(milk,'pour',orig_pour)]), case._mocks ) 143 | mock2 = case.mock( milk, 'pour' ) 144 | self.assertTrue( isinstance(mock2, Mock) ) 145 | self.assertEquals( deque([(milk,'pour',orig_pour),(milk,'pour',mock1)]), case._mocks ) 146 | self.assertNotEqual( mock1, mock2 ) 147 | 148 | mock3 = case.mock( milk, 'foo' ) 149 | self.assertTrue( isinstance(mock3, Mock) ) 150 | self.assertEquals( deque([(milk,'pour',orig_pour),(milk,'pour',mock1),(milk,'foo')]), case._mocks ) 151 | 152 | def test_chai_class_use_metaclass(self): 153 | obj = CupOf() 154 | self.assertTrue(obj, ChaiTestType) 155 | 156 | def test_runs_unmet_expectations(self): 157 | class Stub(object): 158 | unmet_calls = 0 159 | teardown_calls = 0 160 | def unmet_expectations(self): self.unmet_calls += 1; return [] 161 | def teardown(self): self.teardown_calls += 1 162 | 163 | # obj = type('test',(object,),{})() 164 | # setattr(obj, 'mock1', 'foo') 165 | # setattr(obj, 'mock2', 'bar') 166 | 167 | case = CupOf() 168 | stub = Stub() 169 | case._stubs = deque([stub]) 170 | 171 | case.test_local_definitions_work_and_are_global() 172 | self.assertEquals(1, stub.unmet_calls) 173 | self.assertEquals(1, stub.teardown_calls) 174 | 175 | def test_raises_if_unmet_expectations(self): 176 | class Milk(object): 177 | def pour(self): pass 178 | milk = Milk() 179 | case = CupOf() 180 | stub = Stub(milk.pour) 181 | stub.expect() 182 | case._stubs = deque([stub]) 183 | self.assertRaises(ExpectationNotSatisfied, case.test_something) 184 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.1.1 2 | ===== 3 | 4 | Added support for side_effect in spies. 5 | 6 | Fixed spies on classmethods. 7 | 8 | 1.1.0 9 | ===== 10 | 11 | Added support for PyPy 12 | 13 | 1.0.1 14 | ===== 15 | 16 | Fixes tests and unbound method support in Python 3 17 | https://github.com/agoragames/chai/pull/23 @pypingou 18 | https://github.com/agoragames/chai/pull/24 @GraylinKim 19 | 20 | 1.0.0 21 | ===== 22 | 23 | Implemented spies. 24 | 25 | 1.0 release, Chai is largely feature complete. 26 | 27 | 0.4.8 28 | ===== 29 | 30 | Fix PEP8 compliance. 31 | 32 | Fix #18; don't overwrite test module's global namespace where names match 33 | Chai methods. 34 | 35 | 0.4.7 36 | ===== 37 | 38 | Fix reporting on unmet expectations, regression from 0.4.6 release. Thanks 39 | to @ods94065 https://github.com/agoragames/chai/pull/16 40 | 41 | 0.4.6 42 | ===== 43 | 44 | Immediately after running a test, teardown the stubs. This fixes any problems 45 | with exception handling, such as UnexpectedCall, when methods involved in 46 | exception handling, such as `open`, have been stubbed. 47 | 48 | 0.4.5 49 | ===== 50 | 51 | Fixed packaging and test running thanks to @pypingou 52 | 53 | 0.4.4 54 | ===== 55 | 56 | Fixed handling of skipped tests 57 | 58 | 0.4.3 59 | ===== 60 | 61 | Fixed regression in stubbing functions introduced in python 3 changes 62 | 63 | 0.4.2 64 | ===== 65 | 66 | Fixed regression in not raising an AssertionError for python 2 67 | 68 | 0.4.1 69 | ===== 70 | 71 | Fixed UnexpectedCall handling in Python 2. Fixes 72 | https://github.com/agoragames/chai/issues/8. 73 | 74 | 0.4.0 75 | ===== 76 | 77 | Fixed Python 3 support. Fixes https://github.com/agoragames/chai/issues/5. 78 | 79 | 0.3.7 80 | ===== 81 | 82 | Fixed teardown of __new__ on types that overload the method 83 | 84 | 0.3.6 85 | ===== 86 | 87 | Fix logic of raising UnexpectedCall when calling a stub and an unclosed, 88 | non-matching, in-order expectation does not have its counts met 89 | 90 | 0.3.5 91 | ===== 92 | 93 | Only close a non-matching expectation if its counts have been met, else 94 | it's an UnexpectedCall 95 | 96 | 0.3.4 97 | ===== 98 | 99 | Removed termcolor requirement. Both termcolor and unittest2 are optional. 100 | 101 | 0.3.3 102 | ===== 103 | 104 | Removed unittest2 requirement, fixes https://github.com/agoragames/chai/issues/5 105 | 106 | 0.3.2 107 | ===== 108 | 109 | Expectations with a type argument are matched against either an instance 110 | of that type, or the type itself 111 | 112 | 0.3.1 113 | ===== 114 | 115 | Further fixed global namespace manipulation on test class hierarchies 116 | 117 | 0.3.0 118 | ===== 119 | 120 | Improve iterative expectations by not insisting that one knows how many 121 | times an expectation will be called unless explicitly set 122 | 123 | Assume any arguments on an expectation unless explicitly set 124 | 125 | Fixed stubbing properties on an instance 126 | 127 | 0.2.4 128 | ===== 129 | 130 | Fixes bug in stubs on types by preventing secondary initialization 131 | 132 | 0.2.3 133 | ===== 134 | 135 | Mock objects report parameters and method name when raising UnexpectedCall 136 | 137 | Fixed global namespace manipulation on deeply nested Chai subclasses 138 | 139 | 0.2.2 140 | ===== 141 | 142 | UnexpectedCall is now a BaseException which is re-raised as an AssertionError 143 | in Chai metaclass `test_wrapper` method. This decreases the chance that 144 | UnexpectedCall will be caught by the application code being tested. 145 | 146 | 0.2.1 147 | ===== 148 | 149 | Allow variables in returns(), effectively treating the variable as a regex capture. 150 | 151 | 0.2.0 152 | ===== 153 | 154 | Merge pull request to close issue #2 155 | 156 | 0.1.23 157 | ====== 158 | 159 | side_effect is passed the arguments of the expectation if it doesn't define 160 | its own arguments 161 | 162 | 0.1.22 163 | ====== 164 | 165 | side_effect takes arguments after the method to eliminate need for a lambda 166 | 167 | 0.1.21 168 | ====== 169 | 170 | Fixed a bug with (un)stubbing of a module attribute which is really a classmethod 171 | 172 | Added support for `with` statement on expectations, see README for usage 173 | 174 | Fixed (un)stubbing of methods originally defined on `object` class 175 | 176 | 0.1.20 177 | ====== 178 | 179 | Added __eq__ to all comparators so that they can be used inside data structures. 180 | 181 | 0.1.19 182 | ====== 183 | 184 | Fixed failure to teardown stub on __new__ after `expect(type)` 185 | 186 | 0.1.18 187 | ====== 188 | 189 | Simplified stubbing of object creation, so that `expect(type)` can be used as 190 | if it was mocking __init__, but return an object as if it was mocking __new__. 191 | 192 | 0.1.17 193 | ====== 194 | 195 | Fix edge case where an attribute might not be defined on a class when 196 | calling stub(obj, 'attr'). 197 | 198 | 0.1.16 199 | ====== 200 | 201 | Added Mock.__nonzero__ to re-support "if exists" code, which was broken by 202 | the addition of Mock.__len__. 203 | 204 | 0.1.15 205 | ====== 206 | 207 | Added container and context manager interfaces to Mock object 208 | 209 | 0.1.14 210 | ====== 211 | 212 | Added 'any_args' expectation modifier and 'like' comparator. See documentaion 213 | for details. 214 | 215 | 0.1.13 216 | ====== 217 | 218 | Added 'var' comparator for ability to assert that the same argument was used 219 | for one or more expectations, and then to fetch the variable after the call 220 | and run assertions on it. See documentation for details. 221 | 222 | 0.1.12 223 | ====== 224 | 225 | Added teardown() expectation modifier to allow for removing a stub after 226 | it has been met 227 | 228 | 0.1.11 229 | ====== 230 | 231 | Support stubbing of built-in functions and methods 232 | 233 | 0.1.10 234 | ====== 235 | 236 | Support stubbing of functions if they have a module context 237 | 238 | 0.1.9 239 | ===== 240 | 241 | Much nicer output from expectations 242 | 243 | Coloring output 244 | 245 | 0.1.8 246 | ===== 247 | 248 | Support stubbing of get, set and delete on class properties through both 249 | attribute name and object reference 250 | 251 | Improved output on unmet expectations 252 | 253 | Changelog 254 | -------------------------------------------------------------------------------- /tests/comparator_test.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import sys 4 | 5 | from chai.comparators import * 6 | 7 | if sys.version_info.major==2: 8 | from comparator_py2 import * 9 | 10 | class ComparatorsTest(unittest.TestCase): 11 | 12 | def test_build_comparators_builds_equals(self): 13 | comp = build_comparators("str")[0] 14 | self.assertTrue(isinstance(comp, Equals)) 15 | comp = build_comparators(12)[0] 16 | self.assertTrue(isinstance(comp, Equals)) 17 | comp = build_comparators(12.1)[0] 18 | self.assertTrue(isinstance(comp, Equals)) 19 | comp = build_comparators([])[0] 20 | self.assertTrue(isinstance(comp, Equals)) 21 | comp = build_comparators({})[0] 22 | self.assertTrue(isinstance(comp, Equals)) 23 | comp = build_comparators(tuple())[0] 24 | self.assertTrue(isinstance(comp, Equals)) 25 | 26 | def test_build_comparators_is_a(self): 27 | class CustomObject(object): pass 28 | comp = build_comparators(CustomObject)[0] 29 | self.assertTrue(isinstance(comp, Any)) 30 | self.assertTrue( comp.test(CustomObject) ) 31 | self.assertTrue( comp.test(CustomObject()) ) 32 | 33 | def test_build_comparators_passes_comparators(self): 34 | any_comp = Any() 35 | comp = build_comparators(any_comp)[0] 36 | self.assertTrue(comp is any_comp) 37 | 38 | def test_equals(self): 39 | comp = Equals(3) 40 | self.assertTrue( comp.test(3) ) 41 | self.assertTrue( comp.test(3.0) ) 42 | self.assertFalse( comp.test('3') ) 43 | 44 | def test_equals_repr(self): 45 | comp = Equals(3) 46 | self.assertEquals(str(comp), "3") 47 | 48 | def test_eq(self): 49 | comp = Equals(3) 50 | self.assertEquals( comp, 3 ) 51 | 52 | def test_is_a(self): 53 | comp = IsA(str) 54 | self.assertTrue( comp.test('foo') ) 55 | if sys.version_info.major==2: 56 | self.assertFalse( comp.test(bytearray('foo')) ) 57 | else: 58 | self.assertFalse( comp.test(bytearray('foo'.encode('ascii'))) ) 59 | 60 | comp = IsA((str,int)) 61 | self.assertTrue( comp.test('') ) 62 | self.assertTrue( comp.test(42) ) 63 | self.assertFalse( comp.test(3.14) ) 64 | 65 | def test_is_a_repr(self): 66 | comp = IsA(str) 67 | self.assertEquals(repr(comp), "IsA(str)") 68 | 69 | def test_is_a_format_name(self): 70 | comp = IsA(str) 71 | self.assertEquals(comp._format_name(), "str") 72 | comp = IsA((str, list)) 73 | self.assertEquals(comp._format_name(), "['str', 'list']") 74 | 75 | def test_is(self): 76 | class Test(object): 77 | def __eq__(self, other): return True 78 | 79 | obj1 = Test() 80 | obj2 = Test() 81 | comp = Is(obj1) 82 | self.assertEquals( obj1, obj2 ) 83 | self.assertTrue( comp.test(obj1) ) 84 | self.assertFalse( comp.test(obj2) ) 85 | 86 | def test_is_repr(self): 87 | class TestObj(object): 88 | 89 | def __str__(self): 90 | return "An Object" 91 | 92 | obj = TestObj() 93 | self.assertEquals(repr(Is(obj)), "Is(An Object)" ) 94 | 95 | def test_almost_equal(self): 96 | comp = AlmostEqual(3.14159265, 3) 97 | self.assertTrue( comp.test(3.1416) ) 98 | self.assertFalse( comp.test(3.14) ) 99 | 100 | def test_almost_equal_repr(self): 101 | comp = AlmostEqual(3.14159265, 3) 102 | self.assertEquals(repr(comp), "AlmostEqual(value: 3.14159265, places: 3)") 103 | 104 | def test_regex(self): 105 | comp = Regex('[wf][io]{2}') 106 | self.assertTrue( comp.test('fii') ) 107 | self.assertTrue( comp.test('woo') ) 108 | self.assertFalse( comp.test('fuu') ) 109 | 110 | def test_regex_repr(self): 111 | comp = Regex('[wf][io]{2}') 112 | self.assertEquals(repr(comp), "Regex(pattern: [wf][io]{2}, flags: 0)") 113 | 114 | def test_any(self): 115 | comp = Any(1,2.3,str) 116 | self.assertTrue( comp.test(1) ) 117 | self.assertTrue( comp.test(2.3) ) 118 | self.assertFalse( comp.test(4) ) 119 | 120 | def test_any_repr(self): 121 | comp = Any(1,2,3,str) 122 | if sys.version_info.major==2: 123 | self.assertEquals(repr(comp), "Any([1, 2, 3, Any([IsA(str), Is()])])") 124 | else: 125 | self.assertEquals(repr(comp), "Any([1, 2, 3, Any([IsA(str), Is()])])") 126 | 127 | def test_in(self): 128 | comp = In(['foo', 'bar']) 129 | self.assertTrue( comp.test('foo') ) 130 | self.assertTrue( comp.test('bar') ) 131 | self.assertFalse( comp.test('none') ) 132 | 133 | def test_in_repr(self): 134 | comp = In(['foo', 'bar']) 135 | self.assertEqual(repr(comp), "In(['foo', 'bar'])") 136 | 137 | def test_contains(self): 138 | comp = Contains('foo') 139 | self.assertTrue( comp.test('foobar') ) 140 | self.assertTrue( comp.test(['foo','bar']) ) 141 | self.assertTrue( comp.test({'foo':'bar'}) ) 142 | self.assertFalse( comp.test('feet') ) 143 | 144 | def test_contains_repr(self): 145 | comp = Contains("foo") 146 | self.assertEqual(repr(comp), "Contains('foo')") 147 | 148 | def test_all(self): 149 | comp = All(IsA(bytearray), Equals('foo'.encode('ascii'))) 150 | self.assertTrue( comp.test(bytearray('foo'.encode('ascii'))) ) 151 | self.assertFalse( comp.test('foo') ) 152 | self.assertEquals( 'foo'.encode('ascii'), bytearray('foo'.encode('ascii')) ) 153 | 154 | def test_all_repr(self): 155 | comp = All(IsA(bytearray), Equals('foobar')) 156 | self.assertEqual(repr(comp), "All([IsA(bytearray), 'foobar'])") 157 | 158 | def test_not(self): 159 | comp = Not( Any(1,3) ) 160 | self.assertTrue( comp.test(2) ) 161 | self.assertFalse( comp.test(1) ) 162 | self.assertFalse( comp.test(3) ) 163 | 164 | def test_no_repr(self): 165 | comp = Not(Any(1,3)) 166 | self.assertEqual(repr(comp), "Not([Any([1, 3])])") 167 | 168 | def test_function(self): 169 | r = [True,False] 170 | comp = Function(lambda arg: r[arg]) 171 | self.assertTrue( comp.test(0) ) 172 | self.assertFalse( comp.test(1) ) 173 | 174 | def test_function_repr(self): 175 | func = lambda arg: True 176 | comp = Function(func) 177 | self.assertEqual(repr(comp), "Function(%s)" % str(func)) 178 | 179 | def test_ignore(self): 180 | comp = Ignore() 181 | self.assertTrue( comp.test('srsly?') ) 182 | 183 | def test_ignore_repr(self): 184 | comp = Ignore() 185 | self.assertEqual(repr(comp), "Ignore()") 186 | 187 | def test_variable(self): 188 | comp = Variable('foo') 189 | self.assertEquals( 0, len(Variable._cache) ) 190 | self.assertTrue( comp.test('bar') ) 191 | self.assertEquals( 1, len(Variable._cache) ) 192 | self.assertTrue( comp.test('bar') ) 193 | self.assertFalse( comp.test('bar2') ) 194 | 195 | self.assertTrue( Variable('foo').test('bar') ) 196 | self.assertFalse( Variable('foo').test('bar2') ) 197 | self.assertEquals( 1, len(Variable._cache) ) 198 | 199 | self.assertEquals( 'bar', comp.value ) 200 | self.assertEquals( 'bar', Variable('foo').value ) 201 | 202 | v = Variable('foo2') 203 | self.assertEquals( 1, len(Variable._cache) ) 204 | v.test('dog') 205 | self.assertEquals( 'dog', v.value ) 206 | self.assertEquals( 2, len(Variable._cache) ) 207 | 208 | Variable.clear() 209 | self.assertEquals( 0, len(Variable._cache) ) 210 | 211 | def test_variable_repr(self): 212 | v = Variable('foo') 213 | self.assertEquals( repr(v), "Variable('foo')" ) 214 | 215 | def test_like_init(self): 216 | c = Like({'foo':'bar'}) 217 | self.assertEquals( {'foo':'bar'}, c._src ) 218 | 219 | c = Like(['foo', 'bar']) 220 | self.assertEquals( ['foo','bar'], c._src ) 221 | 222 | def test_like_test(self): 223 | c = Like({'foo':'bar'}) 224 | self.assertTrue( c.test({'foo':'bar'}) ) 225 | self.assertTrue( c.test({'foo':'bar', 'cat':'dog'}) ) 226 | self.assertFalse( c.test({'foo':'barf'}) ) 227 | 228 | c = Like(['foo','bar']) 229 | self.assertTrue( c.test(['foo','bar']) ) 230 | self.assertTrue( c.test(['foo','bar','cat','dog']) ) 231 | self.assertFalse( c.test(['foo','barf']) ) 232 | 233 | def test_like_repr(self): 234 | c = Like({'foo':'bar'}) 235 | self.assertEquals( repr(c), "Like({'foo': 'bar'})" ) 236 | -------------------------------------------------------------------------------- /tests/expectation_test.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import types 4 | 5 | from chai.stub import Stub 6 | from chai.expectation import * 7 | from chai.comparators import * 8 | 9 | class ArgumentsExpectationRuleTest(unittest.TestCase): 10 | 11 | def test_validate_with_no_args(self): 12 | r = ArgumentsExpectationRule() 13 | self.assertTrue(r.validate()) 14 | 15 | def test_validate_with_no_args_and_some_params(self): 16 | r = ArgumentsExpectationRule() 17 | self.assertFalse(r.validate('foo')) 18 | self.assertFalse(r.validate(foo='bar')) 19 | 20 | def test_validate_with_args(self): 21 | r = ArgumentsExpectationRule(1,2,2) 22 | self.assertTrue(r.validate(1,2,2)) 23 | 24 | r = ArgumentsExpectationRule(1,1,1) 25 | self.assertFalse(r.validate(2,2,2)) 26 | 27 | def test_validate_with_kwargs(self): 28 | r = ArgumentsExpectationRule(name="vitaly") 29 | self.assertTrue(r.validate(name="vitaly")) 30 | 31 | r = ArgumentsExpectationRule(name="vitaly") 32 | self.assertFalse(r.validate(name="aaron")) 33 | 34 | def test_validate_with_args_and_kwargs(self): 35 | r = ArgumentsExpectationRule(1, name="vitaly") 36 | self.assertTrue(r.validate(1, name="vitaly")) 37 | 38 | r = ArgumentsExpectationRule(1, name="vitaly") 39 | self.assertFalse(r.validate(1, name="aaron")) 40 | 41 | def test_validate_with_args_that_are_different_length(self): 42 | r = ArgumentsExpectationRule(1) 43 | self.assertFalse(r.validate(1,1)) 44 | 45 | def test_validate_with_kwargs_that_are_different(self): 46 | r = ArgumentsExpectationRule(name='vitaly') 47 | self.assertFalse(r.validate(age=837)) 48 | self.assertFalse(r.validate(name='vitaly', age=837)) 49 | 50 | class ExpectationRule(unittest.TestCase): 51 | 52 | def setUp(self): 53 | self.stub = Stub(object) 54 | 55 | def test_default_expectation(self): 56 | exp = Expectation(self.stub) 57 | self.assertFalse(exp.closed()) 58 | self.assertTrue(exp.match()) 59 | 60 | def test_match_with_args(self): 61 | exp = Expectation(self.stub) 62 | exp.args(1, 2 ,3) 63 | 64 | self.assertTrue(exp.match(1,2,3)) 65 | 66 | def test_match_with_kwargs(self): 67 | exp = Expectation(self.stub) 68 | exp.args(name="vitaly") 69 | 70 | self.assertTrue(exp.match(name="vitaly")) 71 | 72 | def test_match_with_both_args_and_kwargs(self): 73 | exp = Expectation(self.stub) 74 | exp.args(1, name="vitaly") 75 | 76 | self.assertTrue(exp.match(1, name="vitaly")) 77 | 78 | def test_match_with_any_args(self): 79 | exp = Expectation(self.stub) 80 | self.assertTrue( exp._any_args ) 81 | self.assertTrue( exp is exp.any_args() ) 82 | self.assertTrue( exp._any_args ) 83 | 84 | self.assertTrue( exp.match() ) 85 | self.assertTrue( exp.match('foo') ) 86 | self.assertTrue( exp.match('bar', hello='world') ) 87 | 88 | def test_times(self): 89 | exp = Expectation(self.stub) 90 | self.assertEquals( exp, exp.times(3) ) 91 | self.assertEquals( 3, exp._min_count ) 92 | self.assertEquals( 3, exp._max_count ) 93 | 94 | def test_at_least_once(self): 95 | exp = Expectation(self.stub) 96 | self.assertEquals( exp, exp.at_least_once() ) 97 | exp.test() 98 | self.assertTrue(exp.closed(with_counts=True)) 99 | 100 | def test_at_least(self): 101 | exp = Expectation(self.stub) 102 | self.assertEquals( exp, exp.at_least(10) ) 103 | for x in range(10): 104 | exp.test() 105 | self.assertTrue(exp.closed(with_counts=True)) 106 | 107 | def test_at_most_once(self): 108 | exp = Expectation(self.stub) 109 | self.assertEquals( exp, exp.args(1).at_most_once() ) 110 | exp.test(1) 111 | self.assertTrue(exp.closed(with_counts=True)) 112 | 113 | def test_at_most(self): 114 | exp = Expectation(self.stub) 115 | self.assertEquals( exp, exp.args(1).at_most(10) ) 116 | for x in range(10): 117 | exp.test(1) 118 | self.assertTrue(exp.closed(with_counts=True)) 119 | 120 | def test_once(self): 121 | exp = Expectation(self.stub) 122 | self.assertEquals( exp, exp.args(1).once() ) 123 | exp.test(1) 124 | self.assertTrue(exp.closed()) 125 | 126 | def test_any_order(self): 127 | exp = Expectation(self.stub) 128 | self.assertEquals( exp, exp.any_order().times(1) ) 129 | self.assertFalse( exp.closed() ) 130 | exp.close() 131 | self.assertFalse( exp.closed() ) 132 | self.assertFalse( exp.closed(with_counts=True) ) 133 | exp.test() 134 | self.assertTrue( exp.closed() ) 135 | self.assertTrue( exp.closed(with_counts=True) ) 136 | 137 | def test_any_order_with_no_max(self): 138 | exp = Expectation(self.stub) 139 | self.assertEquals( exp, exp.any_order().at_least_once() ) 140 | self.assertFalse( exp.closed() ) 141 | exp.close() 142 | self.assertFalse( exp.closed() ) 143 | self.assertFalse( exp.closed(with_counts=True) ) 144 | exp.test() 145 | self.assertFalse( exp.closed() ) 146 | self.assertTrue( exp.closed(with_counts=True) ) 147 | 148 | def test_side_effect(self): 149 | called = [] 150 | def effect(foo=called): 151 | foo.append('foo') 152 | 153 | exp = Expectation(self.stub) 154 | self.assertEquals( exp, exp.side_effect(effect) ) 155 | exp.test() 156 | self.assertEquals( ['foo'], called ) 157 | 158 | def test_side_effect_with_args(self): 159 | called = [] 160 | def effect(a, b=None): 161 | if a=='a' and b=='c': 162 | called.append('foo') 163 | 164 | exp = Expectation(self.stub) 165 | self.assertEquals( exp, exp.side_effect(effect, 'a', b='c') ) 166 | exp.test() 167 | self.assertEquals( ['foo'], called ) 168 | 169 | def test_side_effect_with_passed_args(self): 170 | called = [] 171 | def effect(a, b=None): 172 | if a=='a' and b=='c': 173 | called.append('foo') 174 | 175 | exp = Expectation(self.stub) 176 | exp.args('a', b='c') 177 | self.assertEquals( exp, exp.side_effect(effect) ) 178 | exp.test('a', b='c') 179 | self.assertEquals( ['foo'], called ) 180 | 181 | def test_side_effect_with_an_exception(self): 182 | called = [] 183 | def effect(foo=called): 184 | foo.append('foo') 185 | class Zono(Exception): pass 186 | 187 | exp = Expectation(self.stub) 188 | self.assertEquals( exp, exp.side_effect(effect).raises(Zono) ) 189 | self.assertRaises( Zono, exp.test ) 190 | self.assertEquals( ['foo'], called ) 191 | 192 | def test_teardown(self): 193 | called = [] 194 | class notastub(object): 195 | expectations = [] 196 | def teardown(self, foo=called): 197 | foo.append( 'foo' ) 198 | 199 | exp = Expectation( notastub() ) 200 | exp.teardown() 201 | exp.test() 202 | self.assertEquals( ['foo'], called ) 203 | 204 | def test_closed(self): 205 | exp = Expectation(self.stub) 206 | exp.args(1).times(1) 207 | exp.test(1) 208 | self.assertTrue(exp.closed()) 209 | 210 | def test_closed_with_count(self): 211 | exp = Expectation(self.stub) 212 | exp.args(1).at_least(1) 213 | exp.test(1) 214 | self.assertTrue(exp.closed(with_counts=True)) 215 | 216 | def test_close(self): 217 | exp = Expectation(self.stub) 218 | exp.test() 219 | exp.close() 220 | self.assertTrue(exp.closed()) 221 | 222 | def test_return_value_with_value(self): 223 | exp = Expectation(self.stub) 224 | exp.returns(123) 225 | 226 | self.assertEquals(exp.test(), 123) 227 | 228 | def test_return_value_with_variable(self): 229 | exp = Expectation(self.stub) 230 | var = Variable('test') 231 | Variable._cache['test'] = 123 232 | exp.returns( var ) 233 | 234 | self.assertEquals(exp.test(), 123) 235 | Variable.clear() 236 | 237 | def test_return_value_with_variable_in_tuple(self): 238 | exp = Expectation(self.stub) 239 | var = Variable('test') 240 | Variable._cache['test'] = 123 241 | exp.returns( (var,'foo') ) 242 | 243 | self.assertEquals(exp.test(), (123,'foo')) 244 | Variable.clear() 245 | 246 | def test_with_returns_return_value(self): 247 | exp = Expectation(self.stub) 248 | with exp.returns(123) as num: 249 | self.assertEquals(num, 123) 250 | 251 | def test_with_raises_exceptions(self): 252 | exp = Expectation(self.stub) 253 | 254 | # pre-2.7 compatible 255 | def foo(): 256 | with exp.returns(123) as num: 257 | raise Exception("FAIL!") 258 | self.assertRaises(Exception, foo) 259 | 260 | def test_return_value_with_exception_class(self): 261 | class CustomException(Exception): pass 262 | 263 | exp = Expectation(self.stub) 264 | self.assertEquals( exp, exp.raises(CustomException) ) 265 | 266 | self.assertRaises(CustomException, exp.test) 267 | 268 | def test_return_value_with_exception_instance(self): 269 | class CustomException(Exception): pass 270 | 271 | exp = Expectation(self.stub) 272 | self.assertEquals( exp, exp.raises(CustomException()) ) 273 | 274 | self.assertRaises(CustomException, exp.test) 275 | 276 | -------------------------------------------------------------------------------- /chai/comparators.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (c) 2011-2017, Agora Games, LLC All rights reserved. 3 | 4 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 5 | ''' 6 | import re 7 | 8 | 9 | def build_comparators(*values_or_types): 10 | ''' 11 | All of the comparators that can be used for arguments. 12 | ''' 13 | comparators = [] 14 | for item in values_or_types: 15 | if isinstance(item, Comparator): 16 | comparators.append(item) 17 | elif isinstance(item, type): 18 | # If you are passing around a type you will have to build a Equals 19 | # comparator 20 | comparators.append(Any(IsA(item), Is(item))) 21 | else: 22 | comparators.append(Equals(item)) 23 | return comparators 24 | 25 | 26 | class Comparator(object): 27 | 28 | ''' 29 | Base class of all comparators, used for type testing 30 | ''' 31 | 32 | def __eq__(self, value): 33 | return self.test(value) 34 | 35 | 36 | class Equals(Comparator): 37 | 38 | ''' 39 | Simplest comparator. 40 | ''' 41 | 42 | def __init__(self, value): 43 | self._value = value 44 | 45 | def test(self, value): 46 | return self._value == value 47 | 48 | def __repr__(self): 49 | return repr(self._value) 50 | __str__ = __repr__ 51 | 52 | 53 | class Length(Comparator): 54 | 55 | ''' 56 | Compare the length of the argument. 57 | ''' 58 | 59 | def __init__(self, value): 60 | self._value = value 61 | 62 | def test(self, value): 63 | if isinstance(self._value, int): 64 | return len(value) == self._value 65 | return len(value) in self._value 66 | 67 | def __repr__(self): 68 | return repr(self._value) 69 | __str__ = __repr__ 70 | 71 | 72 | class IsA(Comparator): 73 | 74 | ''' 75 | Test to see if a value is an instance of something. Arguments match 76 | isinstance 77 | ''' 78 | 79 | def __init__(self, types): 80 | self._types = types 81 | 82 | def test(self, value): 83 | return isinstance(value, self._types) 84 | 85 | def _format_name(self): 86 | if isinstance(self._types, type): 87 | return self._types.__name__ 88 | else: 89 | return str([o.__name__ for o in self._types]) 90 | 91 | def __repr__(self): 92 | return "IsA(%s)" % (self._format_name()) 93 | __str__ = __repr__ 94 | 95 | 96 | class Is(Comparator): 97 | 98 | ''' 99 | Checks for identity not equality 100 | ''' 101 | 102 | def __init__(self, obj): 103 | self._obj = obj 104 | 105 | def test(self, value): 106 | return self._obj is value 107 | 108 | def __repr__(self): 109 | return "Is(%s)" % (str(self._obj)) 110 | __str__ = __repr__ 111 | 112 | 113 | class AlmostEqual(Comparator): 114 | 115 | ''' 116 | Compare a float value to n number of palces 117 | ''' 118 | 119 | def __init__(self, float_value, places=7): 120 | self._float_value = float_value 121 | self._places = places 122 | 123 | def test(self, value): 124 | return round(value - self._float_value, self._places) == 0 125 | 126 | def __repr__(self): 127 | return "AlmostEqual(value: %s, places: %d)" % ( 128 | str(self._float_value), self._places) 129 | __str__ = __repr__ 130 | 131 | 132 | class Regex(Comparator): 133 | 134 | ''' 135 | Checks to see if a string matches a regex 136 | ''' 137 | 138 | def __init__(self, pattern, flags=0): 139 | self._pattern = pattern 140 | self._flags = flags 141 | self._regex = re.compile(pattern) 142 | 143 | def test(self, value): 144 | return self._regex.search(value) is not None 145 | 146 | def __repr__(self): 147 | return "Regex(pattern: %s, flags: %s)" % (self._pattern, self._flags) 148 | __str__ = __repr__ 149 | 150 | 151 | class Any(Comparator): 152 | 153 | ''' 154 | Test to see if any comparator matches 155 | ''' 156 | 157 | def __init__(self, *comparators): 158 | self._comparators = build_comparators(*comparators) 159 | 160 | def test(self, value): 161 | for comp in self._comparators: 162 | if comp.test(value): 163 | return True 164 | return False 165 | 166 | def __repr__(self): 167 | return "Any(%s)" % str(self._comparators) 168 | __str__ = __repr__ 169 | 170 | 171 | class In(Comparator): 172 | 173 | ''' 174 | Test if a key is in a list or dict 175 | ''' 176 | 177 | def __init__(self, hay_stack): 178 | self._hay_stack = hay_stack 179 | 180 | def test(self, needle): 181 | return needle in self._hay_stack 182 | 183 | def __repr__(self): 184 | return "In(%s)" % (str(self._hay_stack)) 185 | __str__ = __repr__ 186 | 187 | 188 | class Contains(Comparator): 189 | 190 | ''' 191 | Test if a key is in a list or dict 192 | ''' 193 | 194 | def __init__(self, needle): 195 | self._needle = needle 196 | 197 | def test(self, hay_stack): 198 | return self._needle in hay_stack 199 | 200 | def __repr__(self): 201 | return "Contains('%s')" % (str(self._needle)) 202 | __str__ = __repr__ 203 | 204 | 205 | class All(Comparator): 206 | 207 | ''' 208 | Test to see if all comparators match 209 | ''' 210 | 211 | def __init__(self, *comparators): 212 | self._comparators = build_comparators(*comparators) 213 | 214 | def test(self, value): 215 | for comp in self._comparators: 216 | if not comp.test(value): 217 | return False 218 | return True 219 | 220 | def __repr__(self): 221 | return "All(%s)" % (self._comparators) 222 | __str__ = __repr__ 223 | 224 | 225 | class Not(Comparator): 226 | 227 | ''' 228 | Return the opposite of a comparator 229 | ''' 230 | 231 | def __init__(self, *comparators): 232 | self._comparators = build_comparators(*comparators) 233 | 234 | def test(self, value): 235 | return all([not c.test(value) for c in self._comparators]) 236 | 237 | def __repr__(self): 238 | return "Not(%s)" % (repr(self._comparators)) 239 | __str__ = __repr__ 240 | 241 | 242 | class Function(Comparator): 243 | 244 | ''' 245 | Call a func to compare the values 246 | ''' 247 | 248 | def __init__(self, func): 249 | self._func = func 250 | 251 | def test(self, value): 252 | return self._func(value) 253 | 254 | def __repr__(self): 255 | return "Function(%s)" % (str(self._func)) 256 | __str__ = __repr__ 257 | 258 | 259 | class Ignore(Comparator): 260 | 261 | ''' 262 | Igore this argument 263 | ''' 264 | 265 | def test(self, value): 266 | return True 267 | 268 | def __repr__(self): 269 | return "Ignore()" 270 | __str__ = __repr__ 271 | 272 | 273 | class Variable(Comparator): 274 | 275 | ''' 276 | A mechanism for tracking variables and their values. 277 | ''' 278 | _cache = {} 279 | 280 | @classmethod 281 | def clear(self): 282 | ''' 283 | Delete all cached values. Should only be used by the test suite. 284 | ''' 285 | self._cache.clear() 286 | 287 | def __init__(self, name): 288 | self._name = name 289 | 290 | @property 291 | def value(self): 292 | try: 293 | return self._cache[self._name] 294 | except KeyError: 295 | raise ValueError("no value '%s'" % (self._name)) 296 | 297 | def test(self, value): 298 | try: 299 | return self._cache[self._name] == value 300 | except KeyError: 301 | self._cache[self._name] = value 302 | return True 303 | 304 | def __repr__(self): 305 | return "Variable('%s')" % (self._name) 306 | __str__ = __repr__ 307 | 308 | 309 | class Like(Comparator): 310 | 311 | ''' 312 | A comparator that will assert that fields of a container look like 313 | another. 314 | ''' 315 | 316 | def __init__(self, src): 317 | # This might have to change to support more iterable types 318 | if not isinstance(src, (dict, set, list, tuple)): 319 | raise ValueError( 320 | "Like comparator only implemented for basic container types") 321 | self._src = src 322 | 323 | def test(self, value): 324 | # This might need to change so that the ctor arg can be a list, but 325 | # any iterable type can be tested. 326 | if not isinstance(value, type(self._src)): 327 | return False 328 | 329 | rval = True 330 | if isinstance(self._src, dict): 331 | for k, v in self._src.items(): 332 | rval = rval and value.get(k) == v 333 | 334 | else: 335 | for item in self._src: 336 | rval = rval and item in value 337 | 338 | return rval 339 | 340 | def __repr__(self): 341 | return "Like(%s)" % (str(self._src)) 342 | __str__ = __repr__ 343 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Chai documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Jun 5 20:51:42 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Chai' 44 | copyright = u'2011, Vitaly Babiy, Aaron Westendorf' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1.8' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1.8' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = [] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'nature' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'Chaidoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'Chai.tex', u'Chai Documentation', 182 | u'Vitaly Babiy, Aaron Westendorf', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'chai', u'Chai Documentation', 215 | [u'Vitaly Babiy, Aaron Westendorf'], 1) 216 | ] 217 | 218 | 219 | # -- Options for Epub output --------------------------------------------------- 220 | 221 | # Bibliographic Dublin Core info. 222 | epub_title = u'Chai' 223 | epub_author = u'Vitaly Babiy, Aaron Westendorf' 224 | epub_publisher = u'Vitaly Babiy, Aaron Westendorf' 225 | epub_copyright = u'2011, Vitaly Babiy, Aaron Westendorf' 226 | 227 | # The language of the text. It defaults to the language option 228 | # or en if the language is not set. 229 | #epub_language = '' 230 | 231 | # The scheme of the identifier. Typical schemes are ISBN or URL. 232 | #epub_scheme = '' 233 | 234 | # The unique identifier of the text. This can be a ISBN number 235 | # or the project homepage. 236 | #epub_identifier = '' 237 | 238 | # A unique identification for the text. 239 | #epub_uid = '' 240 | 241 | # HTML files that should be inserted before the pages created by sphinx. 242 | # The format is a list of tuples containing the path and title. 243 | #epub_pre_files = [] 244 | 245 | # HTML files shat should be inserted after the pages created by sphinx. 246 | # The format is a list of tuples containing the path and title. 247 | #epub_post_files = [] 248 | 249 | # A list of files that should not be packed into the epub file. 250 | #epub_exclude_files = [] 251 | 252 | # The depth of the table of contents in toc.ncx. 253 | #epub_tocdepth = 3 254 | 255 | # Allow duplicate toc entries. 256 | #epub_tocdup = True 257 | 258 | 259 | # Example configuration for intersphinx: refer to the Python standard library. 260 | intersphinx_mapping = {'http://docs.python.org/': None} 261 | -------------------------------------------------------------------------------- /chai/chai.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (c) 2011-2017, Agora Games, LLC All rights reserved. 3 | 4 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 5 | ''' 6 | from __future__ import absolute_import 7 | 8 | try: 9 | import unittest2 10 | unittest = unittest2 11 | except ImportError: 12 | import unittest 13 | 14 | import re 15 | import sys 16 | import inspect 17 | import traceback 18 | from collections import deque 19 | 20 | from .exception import * 21 | from .mock import Mock 22 | from .stub import stub 23 | from .comparators import * 24 | 25 | 26 | class ChaiTestType(type): 27 | 28 | """ 29 | Metaclass used to wrap all test methods to make sure the 30 | assert_expectations in the correct context. 31 | """ 32 | 33 | def __init__(cls, name, bases, d): 34 | type.__init__(cls, name, bases, d) 35 | 36 | # also get all the attributes from the base classes to account 37 | # for a case when test class is not the immediate child of Chai 38 | # also alias all the cAmElCaSe methods to more helpful ones 39 | for base in bases: 40 | for attr_name in dir(base): 41 | d[attr_name] = getattr(base, attr_name) 42 | if attr_name.startswith('assert') and attr_name != 'assert_': 43 | pieces = ['assert'] + \ 44 | re.findall('[A-Z][a-z]+', attr_name[5:]) 45 | name = '_'.join([s.lower() for s in pieces]) 46 | d[name] = getattr(base, attr_name) 47 | setattr(cls, name, getattr(base, attr_name)) 48 | 49 | for func_name, func in d.items(): 50 | if func_name.startswith('test') and callable(func): 51 | setattr(cls, func_name, ChaiTestType.test_wrapper(cls, func)) 52 | 53 | @staticmethod 54 | def test_wrapper(cls, func): 55 | """ 56 | Wraps a test method, when that test method has completed it 57 | calls assert_expectations on the stub. This is to avoid getting to 58 | exceptions about the same error. 59 | """ 60 | 61 | def wrapper(self, *args, **kwargs): 62 | try: 63 | func(self, *args, **kwargs) 64 | except UnexpectedCall as e: 65 | # if this is not python3, use python2 syntax 66 | if not hasattr(e, '__traceback__'): 67 | from .python2 import reraise 68 | reraise( 69 | AssertionError, '\n\n' + str(e), sys.exc_info()[-1]) 70 | exc = AssertionError('\n\n' + str(e)) 71 | setattr(exc, '__traceback__', sys.exc_info()[-1]) 72 | raise exc 73 | finally: 74 | # Teardown all stubs so that if anyone stubbed methods that 75 | # would be called during exception handling (e.g. "open"), 76 | # the original method is used. Without, recursion limits are 77 | # common with little insight into what went wrong. 78 | exceptions = [] 79 | try: 80 | for s in self._stubs: 81 | # Make sure we collect any unmet expectations before 82 | # teardown. 83 | exceptions.extend(s.unmet_expectations()) 84 | s.teardown() 85 | except: 86 | # A rare case where this is about the best that can be 87 | # done, as we don't want to supersede the actual 88 | # exception if there is one. 89 | traceback.print_exc() 90 | 91 | if exceptions: 92 | raise ExpectationNotSatisfied(*exceptions) 93 | 94 | wrapper.__name__ = func.__name__ 95 | wrapper.__doc__ = func.__doc__ 96 | wrapper.__module__ = func.__module__ 97 | wrapper.__wrapped__ = func 98 | if getattr(func, '__unittest_skip__', False): 99 | wrapper.__unittest_skip__ = True 100 | wrapper.__unittest_skip_why__ = func.__unittest_skip_why__ 101 | return wrapper 102 | 103 | 104 | class ChaiBase(unittest.TestCase): 105 | ''' 106 | Base class for all tests 107 | ''' 108 | 109 | # Load in the comparators 110 | equals = Equals 111 | almost_equals = AlmostEqual 112 | length = Length 113 | is_a = IsA 114 | is_arg = Is 115 | any_of = Any 116 | all_of = All 117 | not_of = Not 118 | matches = Regex 119 | func = Function 120 | ignore_arg = Ignore 121 | ignore = Ignore 122 | in_arg = In 123 | contains = Contains 124 | var = Variable 125 | like = Like 126 | 127 | def setUp(self): 128 | super(ChaiBase, self).setUp() 129 | 130 | # Setup stub tracking 131 | self._stubs = deque() 132 | 133 | # Setup mock tracking 134 | self._mocks = deque() 135 | 136 | # Try to load this into the module that the test case is defined in, so 137 | # that 'self.' can be removed. This has to be done at the start of the 138 | # test because we need the reference to be correct at the time of test 139 | # run, not when the class is defined or an instance is created. Walks 140 | # through the method resolution order to set it on every module for 141 | # Chai subclasses to handle when tests are defined in subclasses. 142 | for cls in inspect.getmro(self.__class__): 143 | if cls.__module__.startswith('chai'): 144 | break 145 | mod = sys.modules[cls.__module__] 146 | for attr in dir(cls): 147 | if hasattr(mod, attr): 148 | continue 149 | if attr.startswith('assert'): 150 | setattr(mod, attr, getattr(self, attr)) 151 | elif isinstance(getattr(self, attr), type) and \ 152 | issubclass(getattr(self, attr), Comparator): 153 | setattr(mod, attr, getattr(self, attr)) 154 | if not hasattr(mod, 'stub'): 155 | setattr(mod, 'stub', self.stub) 156 | if not hasattr(mod, 'expect'): 157 | setattr(mod, 'expect', self.expect) 158 | if not hasattr(mod, 'spy'): 159 | setattr(mod, 'spy', self.spy) 160 | if not hasattr(mod, 'mock'): 161 | setattr(mod, 'mock', self.mock) 162 | 163 | # Because cAmElCaSe sucks 164 | setup = setUp 165 | 166 | def tearDown(self): 167 | super(ChaiBase, self).tearDown() 168 | 169 | for cls in inspect.getmro(self.__class__): 170 | if cls.__module__.startswith('chai'): 171 | break 172 | mod = sys.modules[cls.__module__] 173 | 174 | if getattr(mod, 'stub', None) == self.stub: 175 | delattr(mod, 'stub') 176 | if getattr(mod, 'expect', None) == self.expect: 177 | delattr(mod, 'expect') 178 | if getattr(mod, 'spy', None) == self.spy: 179 | delattr(mod, 'spy') 180 | if getattr(mod, 'mock', None) == self.mock: 181 | delattr(mod, 'mock') 182 | 183 | # Docs insist that this will be called no matter what happens in 184 | # runTest(), so this should be a safe spot to unstub everything. 185 | # Even with teardown at the end of test_wrapper, tear down here in 186 | # case the test was skipped or there was otherwise a problem with 187 | # that test. 188 | while len(self._stubs): 189 | stub = self._stubs.popleft() 190 | stub.teardown() # Teardown the reset of the stub 191 | 192 | # Do the mocks in reverse order in the rare case someone called 193 | # mock(obj,attr) twice. 194 | while len(self._mocks): 195 | mock = self._mocks.pop() 196 | if len(mock) == 2: 197 | delattr(mock[0], mock[1]) 198 | else: 199 | setattr(mock[0], mock[1], mock[2]) 200 | 201 | # Clear out any cached variables 202 | Variable.clear() 203 | 204 | # Because cAmElCaSe sucks 205 | teardown = tearDown 206 | 207 | def stub(self, obj, attr=None): 208 | ''' 209 | Stub an object. If attr is not None, will attempt to stub that 210 | attribute on the object. Only required for modules and other rare 211 | cases where we can't determine the binding from the object. 212 | ''' 213 | s = stub(obj, attr) 214 | if s not in self._stubs: 215 | self._stubs.append(s) 216 | return s 217 | 218 | def expect(self, obj, attr=None): 219 | ''' 220 | Open and return an expectation on an object. Will automatically create 221 | a stub for the object. See stub documentation for argument information. 222 | ''' 223 | return self.stub(obj, attr).expect() 224 | 225 | def spy(self, obj, attr=None): 226 | ''' 227 | Open and return a spy on an object. Will automatically create a stub 228 | for the object. See stub documentation for argument information. 229 | ''' 230 | return self.stub(obj, attr).spy() 231 | 232 | def mock(self, obj=None, attr=None, **kwargs): 233 | ''' 234 | Return a mock object. 235 | ''' 236 | rval = Mock(**kwargs) 237 | if obj is not None and attr is not None: 238 | rval._object = obj 239 | rval._attr = attr 240 | 241 | if hasattr(obj, attr): 242 | orig = getattr(obj, attr) 243 | self._mocks.append((obj, attr, orig)) 244 | setattr(obj, attr, rval) 245 | else: 246 | self._mocks.append((obj, attr)) 247 | setattr(obj, attr, rval) 248 | return rval 249 | 250 | 251 | Chai = ChaiTestType('Chai', (ChaiBase,), {}) 252 | -------------------------------------------------------------------------------- /chai/expectation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Copyright (c) 2011-2013, Agora Games, LLC All rights reserved. 4 | 5 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 6 | ''' 7 | 8 | import inspect 9 | 10 | from .comparators import * 11 | from .exception import * 12 | from ._termcolor import colored 13 | 14 | 15 | class ExpectationRule(object): 16 | 17 | def __init__(self, *args, **kwargs): 18 | self._passed = False 19 | 20 | def validate(self, *args, **kwargs): 21 | raise NotImplementedError("Must be implemented by subclasses") 22 | 23 | 24 | class ArgumentsExpectationRule(ExpectationRule): 25 | 26 | def __init__(self, *args, **kwargs): 27 | super(ArgumentsExpectationRule, self).__init__(*args, **kwargs) 28 | self.set_args(*args, **kwargs) 29 | 30 | def set_args(self, *args, **kwargs): 31 | self.args = [] 32 | self.kwargs = {} 33 | 34 | # Convert all of the arguments to comparators 35 | self.args = build_comparators(*args) 36 | self.kwargs = dict([(k, build_comparators(v)[0]) 37 | for k, v in kwargs.items()]) 38 | 39 | def validate(self, *args, **kwargs): 40 | self.in_args = args[:] 41 | self.in_kwargs = kwargs.copy() 42 | 43 | # First just check that the number of arguments is the same or 44 | # different 45 | if len(args) != len(self.args) or len(kwargs) != len(self.kwargs): 46 | self._passed = False 47 | return False 48 | 49 | for x in range(len(self.args)): 50 | if not self.args[x].test(args[x]): 51 | self._passed = False 52 | return False 53 | 54 | for arg_name, arg_test in self.kwargs.items(): 55 | try: 56 | value = kwargs.pop(arg_name) 57 | except KeyError: 58 | self._passed = False 59 | return False 60 | if not arg_test.test(value): 61 | self._passed = False 62 | return False 63 | 64 | # If there are arguments left over, is error 65 | if len(kwargs): 66 | self._passed = False 67 | return False 68 | 69 | self._passed = True 70 | return self._passed 71 | 72 | @classmethod 73 | def pretty_format_args(self, *args, **kwargs): 74 | """ 75 | Take the args, and kwargs that are passed them and format in a 76 | prototype style. 77 | """ 78 | args = list([repr(a) for a in args]) 79 | for key, value in kwargs.items(): 80 | args.append("%s=%s" % (key, repr(value))) 81 | 82 | return "(%s)" % ", ".join([a for a in args]) 83 | 84 | def __str__(self): 85 | if hasattr(self, 'in_args') and hasattr(self, 'in_kwargs'): 86 | return "\tExpected: %s\n\t\t Used: %s" % \ 87 | (self.pretty_format_args(*self.args, **self.kwargs), 88 | self.pretty_format_args(*self.in_args, **self.in_kwargs)) 89 | 90 | return "\tExpected: %s" % \ 91 | (self.pretty_format_args(*self.args, **self.kwargs)) 92 | 93 | 94 | class Expectation(object): 95 | 96 | ''' 97 | Encapsulate an expectation. 98 | ''' 99 | 100 | def __init__(self, stub): 101 | self._met = False 102 | self._stub = stub 103 | self._arguments_rule = ArgumentsExpectationRule() 104 | self._raises = None 105 | self._returns = None 106 | self._max_count = None 107 | self._min_count = 1 108 | self._counts_defined = False 109 | self._run_count = 0 110 | self._any_order = False 111 | self._side_effect = False 112 | self._side_effect_args = None 113 | self._side_effect_kwargs = None 114 | self._teardown = False 115 | self._any_args = True 116 | 117 | # If the last expectation has no counts defined yet, set it to the 118 | # run count if it's already been used, else set it to 1 just like 119 | # the original implementation. This makes iterative testing much 120 | # simpler without needing to know ahead of time exactly how many 121 | # times an expectation will be called. 122 | prev_expect = None if not stub.expectations else stub.expectations[-1] 123 | if prev_expect and not prev_expect._counts_defined: 124 | if prev_expect._run_count: 125 | # Close immediately 126 | prev_expect._met = True 127 | prev_expect._max_count = prev_expect._run_count 128 | else: 129 | prev_expect._max_count = prev_expect._min_count 130 | 131 | # Support expectations as context managers. See 132 | # https://github.com/agoragames/chai/issues/1 133 | def __enter__(self): 134 | return self._returns 135 | 136 | def __exit__(*args): 137 | pass 138 | 139 | def args(self, *args, **kwargs): 140 | """ 141 | Creates a ArgumentsExpectationRule and adds it to the expectation 142 | """ 143 | self._any_args = False 144 | self._arguments_rule.set_args(*args, **kwargs) 145 | return self 146 | 147 | def any_args(self): 148 | ''' 149 | Accept any arguments passed to this call. 150 | ''' 151 | self._any_args = True 152 | return self 153 | 154 | def returns(self, value): 155 | """ 156 | What this expectation should return 157 | """ 158 | self._returns = value 159 | return self 160 | 161 | def raises(self, exception): 162 | """ 163 | Adds a raises to the expectation, this will be raised when the 164 | expectation is met. 165 | 166 | This can be either the exception class or instance of a exception. 167 | """ 168 | self._raises = exception 169 | return self 170 | 171 | def times(self, count): 172 | self._min_count = self._max_count = count 173 | self._counts_defined = True 174 | return self 175 | 176 | def at_least(self, min_count): 177 | self._min_count = min_count 178 | self._max_count = None 179 | self._counts_defined = True 180 | return self 181 | 182 | def at_least_once(self): 183 | self.at_least(1) 184 | self._counts_defined = True 185 | return self 186 | 187 | def at_most(self, max_count): 188 | self._max_count = max_count 189 | self._counts_defined = True 190 | return self 191 | 192 | def at_most_once(self): 193 | self.at_most(1) 194 | self._counts_defined = True 195 | return self 196 | 197 | def once(self): 198 | self._min_count = 1 199 | self._max_count = 1 200 | self._counts_defined = True 201 | return self 202 | 203 | def any_order(self): 204 | self._any_order = True 205 | return self 206 | 207 | def is_any_order(self): 208 | return self._any_order 209 | 210 | def side_effect(self, func, *args, **kwargs): 211 | self._side_effect = func 212 | self._side_effect_args = args 213 | self._side_effect_kwargs = kwargs 214 | return self 215 | 216 | def teardown(self): 217 | self._teardown = True 218 | 219 | # If counts have not been defined yet, then there's an implied use case 220 | # here where once the expectation has been run, it should be torn down, 221 | # i.e. max_count is same as min_count, i.e. 1 222 | if not self._counts_defined: 223 | self._max_count = self._min_count 224 | return self 225 | 226 | def return_value(self): 227 | """ 228 | Returns the value for this expectation or raises the proper exception. 229 | """ 230 | if self._raises: 231 | # Handle exceptions 232 | if inspect.isclass(self._raises): 233 | raise self._raises() 234 | else: 235 | raise self._raises 236 | else: 237 | if isinstance(self._returns, tuple): 238 | return tuple([x.value if isinstance(x, Variable) 239 | else x for x in self._returns]) 240 | return self._returns.value if isinstance(self._returns, Variable) \ 241 | else self._returns 242 | 243 | def close(self, *args, **kwargs): 244 | ''' 245 | Mark this expectation as closed. It will no longer be used for matches. 246 | ''' 247 | # If any_order, then this effectively is never closed. The 248 | # Stub.__call__ will just bypass it when it doesn't match. If there 249 | # is a strict count it will also be bypassed, but if there's just a 250 | # min set up, then it'll effectively stay open and catch any matching 251 | # call no matter the order. 252 | if not self._any_order: 253 | self._met = True 254 | 255 | def closed(self, with_counts=False): 256 | rval = self._met 257 | if with_counts: 258 | rval = rval or self.counts_met() 259 | return rval 260 | 261 | def counts_met(self): 262 | return self._run_count >= self._min_count and not ( 263 | self._max_count and not self._max_count == self._run_count) 264 | 265 | def match(self, *args, **kwargs): 266 | """ 267 | Check the if these args match this expectation. 268 | """ 269 | return self._any_args or \ 270 | self._arguments_rule.validate(*args, **kwargs) 271 | 272 | def test(self, *args, **kwargs): 273 | """ 274 | Validate all the rules with in this expectation to see if this 275 | expectation has been met. 276 | """ 277 | side_effect_return = None 278 | if not self._met: 279 | if self.match(*args, **kwargs): 280 | self._run_count += 1 281 | if self._max_count is not None and \ 282 | self._run_count == self._max_count: 283 | self._met = True 284 | if self._side_effect: 285 | if self._side_effect_args or self._side_effect_kwargs: 286 | side_effect_return = self._side_effect( 287 | *self._side_effect_args, 288 | **self._side_effect_kwargs) 289 | else: 290 | side_effect_return = self._side_effect(*args, **kwargs) 291 | else: 292 | self._met = False 293 | 294 | # If this is met and we're supposed to tear down, must do it now 295 | # so that this stub is not called again 296 | if self._met and self._teardown: 297 | self._stub.teardown() 298 | 299 | # return_value has priority to not break existing uses of side effects 300 | rval = self.return_value() 301 | if rval is None: 302 | rval = side_effect_return 303 | return rval 304 | 305 | def __str__(self): 306 | runs_string = " Ran: %s, Min Runs: %s, Max Runs: %s" % ( 307 | self._run_count, self._min_count, 308 | "∞" if self._max_count is None else self._max_count) 309 | return_string = " Raises: %s" % ( 310 | self._raises if self._raises else " Returns: %s" % repr( 311 | self._returns)) 312 | return "\n\t%s\n\t%s\n\t\t%s\n\t\t%s" % ( 313 | colored("%s - %s" % ( 314 | self._stub.name, 315 | "Passed" if self._arguments_rule._passed else "Failed"), 316 | "green" if self._arguments_rule._passed else "red"), 317 | self._arguments_rule, return_string, runs_string) 318 | -------------------------------------------------------------------------------- /tests/sample_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the sample module 3 | """ 4 | 5 | import os 6 | import unittest 7 | import sys 8 | 9 | from chai import Chai 10 | from chai.stub import stub 11 | from chai.exception import * 12 | import tests.samples as samples 13 | from tests.samples import SampleBase, SampleChild 14 | 15 | try: 16 | IS_PYPY = sys.subversion[0] == 'PyPy' 17 | except AttributeError: 18 | IS_PYPY = False 19 | 20 | 21 | class CustomException(Exception): 22 | pass 23 | 24 | 25 | class SampleModuleTest(Chai): 26 | 27 | def test_mod_func_2_as_obj_name(self): 28 | expect(samples, 'mod_func_1').args(42, foo='bar') 29 | samples.mod_func_2(42, foo='bar') 30 | 31 | def test_mod_func_2_as_obj_ref(self): 32 | expect(samples.mod_func_1).args(42, foo='bar') 33 | samples.mod_func_2(42, foo='bar') 34 | 35 | 36 | class SampleBaseTest(Chai): 37 | 38 | def test_expects_property(self): 39 | obj = SampleBase() 40 | expect(obj, 'prop').returns("property value") 41 | assert_equals("property value", obj.prop) 42 | 43 | def test_expects_on_builtin_function(self): 44 | # NOTE: os module is a good example where it binds from another 45 | # (e.g. posix), so it has to use the named reference or else it 46 | # stubs the original module 47 | expect(os, 'remove').args('foo').returns('ok') 48 | assert_equals('ok', os.remove('foo')) 49 | 50 | def test_expects_bound_method_returns(self): 51 | obj = SampleBase() 52 | expect(obj.bound_method).args(1, 2).returns(12) 53 | assert_equals(12, obj.bound_method(1, 2)) 54 | 55 | expect(obj.bound_method).args(1, 4).returns(1100) 56 | assert_equals(1100, obj.bound_method(1, 4)) 57 | 58 | def test_expects_bound_method_at_least_with_other_expectation_and_no_anyorder(self): 59 | obj = SampleBase() 60 | expect(obj.bound_method).args(1, 2).returns(12).at_least(2) 61 | assert_equals(12, obj.bound_method(1, 2)) 62 | assert_equals(12, obj.bound_method(1, 2)) 63 | 64 | expect(obj.bound_method).args(1, 3).returns(100) 65 | assert_equals(100, obj.bound_method(1, 3)) 66 | 67 | assert_raises(UnexpectedCall, obj.bound_method, 1, 2) 68 | 69 | def test_expects_bound_method_at_least_with_other_expectation_and_anyorder(self): 70 | obj = SampleBase() 71 | expect(obj.bound_method).args(1, 2).returns(12).at_least(2).any_order() 72 | assert_equals(12, obj.bound_method(1, 2)) 73 | assert_equals(12, obj.bound_method(1, 2)) 74 | 75 | expect(obj.bound_method).args(1, 3).returns(100) 76 | assert_equals(100, obj.bound_method(1, 3)) 77 | 78 | assert_equals(12, obj.bound_method(1, 2)) 79 | 80 | def test_expects_bound_method_at_least_as_last_expectation(self): 81 | obj = SampleBase() 82 | expect(obj.bound_method).args(1, 2).returns(12).at_least(3) 83 | assert_equals(12, obj.bound_method(1, 2)) 84 | assert_equals(12, obj.bound_method(1, 2)) 85 | assert_equals(12, obj.bound_method(1, 2)) 86 | assert_equals(12, obj.bound_method(1, 2)) 87 | 88 | def test_expects_bound_method_at_most(self): 89 | obj = SampleBase() 90 | expect(obj.bound_method).args(1, 2).returns(12).at_most(3) 91 | assert_equals(12, obj.bound_method(1, 2)) 92 | assert_equals(12, obj.bound_method(1, 2)) 93 | obj.bound_method(1, 2) 94 | assert_raises(UnexpectedCall, obj.bound_method, 1, 2) 95 | 96 | def tests_expects_bound_method_any_order_with_fixed_maxes(self): 97 | obj = SampleBase() 98 | expect(obj.bound_method).args(1).returns(2).any_order() 99 | expect(obj.bound_method).args(3).returns(4).any_order() 100 | assert_equals(4, obj.bound_method(3)) 101 | assert_equals(2, obj.bound_method(1)) 102 | assert_raises(UnexpectedCall, obj.bound_method, 1) 103 | 104 | def tests_expects_bound_method_any_order_with_mins(self): 105 | obj = SampleBase() 106 | expect(obj.bound_method).args(1).returns(2).any_order().at_least_once() 107 | expect(obj.bound_method).args(3).returns(4).any_order().at_least_once() 108 | assert_equals(4, obj.bound_method(3)) 109 | assert_equals(2, obj.bound_method(1)) 110 | assert_equals(4, obj.bound_method(3)) 111 | assert_equals(2, obj.bound_method(1)) 112 | assert_equals(2, obj.bound_method(1)) 113 | assert_equals(4, obj.bound_method(3)) 114 | assert_equals(2, obj.bound_method(1)) 115 | 116 | def test_expects_any_order_without_count_modifiers(self): 117 | obj = SampleBase() 118 | expect(obj.bound_method).args(3).returns(4) 119 | expect(obj.bound_method).args(1).returns(2).any_order() 120 | expect(obj.bound_method).args(3).returns(4) 121 | assert_equals(4, obj.bound_method(3)) 122 | assert_equals(4, obj.bound_method(3)) 123 | assert_equals(2, obj.bound_method(1)) 124 | 125 | def test_expects_bound_method_raises(self): 126 | obj = SampleBase() 127 | expect(obj.bound_method).args(1, 2).raises(CustomException) 128 | assert_raises(CustomException, obj.bound_method, 1, 2) 129 | 130 | expect(obj.bound_method).args(1, 2).raises(CustomException()) 131 | assert_raises(CustomException, obj.bound_method, 1, 2) 132 | 133 | def test_expects_bound_method_can_be_used_for_iterative_testing(self): 134 | obj = SampleBase() 135 | expect(obj.bound_method).args(1, 2).returns(12) 136 | assert_equals(12, obj.bound_method(1, 2)) 137 | assert_raises(UnexpectedCall, obj.bound_method) 138 | 139 | expect(obj.bound_method).args(1, 4).returns(1100) 140 | assert_equals(1100, obj.bound_method(1, 4)) 141 | 142 | def test_stub_bound_method_raises_unexpectedcall(self): 143 | obj = SampleBase() 144 | stub(obj.bound_method) 145 | assert_raises(UnexpectedCall, obj.bound_method) 146 | 147 | def test_expect_bound_method_with_equals_comparator(self): 148 | obj = SampleBase() 149 | expect(obj.bound_method).args(equals(42)) 150 | obj.bound_method(42) 151 | assert_raises(UnexpectedCall, obj.bound_method, 32) 152 | 153 | def test_expect_bound_method_with_is_a_comparator(self): 154 | obj = SampleBase() 155 | expect(obj.bound_method).args(is_a(int)) 156 | obj.bound_method(42) 157 | assert_raises(UnexpectedCall, obj.bound_method, '42') 158 | 159 | def test_expect_bound_method_with_anyof_comparator(self): 160 | obj = SampleBase() 161 | expect(obj.bound_method).times(4).args( 162 | any_of(int, 3.14, 'hello', is_a(list))) 163 | obj.bound_method(42) 164 | obj.bound_method(3.14) 165 | obj.bound_method('hello') 166 | obj.bound_method([1, 2, 3]) 167 | assert_raises(UnexpectedCall, obj.bound_method, '42') 168 | 169 | def test_expect_bound_method_with_allof_comparator(self): 170 | obj = SampleBase() 171 | expect(obj.bound_method).args(all_of(length(5), 'hello')) 172 | obj.bound_method('hello') 173 | 174 | expect(obj.bound_method).args(all_of(length(3), 'hello')).at_least(0) 175 | assert_raises(UnexpectedCall, obj.bound_method, 'hello') 176 | 177 | def test_expect_bound_method_with_notof_comparator(self): 178 | obj = SampleBase() 179 | expect(obj.bound_method).args(not_of(any_of(float, int))) 180 | obj.bound_method('hello') 181 | 182 | def test_expect_bound_method_with_notof_comparator_using_types(self): 183 | obj = SampleBase() 184 | expect(obj.bound_method).args(not_of(float, int)) 185 | obj.bound_method('hello') 186 | 187 | def test_expect_unbound_method_acts_as_any_instance(self): 188 | expect(SampleBase, 'bound_method').args('hello').returns('world') 189 | expect(SampleBase, 'bound_method').args('hello').returns('mars') 190 | 191 | obj1 = SampleBase() 192 | obj2 = SampleBase() 193 | assert_equals('world', obj2.bound_method('hello')) 194 | assert_equals('mars', obj1.bound_method('hello')) 195 | assert_raises(UnexpectedCall, obj2.bound_method) 196 | 197 | def test_stub_unbound_method_acts_as_no_instance(self): 198 | stub(SampleBase, 'bound_method') 199 | 200 | obj1 = SampleBase() 201 | obj2 = SampleBase() 202 | assert_raises(UnexpectedCall, obj2.bound_method) 203 | assert_raises(UnexpectedCall, obj1.bound_method) 204 | 205 | def test_expects_class_method(self): 206 | expect(SampleBase.a_classmethod).returns(12) 207 | assert_equals(12, SampleBase.a_classmethod()) 208 | 209 | obj = SampleBase() 210 | expect(SampleBase.a_classmethod).returns(100) 211 | assert_equals(100, obj.a_classmethod()) 212 | 213 | def test_stub_class_method(self): 214 | stub(SampleBase.a_classmethod) 215 | assert_raises(UnexpectedCall, SampleBase.a_classmethod) 216 | 217 | obj = SampleBase() 218 | assert_raises(UnexpectedCall, obj.a_classmethod) 219 | 220 | def test_expect_callback(self): 221 | obj = SampleBase() 222 | expect(obj.callback_target) 223 | obj.callback_source() 224 | 225 | def test_add_to_list_with_mock_object(self): 226 | obj = SampleBase() 227 | obj._deque = mock() 228 | expect(obj._deque.append).args('value') 229 | obj.add_to_list('value') 230 | 231 | def test_add_to_list_with_module_mock_object(self): 232 | mock(samples, 'deque') 233 | deq = mock() 234 | expect(samples.deque.__call__).returns(deq) 235 | expect(deq.append).args('value') 236 | 237 | obj = SampleBase() 238 | obj.add_to_list('value') 239 | 240 | def test_regex_comparator(self): 241 | obj = SampleBase() 242 | expect(obj.bound_method).args(matches("name$")).returns(100) 243 | assert_equals(obj.bound_method('first_name'), 100) 244 | 245 | def test_ignore_arg(self): 246 | obj = SampleBase() 247 | expect(obj.bound_method).args(ignore_arg()).returns(100) 248 | assert_equals(obj.bound_method('first_name'), 100) 249 | 250 | def test_function_comparator(self): 251 | obj = SampleBase() 252 | expect(obj.bound_method).args(func(lambda arg: arg > 10)).returns(100) 253 | assert_equals(obj.bound_method(100), 100) 254 | 255 | def test_in_comparator(self): 256 | obj = SampleBase() 257 | expect(obj.bound_method).args(contains('name')).returns(100).at_most(3) 258 | assert_equals(obj.bound_method(['name', 'age']), 100) 259 | assert_equals(obj.bound_method({'name': 'vitaly'}), 100) 260 | assert_equals(obj.bound_method('lasfs-name-asfsad'), 100) 261 | 262 | def test_almost_equals_comparator(self): 263 | obj = SampleBase() 264 | expect(obj.bound_method).args(almost_equals(10.1234, 2)).returns(100) 265 | assert_equals(obj.bound_method(10.12), 100) 266 | 267 | def test_is_comparator(self): 268 | obj = SampleBase() 269 | expect(obj.bound_method).args(is_arg(obj)).returns(100) 270 | assert_equals(obj.bound_method(obj), 100) 271 | 272 | def test_var_comparator(self): 273 | obj = SampleBase() 274 | expect(obj.add_to_list).args(var('value1')) 275 | expect(obj.add_to_list).args(var('value2')) 276 | expect(obj.add_to_list).args(var('value3')).at_least_once() 277 | 278 | obj.add_to_list('v1') 279 | obj.add_to_list('v2') 280 | obj.add_to_list('v3') 281 | obj.add_to_list('v3') 282 | self.assertRaises(UnexpectedCall, obj.add_to_list, 'v3a') 283 | 284 | assert_equals('v1', var('value1').value) 285 | assert_equals('v2', var('value2').value) 286 | assert_equals('v3', var('value3').value) 287 | 288 | def test_spy(self): 289 | spy(SampleBase) 290 | obj = SampleBase() 291 | assert_true(isinstance(obj, SampleBase)) 292 | 293 | spy(obj.add_to_list) 294 | obj.add_to_list('v1') 295 | assert_equals(['v1'], list(obj._deque)) 296 | 297 | spy(obj.add_to_list).args(var('value2')) 298 | obj.add_to_list('v2') 299 | assert_equals(['v1', 'v2'], list(obj._deque)) 300 | assert_equals('v2', var('value2').value) 301 | 302 | data = {'foo': 'bar'} 303 | 304 | def _sfx(_data): 305 | _data['foo'] = 'bug' 306 | 307 | spy(obj.add_to_list).side_effect(_sfx, data) 308 | obj.add_to_list('v3') 309 | assert_equals(['v1', 'v2', 'v3'], list(obj._deque)) 310 | assert_equals({'foo': 'bug'}, data) 311 | 312 | capture = [0] 313 | 314 | def _return_spy(_deque): 315 | capture.extend(_deque) 316 | 317 | spy(obj.add_to_list).spy_return(_return_spy) 318 | obj.add_to_list('v4') 319 | assert_equals([0, 'v1', 'v2', 'v3', 'v4'], capture) 320 | 321 | with assert_raises(UnsupportedModifier): 322 | spy(obj.add_to_list).times(0).returns(3) 323 | with assert_raises(UnsupportedModifier): 324 | spy(obj.add_to_list).times(0).raises(Exception('oops')) 325 | 326 | @unittest.skipIf(IS_PYPY, "can't spy on wrapper-descriptors in PyPy") 327 | def test_spy_on_method_wrapper(self): 328 | obj = SampleBase() 329 | spy(SampleBase, '__hash__') 330 | dict()[obj] = 'hello world' 331 | 332 | 333 | class SampleChildTest(Chai): 334 | 335 | def test_stub_base_class_expect_child_classmethod(self): 336 | stub(SampleBase.a_classmethod) 337 | expect(SampleChild.a_classmethod) 338 | 339 | SampleChild.a_classmethod() 340 | -------------------------------------------------------------------------------- /tests/stub_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | 4 | from chai.stub import * 5 | from chai.mock import Mock 6 | import tests.samples as samples 7 | 8 | try: 9 | IS_PYPY = sys.subversion[0] == 'PyPy' 10 | except AttributeError: 11 | IS_PYPY = False 12 | 13 | 14 | class StubTest(unittest.TestCase): 15 | """ 16 | Test the public stub() method 17 | """ 18 | def test_stub_property_on_class_with_attr_name(self): 19 | class Foo(object): 20 | @property 21 | def prop(self): return 3 22 | 23 | res = stub(Foo, 'prop') 24 | self.assertTrue(isinstance(res,StubProperty)) 25 | self.assertTrue(stub(Foo,'prop') is res) 26 | 27 | def test_stub_property_on_instance_with_attr_name(self): 28 | class Foo(object): 29 | @property 30 | def prop(self): return 3 31 | foo = Foo() 32 | 33 | res = stub(foo, 'prop') 34 | self.assertTrue(isinstance(res,StubProperty)) 35 | self.assertTrue(stub(foo,'prop') is res) 36 | 37 | def test_stub_property_on_class_with_attr_name_applies_to_instance(self): 38 | class Foo(object): 39 | @property 40 | def prop(self): return 3 41 | 42 | foo = Foo() 43 | res = stub(Foo, 'prop') 44 | self.assertTrue(stub(foo,'prop') is res) 45 | 46 | def test_stub_property_with_obj_ref_for_the_reader(self): 47 | class Foo(object): 48 | @property 49 | def prop(self): return 3 50 | 51 | res = stub(Foo.prop) 52 | self.assertTrue(isinstance(res, StubProperty)) 53 | self.assertTrue(stub(Foo.prop) is res) 54 | 55 | @unittest.skipIf(IS_PYPY, "can't stub property setter in PyPy") 56 | def test_stub_property_with_obj_ref_for_the_setter(self): 57 | class Foo(object): 58 | @property 59 | def prop(self): return 3 60 | 61 | res = stub(Foo.prop.setter) 62 | self.assertTrue(isinstance(res, StubMethod)) 63 | self.assertTrue(isinstance(Foo.prop, StubProperty)) 64 | 65 | @unittest.skipIf(IS_PYPY, "can't stub property deleter in PyPy") 66 | def test_stub_property_with_obj_ref_for_the_deleter(self): 67 | class Foo(object): 68 | @property 69 | def prop(self): return 3 70 | 71 | res = stub(Foo.prop.deleter) 72 | self.assertTrue(isinstance(res, StubMethod)) 73 | self.assertTrue(isinstance(Foo.prop, StubProperty)) 74 | 75 | def test_stub_mock_with_attr_name(self): 76 | class Foo(object): 77 | def bar(self): pass 78 | 79 | f = Foo() 80 | f.bar = Mock() 81 | res = stub(f, 'bar') 82 | self.assertTrue(isinstance(res, StubMethod)) 83 | self.assertEquals(res, f.bar.__call__) 84 | self.assertEquals(res, stub(f, 'bar')) 85 | 86 | def test_stub_mock_with_obj_ref(self): 87 | class Foo(object): 88 | def bar(self): pass 89 | 90 | f = Foo() 91 | f.bar = Mock() 92 | res = stub(f.bar) 93 | self.assertTrue(isinstance(res, StubMethod)) 94 | self.assertEquals(res, f.bar.__call__) 95 | self.assertEquals(res, stub(f.bar)) 96 | 97 | def test_stub_type_with_obj_ref(self): 98 | class Foo(object): 99 | def bar(self): pass 100 | 101 | res = stub(Foo) 102 | self.assertTrue(isinstance(res, StubNew)) 103 | self.assertEquals(res, Foo.__new__) 104 | self.assertEquals(res, stub(Foo)) 105 | 106 | # test that __init__ called only once 107 | res.expect() 108 | self.assertEquals(1, len(res._expectations)) 109 | res = stub(Foo) 110 | self.assertEquals(1, len(res._expectations)) 111 | 112 | def test_stub_unbound_method_with_attr_name(self): 113 | class Foo(object): 114 | def bar(self): pass 115 | 116 | res = stub(Foo, 'bar') 117 | self.assertTrue(isinstance(res,StubUnboundMethod)) 118 | self.assertEquals(res, stub(Foo,'bar')) 119 | self.assertEquals(res, getattr(Foo,'bar')) 120 | 121 | @unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods by reference in python 3") 122 | def test_stub_unbound_method_with_obj_ref(self): 123 | class Foo(object): 124 | def bar(self): pass 125 | 126 | res = stub(Foo.bar) 127 | self.assertTrue(isinstance(res,StubUnboundMethod)) 128 | self.assertEquals(res, stub(Foo.bar)) 129 | self.assertEquals(res, getattr(Foo,'bar')) 130 | 131 | def test_stub_bound_method_for_instance_with_attr_name(self): 132 | class Foo(object): 133 | def bar(self): pass 134 | 135 | foo = Foo() 136 | orig = foo.bar 137 | res = stub(foo, 'bar') 138 | self.assertTrue(isinstance(res,StubMethod)) 139 | self.assertEquals(res._instance, foo) 140 | self.assertEquals(res._obj, orig) 141 | self.assertEquals(res._attr, 'bar') 142 | self.assertEquals(res, stub(foo,'bar')) 143 | self.assertEquals(res, getattr(foo,'bar')) 144 | 145 | def test_stub_bound_method_for_instance_with_obj_ref(self): 146 | class Foo(object): 147 | def bar(self): pass 148 | 149 | foo = Foo() 150 | orig = foo.bar 151 | res = stub(foo.bar) 152 | self.assertTrue(isinstance(res,StubMethod)) 153 | self.assertEquals(res._instance, foo) 154 | self.assertEquals(res._obj, orig) 155 | self.assertEquals(res._attr, 'bar') 156 | self.assertEquals(res, stub(foo.bar)) 157 | self.assertEquals(res, getattr(foo,'bar')) 158 | 159 | def test_stub_bound_method_for_classmethod_with_attr_name(self): 160 | class Foo(object): 161 | @classmethod 162 | def bar(self): pass 163 | 164 | res = stub(Foo, 'bar') 165 | self.assertTrue(isinstance(res,StubMethod)) 166 | self.assertEquals(res, stub(Foo,'bar')) 167 | self.assertEquals(res, getattr(Foo,'bar')) 168 | 169 | def test_stub_bound_method_for_classmethod_with_obj_ref(self): 170 | class Foo(object): 171 | @classmethod 172 | def bar(self): pass 173 | 174 | res = stub(Foo.bar) 175 | self.assertTrue(isinstance(res,StubMethod)) 176 | self.assertEquals(res, stub(Foo.bar)) 177 | self.assertEquals(res, getattr(Foo,'bar')) 178 | 179 | @unittest.skipIf(IS_PYPY, "no method-wrapper in PyPy") 180 | def test_stub_method_wrapper_with_attr_name(self): 181 | class Foo(object): pass 182 | 183 | foo = Foo() 184 | res = stub(foo, '__hash__') 185 | self.assertTrue(isinstance(res,StubMethodWrapper)) 186 | self.assertEquals(res, stub(foo, '__hash__')) 187 | self.assertEquals(res, getattr(foo, '__hash__')) 188 | 189 | @unittest.skipIf(IS_PYPY, "no method-wrapper in PyPy") 190 | def test_stub_method_wrapper_with_obj_ref(self): 191 | class Foo(object): pass 192 | 193 | foo = Foo() 194 | res = stub(foo.__hash__) 195 | self.assertTrue(isinstance(res,StubMethodWrapper)) 196 | self.assertEquals(res, stub(foo.__hash__)) 197 | self.assertEquals(res, getattr(foo, '__hash__')) 198 | 199 | def test_stub_module_function_with_attr_name(self): 200 | res = stub(samples, 'mod_func_1') 201 | self.assertTrue(isinstance(res,StubFunction)) 202 | self.assertEquals(res, getattr(samples,'mod_func_1')) 203 | self.assertEquals(res, stub(samples,'mod_func_1')) 204 | res.teardown() 205 | 206 | def test_stub_module_function_with_obj_ref(self): 207 | res = stub(samples.mod_func_1) 208 | self.assertTrue(isinstance(res,StubFunction)) 209 | self.assertEquals(res, getattr(samples,'mod_func_1')) 210 | self.assertEquals(res, samples.mod_func_1) 211 | self.assertEquals(res, stub(samples.mod_func_1)) 212 | res.teardown() 213 | 214 | class StubClassTest(unittest.TestCase): 215 | ### 216 | ### Test Stub class (if only I could mock my mocking mocks) 217 | ### 218 | def test_init(self): 219 | s = Stub('obj','attr') 220 | self.assertEquals('obj', s._obj) 221 | self.assertEquals('attr', s._attr) 222 | self.assertEquals([], s._expectations) 223 | 224 | def test_unment_expectations(self): 225 | s = Stub('obj', 'attr') 226 | s.expect().args(123).returns(1) 227 | 228 | self.assertTrue(all([isinstance(e, ExpectationNotSatisfied) for e in s.unmet_expectations()])) 229 | 230 | def test_teardown(self): 231 | s = Stub('obj') 232 | s._expections = ['1','2'] 233 | s.teardown() 234 | self.assertEquals([], s._expectations) 235 | 236 | def test_expect(self): 237 | s = Stub('obj') 238 | 239 | self.assertEquals([], s._expectations) 240 | e = s.expect() 241 | self.assertEquals([e], s._expectations) 242 | self.assertEquals(s, e._stub) 243 | 244 | def test_call_orig_raises_notimplemented(self): 245 | s = Stub('obj') 246 | 247 | with self.assertRaises(NotImplementedError): 248 | s.call_orig(1,2,a='b') 249 | 250 | def test_call_raises_unexpected_call_when_no_expectations(self): 251 | s = Stub('obj') 252 | self.assertRaises(UnexpectedCall, s, 'foo') 253 | 254 | def test_call_when_args_match(self): 255 | class Expect(object): 256 | def closed(self): return False 257 | def match(self, *args, **kwargs): return True 258 | def test(self, *args, **kwargs): return 'success' 259 | 260 | s = Stub('obj') 261 | s._expectations = [ Expect() ] 262 | self.assertEquals('success', s('foo')) 263 | 264 | def test_call_raises_unexpected_call_when_all_expectations_closed(self): 265 | class Expect(object): 266 | def closed(self): return True 267 | 268 | s = Stub('obj') 269 | s._expectations = [ Expect(), Expect() ] 270 | self.assertRaises(UnexpectedCall, s, 'foo') 271 | 272 | def test_call_raises_unexpected_call_when_closed_and_no_matching(self): 273 | class Expect(object): 274 | def __init__(self, closed): 275 | self._closed=closed 276 | self._match_count = 0 277 | self._close_count=0 278 | def closed(self): 279 | return self._closed 280 | def match(self, *args, **kwargs): 281 | self._match_count +=1 282 | return False 283 | def close(self, *args, **kwargs): 284 | self._close_count += 1 285 | self._close_args = (args,kwargs) 286 | def counts_met(self): 287 | return self._closed 288 | def is_any_order(self): 289 | return False 290 | 291 | s = Stub('obj') 292 | s._expectations = [ Expect(True), Expect(False) ] 293 | self.assertRaises(UnexpectedCall, s, 'foo') 294 | self.assertEquals(0, s._expectations[0]._match_count) 295 | self.assertEquals(1, s._expectations[1]._match_count) 296 | self.assertEquals(0, s._expectations[0]._close_count) 297 | self.assertEquals(0, s._expectations[1]._close_count) 298 | 299 | class StubPropertyTest(unittest.TestCase): 300 | # FIXME: Need to test teardown and init, these test might be in the base stub tests. 301 | 302 | def test_name(self): 303 | class Foo(object): 304 | @property 305 | def prop(self): return 3 306 | 307 | s = StubProperty(Foo, 'prop') 308 | self.assertEquals(s.name, 'Foo.prop') 309 | 310 | class StubMethodTest(unittest.TestCase): 311 | 312 | def test_init(self): 313 | class Foo(object): 314 | def bar(self): pass 315 | 316 | f = Foo() 317 | orig = f.bar 318 | s = StubMethod(f.bar) 319 | self.assertEquals(s._obj, orig) 320 | self.assertEquals(s._instance, f) 321 | self.assertEquals(s._attr, 'bar') 322 | self.assertEquals(s, getattr(f,'bar')) 323 | 324 | f = Foo() 325 | orig = f.bar 326 | s = StubMethod(f, 'bar') 327 | self.assertEquals(s._obj, orig) 328 | self.assertEquals(s._instance, f) 329 | self.assertEquals(s._attr, 'bar') 330 | self.assertEquals(s, getattr(f,'bar')) 331 | 332 | def test_name(self): 333 | class Expect(object): 334 | def closed(self): return False 335 | obj = Expect() 336 | s = StubMethod(obj.closed) 337 | self.assertEquals("Expect.closed", s.name) 338 | s.teardown() 339 | 340 | s = StubMethod(obj, 'closed') 341 | self.assertEquals("Expect.closed", s.name) 342 | s.teardown() 343 | 344 | def test_call_orig(self): 345 | class Foo(object): 346 | def __init__(self, val): self._val = val 347 | def a(self): return self._val 348 | def b(self, val): self._val = val 349 | 350 | f = Foo(3) 351 | sa = StubMethod(f.a) 352 | sb = StubMethod(f.b) 353 | self.assertEquals(3, sa.call_orig()) 354 | sb.call_orig(5) 355 | self.assertEquals(5, sa.call_orig()) 356 | 357 | def test_teardown(self): 358 | class Foo(object): 359 | def bar(self): pass 360 | 361 | f = Foo() 362 | orig = f.bar 363 | s = StubMethod(f.bar) 364 | s.teardown() 365 | self.assertEquals(orig, f.bar) 366 | 367 | def test_teardown_of_classmethods(self): 368 | class Foo(object): 369 | @classmethod 370 | def bar(self): pass 371 | 372 | self.assertTrue(isinstance(Foo.__dict__['bar'], classmethod)) 373 | s = StubMethod(Foo.bar) 374 | s.teardown() 375 | self.assertTrue(isinstance(Foo.__dict__['bar'], classmethod), "Is not a classmethod") 376 | 377 | def test_teardown_of_bound_instance_methods_exported_in_module(self): 378 | orig = samples.mod_instance_foo 379 | s = StubMethod(samples, 'mod_instance_foo') 380 | s.teardown() 381 | self.assertEquals(orig, samples.mod_instance_foo) 382 | 383 | class StubFunctionTest(unittest.TestCase): 384 | 385 | def test_init(self): 386 | s = StubFunction(samples.mod_func_1) 387 | self.assertEquals(s._instance, samples) 388 | self.assertEquals(s._attr, 'mod_func_1') 389 | self.assertEquals(s, samples.mod_func_1) 390 | self.assertEquals(False, s._was_object_method) 391 | s.teardown() 392 | 393 | def test_init_with_object_method(self): 394 | x = samples.SampleBase() 395 | s = StubFunction(x, '__new__') 396 | self.assertEquals(True, s._was_object_method) 397 | 398 | def test_name(self): 399 | s = StubFunction(samples.mod_func_1) 400 | self.assertEquals('tests.samples.mod_func_1', s.name) 401 | s.teardown() 402 | 403 | def test_call_orig(self): 404 | s = StubFunction(samples.mod_func_3) 405 | self.assertEquals(12, s.call_orig(4)) 406 | s.teardown() 407 | 408 | def test_teardown(self): 409 | orig = samples.mod_func_1 410 | s = StubFunction(samples.mod_func_1) 411 | s.teardown() 412 | self.assertEquals(orig, samples.mod_func_1) 413 | 414 | def test_teardown_on_object_method(self): 415 | x = samples.SampleBase() 416 | self.assertEquals(object.__new__, getattr(x, '__new__')) 417 | s = StubFunction(x, '__new__') 418 | self.assertNotEquals(object.__new__, getattr(x, '__new__')) 419 | s.teardown() 420 | self.assertEquals(object.__new__, getattr(x, '__new__')) 421 | 422 | class StubNewTest(unittest.TestCase): 423 | 424 | @unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods in python 3") 425 | def test_new(self): 426 | class Foo(object): pass 427 | 428 | self.assertEquals(0, len(StubNew._cache)) 429 | x = StubNew(Foo) 430 | self.assertTrue(x is StubNew(Foo)) 431 | self.assertEquals(1, len(StubNew._cache)) 432 | StubNew._cache.clear() 433 | 434 | def test_init(self): 435 | class Foo(object): pass 436 | 437 | s = StubNew(Foo) 438 | self.assertEquals(s._instance, Foo) 439 | self.assertEquals(s._attr, '__new__') 440 | self.assertEquals(s, Foo.__new__) 441 | s.teardown() 442 | 443 | def test_call(self): 444 | class Foo(object): pass 445 | class Expect(object): 446 | def closed(self): return False 447 | def match(self, *args, **kwargs): return args==('state',) and kwargs=={'a':'b'} 448 | def test(self, *args, **kwargs): return 'success' 449 | 450 | s = StubNew(Foo) 451 | s._expectations = [ Expect() ] 452 | self.assertEquals('success', Foo('state', a='b')) 453 | s.teardown() 454 | 455 | @unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods in python 3") 456 | def test_call_orig(self): 457 | class Foo(object): 458 | def __init__(self, val): 459 | self._val = val 460 | 461 | StubNew._cache.clear() 462 | s = StubNew(Foo) 463 | f = s.call_orig(3) 464 | self.assertTrue(isinstance(f,Foo)) 465 | self.assertEquals(3, f._val) 466 | s.teardown() 467 | StubNew._cache.clear() 468 | 469 | @unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods in python 3") 470 | def test_teardown(self): 471 | class Foo(object): pass 472 | 473 | orig = Foo.__new__ 474 | self.assertEquals(0, len(StubNew._cache)) 475 | x = StubNew(Foo) 476 | self.assertEquals(1, len(StubNew._cache)) 477 | x.teardown() 478 | self.assertEquals(0, len(StubNew._cache)) 479 | self.assertEquals(orig, Foo.__new__) 480 | 481 | @unittest.skipIf(sys.version_info.major==3, "can't stub unbound methods in python 3") 482 | def test_teardown_on_custom_new(self): 483 | class Foo(object): 484 | def __new__(cls, *args, **kwargs): 485 | rval = object.__new__(cls) 486 | rval.args = args 487 | return rval 488 | 489 | f1 = Foo('f1') 490 | self.assertEquals(('f1',), f1.args) 491 | orig = Foo.__new__ 492 | self.assertEquals(0, len(StubNew._cache)) 493 | x = StubNew(Foo) 494 | self.assertEquals(1, len(StubNew._cache)) 495 | x.teardown() 496 | self.assertEquals(0, len(StubNew._cache)) 497 | self.assertEquals(orig, Foo.__new__) 498 | f2 = Foo('f2') 499 | self.assertEquals(('f2',), f2.args) 500 | 501 | 502 | class StubUnboundMethodTest(unittest.TestCase): 503 | 504 | def test_init(self): 505 | class Foo(object): 506 | def bar(self): pass 507 | 508 | s = StubUnboundMethod(Foo, 'bar') 509 | self.assertEquals(s._instance, Foo) 510 | self.assertEquals(s._attr, 'bar') 511 | self.assertEquals(s, getattr(Foo,'bar')) 512 | 513 | def test_name(self): 514 | class Expect(object): 515 | def closed(self): return False 516 | 517 | s = StubUnboundMethod(Expect, 'closed') 518 | self.assertEquals("Expect.closed", s.name) 519 | s.teardown() 520 | 521 | def test_teardown(self): 522 | class Foo(object): 523 | def bar(self): pass 524 | 525 | orig = Foo.bar 526 | s = StubUnboundMethod(Foo, 'bar') 527 | s.teardown() 528 | self.assertEquals(orig, Foo.bar) 529 | 530 | def test_call_acts_as_any_instance(self): 531 | class Foo(object): 532 | def bar(self): pass 533 | 534 | class StubIntercept(StubUnboundMethod): 535 | calls = 0 536 | def __call__(self, *args, **kwargs): 537 | self.calls += 1 538 | 539 | orig = Foo.bar 540 | s = StubIntercept(Foo, 'bar') 541 | 542 | f1 = Foo() 543 | f1.bar() 544 | f2 = Foo() 545 | f2.bar() 546 | 547 | self.assertEquals(2, s.calls) 548 | 549 | class StubMethodWrapperTest(unittest.TestCase): 550 | 551 | def test_init(self): 552 | class Foo(object):pass 553 | foo = Foo() 554 | 555 | s = StubMethodWrapper(foo.__hash__) 556 | self.assertEquals(s._instance, foo) 557 | self.assertEquals(s._attr, '__hash__') 558 | self.assertEquals(s, getattr(foo,'__hash__')) 559 | 560 | def test_name(self): 561 | class Foo(object):pass 562 | foo = Foo() 563 | 564 | s = StubMethodWrapper(foo.__hash__) 565 | self.assertEquals("Foo.__hash__", s.name) 566 | s.teardown() 567 | 568 | def test_call_orig(self): 569 | class Foo(object): 570 | def __init__(self, val): self._val = val 571 | def get(self): return self._val 572 | def set(self, val): self._val = val 573 | 574 | f = Foo(3) 575 | sg = StubMethodWrapper(f.get) 576 | ss = StubMethodWrapper(f.set) 577 | 578 | self.assertEquals(3, sg.call_orig()) 579 | ss.call_orig(5) 580 | self.assertEquals(5, sg.call_orig()) 581 | 582 | def test_teardown(self): 583 | class Foo(object):pass 584 | obj = Foo() 585 | orig = obj.__hash__ 586 | s = StubMethodWrapper(obj.__hash__) 587 | s.teardown() 588 | self.assertEquals(orig, obj.__hash__) 589 | 590 | @unittest.skipIf(IS_PYPY, "no method-wrapper in PyPy") 591 | class StubWrapperDescriptionTest(unittest.TestCase): 592 | 593 | def test_init(self): 594 | class Foo(object):pass 595 | s = StubWrapperDescriptor(Foo, '__hash__') 596 | self.assertEquals(s._obj, Foo) 597 | self.assertEquals(s._attr, '__hash__') 598 | self.assertEquals(s, getattr(Foo,'__hash__')) 599 | 600 | def test_name(self): 601 | class Foo(object):pass 602 | 603 | s = StubWrapperDescriptor(Foo, '__hash__') 604 | self.assertEquals("Foo.__hash__", s.name) 605 | s.teardown() 606 | 607 | def test_call_orig(self): 608 | class Foo(object): pass 609 | if sys.version_info < (3, 3): 610 | foo_str = "" 611 | else: 612 | foo_str = ".Foo'>" 613 | 614 | s = StubWrapperDescriptor(Foo, '__str__') 615 | self.assertEquals(foo_str, s.call_orig()) 616 | s.teardown() 617 | 618 | def test_teardown(self): 619 | class Foo(object):pass 620 | orig = Foo.__hash__ 621 | s = StubWrapperDescriptor(Foo, '__hash__') 622 | s.teardown() 623 | self.assertEquals(orig, Foo.__hash__) 624 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | Chai - Python Mocking Made Easy 3 | ================================= 4 | 5 | .. image:: https://travis-ci.org/agoragames/chai.svg?branch=master 6 | :target: https://travis-ci.org/agoragames/chai 7 | 8 | :Version: 1.1.2 9 | :Download: http://pypi.python.org/pypi/chai 10 | :Source: https://github.com/agoragames/chai 11 | :Keywords: python, mocking, testing, unittest, unittest2 12 | 13 | .. contents:: 14 | :local: 15 | 16 | .. _chai-overview: 17 | 18 | Overview 19 | ======== 20 | 21 | Chai provides a very easy to use api for mocking, stubbing and spying your python objects, patterned after the `Mocha `_ library for Ruby. 22 | 23 | .. _chai-example: 24 | 25 | Example 26 | ======= 27 | 28 | The following is an example of a simple test case which is mocking out a get method 29 | on the ``CustomObject``. The Chai api allows use of call chains to make the code 30 | short, clean, and very readable. It also does away with the standard setup-and-replay 31 | work flow, giving you more flexibility in how you write your cases. :: 32 | 33 | 34 | from chai import Chai 35 | 36 | class CustomObject (object): 37 | def get(self, arg): 38 | pass 39 | 40 | class TestCase(Chai): 41 | def test_mock_get(self): 42 | obj = CustomObject() 43 | self.expect(obj.get).args('name').returns('My Name') 44 | self.assert_equals(obj.get('name'), 'My Name') 45 | self.expect(obj.get).args('name').returns('Your Name') 46 | self.assert_equals(obj.get('name'), 'Your Name') 47 | 48 | def test_mock_get_with_at_most(self): 49 | obj = CustomObject() 50 | self.expect(obj.get).args('name').returns('My Name').at_most(2) 51 | self.assert_equals(obj.get('name'), 'My Name') 52 | self.assert_equals(obj.get('name'), 'My Name') 53 | self.assert_equals(obj.get('name'), 'My Name') # this one will fail 54 | 55 | if __name__ == '__main__': 56 | import unittest2 57 | unittest2.main() 58 | 59 | 60 | .. _chai-api: 61 | 62 | API 63 | === 64 | 65 | All of the features are available by extending the ``Chai`` class, itself a subclass of ``unittest.TestCase``. If ``unittest2`` is available Chai will use that, else it will fall back to ``unittest``. Chai also aliases all of the ``assert*`` methods to lower-case with undersores. For example, ``assertNotEquals`` can also be referenced as ``assert_not_equals``. 66 | 67 | Additionally, ``Chai`` loads in all assertions, comparators and mocking methods into the module in which a ``Chai`` subclass is declared. This is done to cut down on the verbosity of typing ``self.`` everywhere that you want to run a test. The references are loaded into the subclass' module during ``setUp``, so you're sure any method you call will be a reference to the class and module in which a particular test method is currently being executed. Methods and comparators you define locally in a test case will be globally available when you're running that particular case as well. :: 68 | 69 | class ProtocolInterface(object): 70 | def _private_call(self, arg): 71 | pass 72 | def get_result(self, arg): 73 | self._private_call(arg) 74 | return 'ok' 75 | 76 | class TestCase(Chai): 77 | def assert_complicated_state(self, obj): 78 | return True # ..or.. raise AssertionError() 79 | 80 | def test_mock_get(self): 81 | obj = ProtocolInterface() 82 | data = object() 83 | expect(obj._private_call).args(data) 84 | assert_equals('ok', obj.get_result(data)) 85 | assert_complicated_state(data) 86 | 87 | As of 0.3.0, the Chai API has significantly changed such that the default behavior of an expectation is least specific. This supports rapid iterative testing with minimal pain and verbosity. An example of the differences: :: 88 | 89 | class CustomObject (object): 90 | def get(self, arg): 91 | return arg 92 | 93 | class TestCase(Chai): 94 | def test_0_2_0(self): 95 | obj = CustomObject() 96 | expect(obj.get).args(5) 97 | assert_equals( None, obj.get(5) ) 98 | assert_equals( None, obj.get(5) ) 99 | assert_equals( None, obj.get(5) ) 100 | expect(obj.get).any_args().returns( 'test' ).times(2) 101 | assert_equals( 'test', obj.get(5) ) 102 | assert_equals( 'test', obj.get(5) ) 103 | assert_raises( UnexpectedCall, obj.get ) 104 | 105 | def test_0_3_0(self): 106 | obj = CustomObject() 107 | expect(obj.get) 108 | assert_equals( None, obj.get() ) 109 | expect(obj.get).returns( 'test' ) 110 | assert_equals( 'test', obj.get(5) ) 111 | assert_equals( 'test', obj.get(5) ) 112 | 113 | Stubbing 114 | -------- 115 | 116 | The simplest mock is to stub a method. This replaces the original method with a subclass of ``chai.Stub``, the main instrumentation class. All additional ``stub`` and ``expect`` calls will re-use this stub, and the stub is responsible for re-installing the original reference when ``Chai.tearDown`` is run. 117 | 118 | Stubbing is used for situations when you want to assert that a method is never called, and is equivalent to ``expect(target).times(0)``:: 119 | 120 | class CustomObject (object): 121 | def get(self, arg): 122 | pass 123 | @property 124 | def prop(self): 125 | pass 126 | 127 | class TestCase(Chai): 128 | def test_mock_get(self): 129 | obj = CustomObject() 130 | stub(obj.get) 131 | assert_raises( UnexpectedCall, obj.get ) 132 | 133 | In this example, we can reference ``obj.get`` directly because ``get`` is a bound method and provides all of the context we need to refer back to ``obj`` and stub the method accordingly. There are cases where this is insufficient, such as module imports, special Python types, and when module attributes are imported from another (like ``os`` and ``posix``). If the object can't be stubbed with a reference, ``UnsupportedStub`` will be raised and you can use the verbose reference instead. :: 134 | 135 | class TestCase(Chai): 136 | def test_mock_get(self): 137 | obj = CustomObject() 138 | stub(obj, 'get') 139 | assert_raises( UnexpectedCall, obj.get ) 140 | 141 | Stubbing an unbound method will apply that stub to all future instances of that class. :: 142 | 143 | class TestCase(Chai): 144 | def test_mock_get(self): 145 | stub(CustomObject.get) 146 | obj = CustomObject() 147 | assert_raises( UnexpectedCall, obj.get ) 148 | 149 | Unbound methods can also be stubbed by attribute name instead of by reference. :: 150 | 151 | class TestCase(Chai): 152 | def test_mock_get(self): 153 | stub(CustomObject, 'get') 154 | obj = CustomObject() 155 | assert_raises( UnexpectedCall, obj.get ) 156 | 157 | Some methods cannot be stubbed because it is impossible to call ``setattr`` on the object, typically because it's a C extension. A good example of this is the ``datetime.datetime`` class. In that situation, it is best to mock out the entire module (see below). 158 | 159 | Finally, Chai supports stubbing of properties on classes. In all cases, the stub will be applied to a class and individually to each of the 3 property methods. Because the stub is on the class, all instances need to be addressed when you write expectations. The first interface is via the named attribute method which can be used on both classes and instances. :: 160 | 161 | class TestCase(Chai): 162 | def test_prop_attr(self): 163 | obj = CustomObject() 164 | stub( obj, 'prop' ) 165 | assert_raises( UnexpectedCall, lambda: obj.prop ) 166 | stub( stub( obj, 'prop' ).setter ) 167 | 168 | Using the class, you can directly refer to all 3 methods of the property. To refer to the getter you use the property directly, and for the methods you use its associated attribute name. You can stub in any order and it will still resolve correctly. :: 169 | 170 | class TestCase(Chai): 171 | def test_prop_attr(self): 172 | stub( CustomObject.prop.setter ) 173 | stub( CustomObject.prop ) 174 | stub( CustomObject.prop.deleter ) 175 | assert_raises( UnexpectedCall, lambda: CustomObject().prop ) 176 | 177 | Python 3 178 | ++++++++ 179 | 180 | Unbound methods can only be stubbed by attribute in Python 3 as unbound methods do not have a reference to the class they're defined in, and appear as module functions. 181 | 182 | PyPy 183 | ++++ 184 | PyPy does not support stubs on the setter and deleter methods of properties. Additionally, it does not support spies on methods such as ``ExampleClass.__hash__`` because it is represented as an unbound method, rather than CPython's ``method-wrapper``, and unbound methods do not support spies. 185 | 186 | 187 | Expectations and Spies 188 | ---------------------- 189 | 190 | Expectations are individual test cases that can be applied to a stub. They are expected to be run in order (unless otherwise noted). They are greedy, in that so long as an expectation has not been met and the arguments match, the arguments will be processed by that expectation. This mostly applies to the "at_least" and "any_order" expectations, which (may) stay open throughout the test and will handle any matching call. 191 | 192 | Expectations will automatically create a stub if it's not already applied, so no separate call to ``stub`` is necessary. The arguments and edge cases regarding what can and cannot have expectations applied are identical to stubs. The ``expect`` call will return a new ``chai.Expectation`` object which can then be used to modify the expectation. Without any modifiers, an expectation will expect at least one call with any arguments and return None. :: 193 | 194 | class TestCase(Chai): 195 | def test_mock_get(self): 196 | obj = CustomObject() 197 | expect(obj.get) 198 | assert_equals(None, obj.get()) 199 | assert_equals(None, obj.get()) 200 | 201 | As noted above, Chai will by default perform a greedy match, closing out an implied ``at_least_once()`` on every expectation when a new expectation is defined. The expectation will be immediately closed if it has already been satisfied when a new expectation is created. :: 202 | 203 | class TestCase(Chai): 204 | def test_mock_get(self): 205 | obj = CustomObject() 206 | expect(obj.get).returns(1) 207 | expect(obj.get).returns(2) 208 | assert_equals(1, obj.get()) 209 | assert_equals(2, obj.get()) 210 | assert_equals(2, obj.get()) 211 | expect(obj.get).returns(3) 212 | assert_equals(3, obj.get()) 213 | 214 | Modifiers can be applied to the expectation. Each modifier will return a reference to the expectation for easy chaining. In this example, we're going to match a parameter and change the behavior depending on the argument. This also shows the ability to incrementally add expectations throughout the test. :: 215 | 216 | class TestCase(Chai): 217 | def test_mock_get(self): 218 | obj = CustomObject() 219 | expect(obj.get).args('foo').returns('hello').times(2) 220 | assert_equals('hello', obj.get('foo') ) 221 | assert_equals('hello', obj.get('foo') ) 222 | expect(obj.get).args('bar').raises( ValueError ) 223 | assert_raises(ValueError, obj.get, 'bar') 224 | 225 | It is very common to need to run expectations on the constructor for an object, possibly including returning a mock object. Chai makes this very simple. :: 226 | 227 | def method(): 228 | obj = CustomObject('state') 229 | obj.save() 230 | return obj 231 | 232 | class TestCase(Chai): 233 | def test_method(self): 234 | obj = mock() 235 | expect( CustomObject ).args('state').returns( obj ) 236 | expect( obj.save ) 237 | assert_equals( obj, method() ) 238 | 239 | 240 | The arguments modifier supports several matching functions. For simplicity in covering the common cases, the ``args`` modifier assumes an equals test for instances and a logical or of ``[instanceof, equals]`` test for types. All rules that apply to positional arguments also apply to keyword arguments. :: 241 | 242 | class TestCase(Chai): 243 | def test_mock_get(self): 244 | obj = CustomObject() 245 | expect(obj.get).args(is_a(float)).returns(42) 246 | assert_raises( UnexpectedCall, obj.get, 3 ) 247 | assert_equals( 42, obj.get(3.14) ) 248 | 249 | expect(obj.get).args(str).returns('yes') 250 | assert_equals( 'yes', obj.get('no') ) 251 | 252 | expect(obj.get).args(is_arg(list)).return('yes') 253 | assert_raises( UnexpectedCall, obj.get, [] ) 254 | assert_equals( 'yes', obj.get(list) ) 255 | 256 | Lastly, Chai 1.0.0 supports spies. These are an extension of expectations and support most of the same features. The modifiers ``returns`` and ``raises`` raise ``UnsupportedModifier`` because the spy passes arguments and returns or raises the results of the stubbed function. You can make use of ``side_effect`` to inject code just before the spied-on function is executed, however the return value will be ignored. This behavior is especially useful when testing race conditions. Additionally, there are a few types of stubs which are not (currently) supported by spies: 257 | 258 | * properties 259 | * unbound methods 260 | 261 | Spies can be used just like any expectation. :: 262 | 263 | class TestCase(Chai): 264 | def test_spy(self): 265 | class Spy(object): 266 | def __init__(self, val): 267 | self._val = val 268 | def set(self, val): 269 | self._val = val 270 | def get(self): 271 | return self._val 272 | 273 | # Spy on the constructor 274 | spy(Spy) 275 | obj = Spy(3) 276 | assert_true(isinstance(obj,Spy)) 277 | assert_equals(3, obj._val) 278 | 279 | # Spy on the set function 280 | spy(obj.set).args(5) 281 | obj.set(5) 282 | assert_equals(5, obj._val) 283 | 284 | # Spy on the get function 285 | spy(obj.get) 286 | assert_equals(5, obj.get()) 287 | 288 | # Spy on the hash function 289 | spy(Spy, '__hash__') 290 | dict()[obj] = "I spy with my little eye" 291 | 292 | Modifiers 293 | +++++++++ 294 | 295 | Expectations expose the following public methods for changing their behavior. 296 | 297 | 298 | args(``*args``, ``**kwargs``) 299 | Add a test to the expectation for matching arguments. 300 | 301 | any_args 302 | Any arguments are accepted. 303 | 304 | returns(object) 305 | Add a return value to the expectation when it is matched and executed. 306 | 307 | raises(exception) 308 | When the expectation is run it will raise this exception. Accepts type or instance. 309 | 310 | times(int) 311 | An integer that defines a hard limit on the minimum and maximum number of times the expectation should be executed. 312 | 313 | at_least(int) 314 | Sets a minimum number of times the expectation should run and removes any maximum. 315 | 316 | at_least_once 317 | Equivalent to ``at_least(1)``. 318 | 319 | at_most(int) 320 | Sets a maximum number of times the expectation should run. Does not affect the minimum. 321 | 322 | at_most_once 323 | Equivalent to ``at_most(1)``. 324 | 325 | once 326 | Equivalent to ``times(1)``, also the default for any expectation. 327 | 328 | any_order 329 | The expectation can be called at any time, independent of when it was defined. Can be combined with ``at_least_once`` to force it to respond to all matching calls throughout the test. 330 | 331 | side_effect(callable, \*args, \*\*kwargs) 332 | Called with a function argument. When the expectation passes a test, the function will be executed. The side effect will be executed even if the expectation is configured to raise an exception. If the side effect is defined with arguments, then those arguments will be passed in when it's called, otherwise the arguments passed in to the expectation will be passed in. 333 | 334 | spy_return(callable) 335 | [Spies Only] Called with a function argument. When the expectation passes a test, the function will be executed and passed the return value from the function as an argument. 336 | 337 | teardown 338 | Will remove the stub after the expectation has been met. This is useful in cases where you need to mock core methods such as ``open``, but immediately return its original behavior after the mocked call has run. 339 | 340 | 341 | Argument Comparators 342 | ++++++++++++++++++++ 343 | 344 | Argument comparators are defined as classes in ``chai.comparators``, but loaded into the ``Chai`` class for convenience (and by extension, a subclass' module). ``Chai`` handles the common case of a ``type`` object by using the ``is_a`` comparator, else defaults to the ``equals`` comparator. Users can create subclasses of ``Comparator`` and use those for custom argument processing. 345 | 346 | Comparators can also be used inside data structures. For example: :: 347 | 348 | expect( area ).args( {'pi':almost_equals(3.14), 'radius':is_a(int,long,float)} ) 349 | 350 | 351 | 352 | equals(object) 353 | The default comparator, uses standard Python equals operator 354 | 355 | almost_equals(float, places) 356 | Identical to assertAlmostEquals, will match an argument to the comparator value to a most ``places`` digits beyond the decimal point. 357 | 358 | length(len) 359 | Matches parameters with defined length. Must be either an integer or a set of integers that implements the ``in`` function. 360 | 361 | is_a(type) 362 | Match an argument of a given type. Supports same arguments as builtin function ``isinstance``. 363 | 364 | is_arg(object) 365 | Matches an argument using the Python ``is`` comparator. 366 | 367 | any_of(comparator_list) 368 | Matches an argument if any of the comparators in the argument list are met. Uses automatic comparator generation for instances and types in the list. 369 | 370 | all_of(comparator_list) 371 | Matches an argument if all of the comparators in the argument list are met. Uses automatic comparator generation for instances and types in the list. 372 | 373 | not_of(comparator) 374 | Matches an argument if the supplied comparator does not match. 375 | 376 | matches(pattern) 377 | Matches an argument using a regular expression. Standard ``re`` rules apply. 378 | 379 | func(callable) 380 | Matches an argument if the callable returns True. The callable must take one argument, the parameter being checked. 381 | 382 | ignore 383 | Matches any argument. 384 | 385 | in_arg(in_list) 386 | Matches if the argument is in the ``in_list``. 387 | 388 | contains(object) 389 | Matches if the argument contains the object using the Python ``in`` function. 390 | 391 | like(container) 392 | Matches if the argument contains all of the same items as in ``container``. Insists that the argument is the same type as ``container``. Useful when you need to assert a few values in a list or dictionary, but the exact contents are not known or can vary. 393 | 394 | var(name) 395 | A variable match against the first time that the argument is called. In the case of multiple calls, the second one must match the previous value of ``name``. After your tests have run, you can check the value against expected arguments through ``var(name).value``. This is really useful when you're testing a deep stack and it's simpler to assert that "value A was used in method call X". Variables can also be used to capture an argument and return it. :: 396 | 397 | expect( encode ).args( var('src'), 'gzip' ).returns( var('src') ) 398 | 399 | 400 | **A note of caution** 401 | If you are using the ``func`` comparator to produce side effects, be aware that it may be called more than once even if the expectation you're defining only occurs once. This is due to the way ``Stub.__call__`` processes the expectations and determines when to process arguments through an expectation. 402 | 403 | 404 | Context Manager 405 | +++++++++++++++ 406 | 407 | An expectation can act as a context manager, which is very useful in complex mocking situations. The context will always be the return value for the expectation. For example: :: 408 | 409 | def get_cursor(cname): 410 | return db.Connection( 'host:port' ).collection( cname ).cursor() 411 | 412 | def test_get_cursor(): 413 | with expect( db.Connection ).any_args().returns( mock() ) as connection: 414 | with expect( connection.collection ).args( 'collection' ).returns( mock() ) as collection: 415 | expect( collection.cursor ).returns( 'cursor' ) 416 | 417 | assert_equals( 'cursor', get_cursor('collection') ) 418 | 419 | Mock 420 | ---- 421 | 422 | Sometimes you need a mock object which can be used to stub and expect anything. Chai exposes this through the ``mock`` method which can be called in one of two ways. 423 | 424 | Without any arguments, ``Chai.mock()`` will return a ``chai.Mock`` object that can be used for any purpose. If called with arguments, it behaves like ``stub`` and ``expect``, creating a Mock object and setting it as the attribute on another object. 425 | 426 | Any request for an attribute from a Mock will return a new Mock object, but ``setattr`` behaves as expected so it can store state as well. The dynamic function will act like a stub, raising ``UnexpectedCall`` if no expectation is defined. :: 427 | 428 | class CustomObject(object): 429 | def __init__(self, handle): 430 | _handle = handle 431 | def do(self, arg): 432 | return _handle.do(arg) 433 | 434 | class TestCase(Chai): 435 | def test_mock_get(self): 436 | obj = CustomObject( mock() ) 437 | expect( obj._handle.do ).args('it').returns('ok') 438 | assert_equals('ok', obj.do('it')) 439 | assert_raises( UnexpectedCall, obj._handle.do_it_again ) 440 | 441 | The ``stub`` and ``expect`` methods handle ``Mock`` objects as arguments by mocking the ``__call__`` method, which can also act in place of ``__init__``. :: 442 | 443 | # module custom.py 444 | from collections import deque 445 | 446 | class CustomObject(object): 447 | def __init__(self): 448 | self._stack = deque() 449 | 450 | # module custom_test.py 451 | import custom 452 | from custom import CustomObject 453 | 454 | class TestCase(Chai): 455 | def test_mock_get(self): 456 | mock( custom, 'deque' ) 457 | expect( custom.deque ).returns( 'stack' ) 458 | 459 | obj = CustomObject() 460 | assert_equals('stack', obj._stack) 461 | 462 | Here we can see how to mock an entire module, in this case replacing the ``deque`` import in ``custom.py`` with a ``Mock``. 463 | 464 | ``Mock`` objects, because of the ``getattr`` implementation, can also support nested attributes. :: 465 | 466 | class TestCase(Chai): 467 | def test_mock(self): 468 | m = mock() 469 | m.id = 42 470 | expect( m.foo.bar ).returns( 'hello' ) 471 | assert_equals( 'hello', m.foo.bar() ) 472 | assert_equals( 42, m.id ) 473 | 474 | In addition to implementing ``__call__``, ``Mock`` objects implement ``__nonzero__``, 475 | the container and context manager interfaces are defined. Nonzero will always return 476 | ``True``; other methods will raise ``UnexpectedCall``. The ``__getattr__`` method 477 | cannot be itself stubbed. 478 | 479 | .. _chai-installation: 480 | 481 | Installation 482 | ============ 483 | 484 | You can install Chai either via the Python Package Index (PyPI) 485 | or from source. 486 | 487 | To install using ``pip``,:: 488 | 489 | $ pip install chai 490 | 491 | .. _chai-installing-from-source: 492 | 493 | Downloading and installing from source 494 | -------------------------------------- 495 | 496 | Download the latest version of Chai from http://pypi.python.org/pypi/chai 497 | 498 | You can install it by doing the following,:: 499 | 500 | $ tar xvfz chai-*.*.*.tar.gz 501 | $ cd chai-*.*.*.tar.gz 502 | $ python setup.py install # as root 503 | 504 | .. _chai-installing-from-git: 505 | 506 | Using the development version 507 | ----------------------------- 508 | 509 | You can clone the repository by doing the following:: 510 | 511 | $ git clone git://github.com/agoragames/chai.git 512 | 513 | .. _testing: 514 | 515 | Testing 516 | ======= 517 | 518 | Use `nose `_ to run the test suite. :: 519 | 520 | $ nosetests 521 | 522 | .. _bug-tracker: 523 | 524 | Bug tracker 525 | =========== 526 | 527 | If you have any suggestions, bug reports or annoyances please report them 528 | to our issue tracker at https://github.com/agoragames/chai/issues 529 | 530 | .. _license: 531 | 532 | License 533 | ======= 534 | 535 | This software is licensed under the `New BSD License`. See the ``LICENSE.txt`` 536 | file in the top distribution directory for the full license text. 537 | 538 | .. _contributors: 539 | 540 | Contributors 541 | ============ 542 | 543 | Special thank you to the following people for contributions to Chai 544 | 545 | * Jason Baker (https://github.com/jasonbaker) 546 | * Pierre-Yves Chibon (https://github.com/pypingou) 547 | * Graylin Kim (https://github.com/GraylinKim) 548 | 549 | .. # vim: syntax=rst expandtab tabstop=4 shiftwidth=4 shiftround 550 | 551 | -------------------------------------------------------------------------------- /chai/stub.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Copyright (c) 2011-2017, Agora Games, LLC All rights reserved. 3 | 4 | https://github.com/agoragames/chai/blob/master/LICENSE.txt 5 | ''' 6 | import inspect 7 | import types 8 | import sys 9 | import gc 10 | 11 | from .expectation import Expectation 12 | from .spy import Spy 13 | from .exception import * 14 | from ._termcolor import colored 15 | 16 | # For clarity here and in tests, could make these class or static methods on 17 | # Stub. Chai base class would hide that. 18 | 19 | 20 | def stub(obj, attr=None): 21 | ''' 22 | Stub an object. If attr is not None, will attempt to stub that attribute 23 | on the object. Only required for modules and other rare cases where we 24 | can't determine the binding from the object. 25 | ''' 26 | if attr: 27 | return _stub_attr(obj, attr) 28 | else: 29 | return _stub_obj(obj) 30 | 31 | 32 | def _stub_attr(obj, attr_name): 33 | ''' 34 | Stub an attribute of an object. Will return an existing stub if 35 | there already is one. 36 | ''' 37 | # Annoying circular reference requires importing here. Would like to see 38 | # this cleaned up. @AW 39 | from .mock import Mock 40 | 41 | # Check to see if this a property, this check is only for when dealing 42 | # with an instance. getattr will work for classes. 43 | is_property = False 44 | 45 | if not inspect.isclass(obj) and not inspect.ismodule(obj): 46 | # It's possible that the attribute is defined after initialization, and 47 | # so is not on the class itself. 48 | attr = getattr(obj.__class__, attr_name, None) 49 | if isinstance(attr, property): 50 | is_property = True 51 | 52 | if not is_property: 53 | attr = getattr(obj, attr_name) 54 | 55 | # Return an existing stub 56 | if isinstance(attr, Stub): 57 | return attr 58 | 59 | # If a Mock object, stub its __call__ 60 | if isinstance(attr, Mock): 61 | return stub(attr.__call__) 62 | 63 | if isinstance(attr, property): 64 | return StubProperty(obj, attr_name) 65 | 66 | # Sadly, builtin functions and methods have the same type, so we have to 67 | # use the same stub class even though it's a bit ugly 68 | if inspect.ismodule(obj) and isinstance(attr, (types.FunctionType, 69 | types.BuiltinFunctionType, 70 | types.BuiltinMethodType)): 71 | return StubFunction(obj, attr_name) 72 | 73 | # In python3 unbound methods are treated as functions with no reference 74 | # back to the parent class and no im_* fields. We can still make unbound 75 | # methods work by passing these through to the stub 76 | if inspect.isclass(obj) and isinstance(attr, types.FunctionType): 77 | return StubUnboundMethod(obj, attr_name) 78 | 79 | # I thought that types.UnboundMethodType differentiated these cases but 80 | # apparently not. 81 | if isinstance(attr, types.MethodType): 82 | # Handle differently if unbound because it's an implicit "any instance" 83 | if getattr(attr, 'im_self', None) is None: 84 | # Handle the python3 case and py2 filter 85 | if hasattr(attr, '__self__'): 86 | if attr.__self__ is not None: 87 | return StubMethod(obj, attr_name) 88 | if sys.version_info.major == 2: 89 | return StubUnboundMethod(attr) 90 | else: 91 | return StubMethod(obj, attr_name) 92 | 93 | if isinstance(attr, (types.BuiltinFunctionType, types.BuiltinMethodType)): 94 | return StubFunction(obj, attr_name) 95 | 96 | # What an absurd type this is .... 97 | if type(attr).__name__ == 'method-wrapper': 98 | return StubMethodWrapper(attr) 99 | 100 | # This is also slot_descriptor 101 | if type(attr).__name__ == 'wrapper_descriptor': 102 | return StubWrapperDescriptor(obj, attr_name) 103 | 104 | raise UnsupportedStub( 105 | "can't stub %s(%s) of %s", attr_name, type(attr), obj) 106 | 107 | 108 | def _stub_obj(obj): 109 | ''' 110 | Stub an object directly. 111 | ''' 112 | # Annoying circular reference requires importing here. Would like to see 113 | # this cleaned up. @AW 114 | from .mock import Mock 115 | 116 | # Return an existing stub 117 | if isinstance(obj, Stub): 118 | return obj 119 | 120 | # If a Mock object, stub its __call__ 121 | if isinstance(obj, Mock): 122 | return stub(obj.__call__) 123 | 124 | # If passed-in a type, assume that we're going to stub out the creation. 125 | # See StubNew for the awesome sauce. 126 | # if isinstance(obj, types.TypeType): 127 | if hasattr(types, 'TypeType') and isinstance(obj, types.TypeType): 128 | return StubNew(obj) 129 | elif hasattr(__builtins__, 'type') and \ 130 | isinstance(obj, __builtins__.type): 131 | return StubNew(obj) 132 | elif inspect.isclass(obj): 133 | return StubNew(obj) 134 | 135 | # I thought that types.UnboundMethodType differentiated these cases but 136 | # apparently not. 137 | if isinstance(obj, types.MethodType): 138 | # Handle differently if unbound because it's an implicit "any instance" 139 | if getattr(obj, 'im_self', None) is None: 140 | # Handle the python3 case and py2 filter 141 | if hasattr(obj, '__self__'): 142 | if obj.__self__ is not None: 143 | return StubMethod(obj) 144 | if sys.version_info.major == 2: 145 | return StubUnboundMethod(obj) 146 | else: 147 | return StubMethod(obj) 148 | 149 | # These aren't in the types library 150 | if type(obj).__name__ == 'method-wrapper': 151 | return StubMethodWrapper(obj) 152 | 153 | if type(obj).__name__ == 'wrapper_descriptor': 154 | raise UnsupportedStub( 155 | "must call stub(obj,'%s') for slot wrapper on %s", 156 | obj.__name__, obj.__objclass__.__name__) 157 | 158 | # (Mostly) Lastly, look for properties. 159 | # First look for the situation where there's a reference back to the 160 | # property. 161 | prop = obj 162 | if isinstance(getattr(obj, '__self__', None), property): 163 | obj = prop.__self__ 164 | 165 | # Once we've found a property, we have to figure out how to reference 166 | # back to the owning class. This is a giant pain and we have to use gc 167 | # to find out where it comes from. This code is dense but resolves to 168 | # something like this: 169 | # >>> gc.get_referrers( foo.x ) 170 | # [{'__dict__': , 171 | # 'x': , 172 | # '__module__': '__main__', 173 | # '__weakref__': , 174 | # '__doc__': None}] 175 | if isinstance(obj, property): 176 | klass, attr = None, None 177 | for ref in gc.get_referrers(obj): 178 | if klass and attr: 179 | break 180 | if isinstance(ref, dict) and ref.get('prop', None) is obj: 181 | klass = getattr( 182 | ref.get('__dict__', None), '__objclass__', None) 183 | for name, val in getattr(klass, '__dict__', {}).items(): 184 | if val is obj: 185 | attr = name 186 | break 187 | # In the case of PyPy, we have to check all types that refer to 188 | # the property, and see if any of their attrs are the property 189 | elif isinstance(ref, type): 190 | # Use dir as a means to quickly walk through the class tree 191 | for name in dir(ref): 192 | if getattr(ref, name) == obj: 193 | klass = ref 194 | attr = name 195 | break 196 | 197 | if klass and attr: 198 | rval = stub(klass, attr) 199 | if prop != obj: 200 | return stub(rval, prop.__name__) 201 | return rval 202 | 203 | # If a function and it has an associated module, we can mock directly. 204 | # Note that this *must* be after properties, otherwise it conflicts with 205 | # stubbing out the deleter methods and such 206 | # Sadly, builtin functions and methods have the same type, so we have to 207 | # use the same stub class even though it's a bit ugly 208 | if isinstance(obj, (types.FunctionType, types.BuiltinFunctionType, 209 | types.BuiltinMethodType)) and hasattr(obj, '__module__'): 210 | return StubFunction(obj) 211 | 212 | raise UnsupportedStub("can't stub %s", obj) 213 | 214 | 215 | class Stub(object): 216 | 217 | ''' 218 | Base class for all stubs. 219 | ''' 220 | 221 | def __init__(self, obj, attr=None): 222 | ''' 223 | Setup the structs for expectations 224 | ''' 225 | self._obj = obj 226 | self._attr = attr 227 | self._expectations = [] 228 | self._torn = False 229 | 230 | @property 231 | def name(self): 232 | return None # The base class implement this. 233 | 234 | @property 235 | def expectations(self): 236 | return self._expectations 237 | 238 | def unmet_expectations(self): 239 | ''' 240 | Assert that all expectations on the stub have been met. 241 | ''' 242 | unmet = [] 243 | for exp in self._expectations: 244 | if not exp.closed(with_counts=True): 245 | unmet.append(ExpectationNotSatisfied(exp)) 246 | return unmet 247 | 248 | def teardown(self): 249 | ''' 250 | Clean up all expectations and restore the original attribute of the 251 | mocked object. 252 | ''' 253 | if not self._torn: 254 | self._expectations = [] 255 | self._torn = True 256 | self._teardown() 257 | 258 | def _teardown(self): 259 | ''' 260 | Hook for subclasses to teardown their stubs. Called only once. 261 | ''' 262 | 263 | def expect(self): 264 | ''' 265 | Add an expectation to this stub. Return the expectation. 266 | ''' 267 | exp = Expectation(self) 268 | self._expectations.append(exp) 269 | return exp 270 | 271 | def spy(self): 272 | ''' 273 | Add a spy to this stub. Return the spy. 274 | ''' 275 | spy = Spy(self) 276 | self._expectations.append(spy) 277 | return spy 278 | 279 | def call_orig(self, *args, **kwargs): 280 | ''' 281 | Calls the original function. 282 | ''' 283 | raise NotImplementedError("Must be implemented by subclasses") 284 | 285 | def __call__(self, *args, **kwargs): 286 | for exp in self._expectations: 287 | # If expectation closed skip 288 | if exp.closed(): 289 | continue 290 | 291 | # If args don't match the expectation but its minimum counts have 292 | # been met, close it and move on, else it's an unexpected call. 293 | # Have to check counts here now due to the looser definitions of 294 | # expectations in 0.3.x If we dont match, the counts aren't met 295 | # and we're not allowing out-of-order, then break out and raise 296 | # an exception. 297 | if not exp.match(*args, **kwargs): 298 | if exp.counts_met(): 299 | exp.close(*args, **kwargs) 300 | elif not exp.is_any_order(): 301 | break 302 | else: 303 | return exp.test(*args, **kwargs) 304 | 305 | raise UnexpectedCall( 306 | call=self.name, suffix=self._format_exception(), 307 | args=args, kwargs=kwargs) 308 | 309 | def _format_exception(self): 310 | result = [ 311 | colored("All expectations", 'white', attrs=['bold']) 312 | ] 313 | for e in self._expectations: 314 | result.append(str(e)) 315 | return "\n".join(result) 316 | 317 | 318 | class StubProperty(Stub, property): 319 | 320 | ''' 321 | Property stubbing. 322 | ''' 323 | 324 | def __init__(self, obj, attr): 325 | super(StubProperty, self).__init__(obj, attr) 326 | property.__init__(self, lambda x: self(), 327 | lambda x, val: self.setter(val), 328 | lambda x: self.deleter()) 329 | # In order to stub out a property we have ask the class for the 330 | # propery object that was created we python execute class code. 331 | if inspect.isclass(obj): 332 | self._instance = obj 333 | else: 334 | self._instance = obj.__class__ 335 | 336 | # Use a simple Mock object for the deleter and setter. Use same 337 | # namespace as property type so that it simply works. 338 | # Annoying circular reference requires importing here. Would like to 339 | # see this cleaned up. @AW 340 | from .mock import Mock 341 | self._obj = getattr(self._instance, attr) 342 | self.setter = Mock() 343 | self.deleter = Mock() 344 | 345 | setattr(self._instance, self._attr, self) 346 | 347 | def call_orig(self, *args, **kwargs): 348 | ''' 349 | Calls the original function. 350 | ''' 351 | # TODO: this is probably the most complicated one to implement. Will 352 | # figure it out eventually. 353 | raise NotImplementedError("property spies are not supported") 354 | 355 | @property 356 | def name(self): 357 | return "%s.%s" % (self._instance.__name__, self._attr) 358 | 359 | def _teardown(self): 360 | ''' 361 | Replace the original method. 362 | ''' 363 | setattr(self._instance, self._attr, self._obj) 364 | 365 | 366 | class StubMethod(Stub): 367 | 368 | ''' 369 | Stub a method. 370 | ''' 371 | 372 | def __init__(self, obj, attr=None): 373 | ''' 374 | Initialize with an object of type MethodType 375 | ''' 376 | super(StubMethod, self).__init__(obj, attr) 377 | if not self._attr: 378 | # python3 379 | if sys.version_info.major == 3: # hasattr(obj,'__func__'): 380 | self._attr = obj.__func__.__name__ 381 | else: 382 | self._attr = obj.im_func.func_name 383 | 384 | if sys.version_info.major == 3: # hasattr(obj, '__self__'): 385 | self._instance = obj.__self__ 386 | else: 387 | self._instance = obj.im_self 388 | else: 389 | self._instance = self._obj 390 | self._obj = getattr(self._instance, self._attr) 391 | setattr(self._instance, self._attr, self) 392 | 393 | @property 394 | def name(self): 395 | from .mock import Mock # Import here for the same reason as above. 396 | if hasattr(self._obj, 'im_class'): 397 | if issubclass(self._obj.im_class, Mock): 398 | return self._obj.im_self._name 399 | 400 | # Always use the class to get the name 401 | klass = self._instance 402 | if not inspect.isclass(self._instance): 403 | klass = self._instance.__class__ 404 | 405 | return "%s.%s" % (klass.__name__, self._attr) 406 | 407 | def call_orig(self, *args, **kwargs): 408 | ''' 409 | Calls the original function. 410 | ''' 411 | if hasattr(self._obj, '__self__') and \ 412 | inspect.isclass(self._obj.__self__) and \ 413 | self._obj.__self__ is self._instance: 414 | return self._obj.__func__(self._instance, *args, **kwargs) 415 | elif hasattr(self._obj, 'im_self') and \ 416 | inspect.isclass(self._obj.im_self) and \ 417 | self._obj.im_self is self._instance: 418 | return self._obj.im_func(self._instance, *args, **kwargs) 419 | else: 420 | return self._obj(*args, **kwargs) 421 | 422 | def _teardown(self): 423 | ''' 424 | Put the original method back in place. This will also handle the 425 | special case when it putting back a class method. 426 | 427 | The following code snippet best describe why it fails using settar, 428 | the class method would be replaced with a bound method not a class 429 | method. 430 | 431 | >>> class Example(object): 432 | ... @classmethod 433 | ... def a_classmethod(self): 434 | ... pass 435 | ... 436 | >>> Example.__dict__['a_classmethod'] 437 | 438 | >>> orig = getattr(Example, 'a_classmethod') 439 | >>> orig 440 | > 441 | >>> setattr(Example, 'a_classmethod', orig) 442 | >>> Example.__dict__['a_classmethod'] 443 | > 444 | 445 | The only way to figure out if this is a class method is to check and 446 | see if the bound method im_self is a class, if so then we need to wrap 447 | the function object (im_func) with class method before setting it back 448 | on the class. 449 | ''' 450 | # Figure out if this is a class method and we're unstubbing it on the 451 | # class to which it belongs. This addresses an edge case where a 452 | # module can expose a method of an instance. e.g gevent. 453 | if hasattr(self._obj, '__self__') and \ 454 | inspect.isclass(self._obj.__self__) and \ 455 | self._obj.__self__ is self._instance: 456 | setattr( 457 | self._instance, self._attr, classmethod(self._obj.__func__)) 458 | elif hasattr(self._obj, 'im_self') and \ 459 | inspect.isclass(self._obj.im_self) and \ 460 | self._obj.im_self is self._instance: 461 | # Wrap it and set it back on the class 462 | setattr(self._instance, self._attr, classmethod(self._obj.im_func)) 463 | else: 464 | setattr(self._instance, self._attr, self._obj) 465 | 466 | 467 | class StubFunction(Stub): 468 | 469 | ''' 470 | Stub a function. 471 | ''' 472 | 473 | def __init__(self, obj, attr=None): 474 | ''' 475 | Initialize with an object that is an unbound method 476 | ''' 477 | super(StubFunction, self).__init__(obj, attr) 478 | if not self._attr: 479 | if getattr(obj, '__module__', None): 480 | self._instance = sys.modules[obj.__module__] 481 | elif getattr(obj, '__self__', None): 482 | self._instance = obj.__self__ 483 | else: 484 | raise UnsupportedStub("Failed to find instance of %s" % (obj)) 485 | 486 | if getattr(obj, 'func_name', None): 487 | self._attr = obj.func_name 488 | elif getattr(obj, '__name__', None): 489 | self._attr = obj.__name__ 490 | else: 491 | raise UnsupportedStub("Failed to find name of %s" % (obj)) 492 | else: 493 | self._instance = self._obj 494 | self._obj = getattr(self._instance, self._attr) 495 | 496 | # This handles the case where we're stubbing a special method that's 497 | # inherited from object, and so instead of calling setattr on teardown, 498 | # we want to call delattr. This is particularly important for not 499 | # seeing those stupid DeprecationWarnings after StubNew 500 | self._was_object_method = False 501 | if hasattr(self._instance, '__dict__'): 502 | self._was_object_method = \ 503 | self._attr not in self._instance.__dict__.keys() and\ 504 | self._attr in object.__dict__.keys() 505 | setattr(self._instance, self._attr, self) 506 | 507 | @property 508 | def name(self): 509 | return "%s.%s" % (self._instance.__name__, self._attr) 510 | 511 | def call_orig(self, *args, **kwargs): 512 | ''' 513 | Calls the original function. 514 | ''' 515 | # TODO: Does this change if was_object_method? 516 | return self._obj(*args, **kwargs) 517 | 518 | def _teardown(self): 519 | ''' 520 | Replace the original method. 521 | ''' 522 | if not self._was_object_method: 523 | setattr(self._instance, self._attr, self._obj) 524 | else: 525 | delattr(self._instance, self._attr) 526 | 527 | 528 | class StubNew(StubFunction): 529 | 530 | ''' 531 | Stub out the constructor, but hide the fact that we're stubbing "__new__" 532 | and act more like we're stubbing "__init__". Needs to use the logic in 533 | the StubFunction ctor. 534 | ''' 535 | _cache = {} 536 | 537 | def __new__(self, klass, *args): 538 | ''' 539 | Because we're not saving the stub into any attribute, then we have 540 | to do some faking here to return the same handle. 541 | ''' 542 | rval = self._cache.get(klass) 543 | if not rval: 544 | rval = self._cache[klass] = super( 545 | StubNew, self).__new__(self, *args) 546 | rval._allow_init = True 547 | else: 548 | rval._allow_init = False 549 | return rval 550 | 551 | def __init__(self, obj): 552 | ''' 553 | Overload the initialization so that we can hack access to __new__. 554 | ''' 555 | if self._allow_init: 556 | self._new = obj.__new__ 557 | super(StubNew, self).__init__(obj, '__new__') 558 | self._type = obj 559 | 560 | def __call__(self, *args, **kwargs): 561 | ''' 562 | When calling the new function, strip out the first arg which is 563 | the type. In this way, the mocker writes their expectation as if it 564 | was an __init__. 565 | ''' 566 | return super(StubNew, self).__call__(*(args[1:]), **kwargs) 567 | 568 | def call_orig(self, *args, **kwargs): 569 | ''' 570 | Calls the original function. Simulates __new__ and __init__ together. 571 | ''' 572 | rval = super(StubNew, self).call_orig(self._type) 573 | rval.__init__(*args, **kwargs) 574 | return rval 575 | 576 | def _teardown(self): 577 | ''' 578 | Overload so that we can clear out the cache after a test run. 579 | ''' 580 | # __new__ is a super-special case in that even when stubbing a class 581 | # which implements its own __new__ and subclasses object, the 582 | # "Class.__new__" reference is a staticmethod and not a method (or 583 | # function). That confuses the "was_object_method" logic in 584 | # StubFunction which then fails to delattr and from then on the class 585 | # is corrupted. So skip that teardown and use a __new__-specific case. 586 | setattr(self._instance, self._attr, staticmethod(self._new)) 587 | StubNew._cache.pop(self._type) 588 | 589 | 590 | class StubUnboundMethod(Stub): 591 | 592 | ''' 593 | Stub an unbound method. 594 | ''' 595 | 596 | def __init__(self, obj, attr=None): 597 | ''' 598 | Initialize with an object that is an unbound method 599 | ''' 600 | # Note: It doesn't appear that there's any way to support stubbing 601 | # by method in python3 because an unbound method has no reference 602 | # to its parent class, it just looks like a regular function 603 | super(StubUnboundMethod, self).__init__(obj, attr) 604 | if self._attr is None: 605 | self._instance = obj.im_class 606 | self._attr = obj.im_func.func_name 607 | else: 608 | self._obj = getattr(obj, attr) 609 | self._instance = obj 610 | setattr(self._instance, self._attr, self) 611 | 612 | @property 613 | def name(self): 614 | return "%s.%s" % (self._instance.__name__, self._attr) 615 | 616 | def call_orig(self, *args, **kwargs): 617 | ''' 618 | Calls the original function. 619 | ''' 620 | # TODO: Figure out if this can be implemented. The challenge is that 621 | # the context of "self" has to be passed in as an argument, but there's 622 | # not necessarily a generic way of doing that. It may fall out as a 623 | # side-effect of the actual implementation of spies. 624 | raise NotImplementedError("unbound method spies are not supported") 625 | 626 | def _teardown(self): 627 | ''' 628 | Replace the original method. 629 | ''' 630 | setattr(self._instance, self._attr, self._obj) 631 | 632 | 633 | class StubMethodWrapper(Stub): 634 | 635 | ''' 636 | Stub a method-wrapper. 637 | ''' 638 | 639 | def __init__(self, obj): 640 | ''' 641 | Initialize with an object that is a method wrapper. 642 | ''' 643 | super(StubMethodWrapper, self).__init__(obj) 644 | self._instance = obj.__self__ 645 | self._attr = obj.__name__ 646 | setattr(self._instance, self._attr, self) 647 | 648 | @property 649 | def name(self): 650 | return "%s.%s" % (self._instance.__class__.__name__, self._attr) 651 | 652 | def call_orig(self, *args, **kwargs): 653 | ''' 654 | Calls the original function. 655 | ''' 656 | return self._obj(*args, **kwargs) 657 | 658 | def _teardown(self): 659 | ''' 660 | Replace the original method. 661 | ''' 662 | setattr(self._instance, self._attr, self._obj) 663 | 664 | 665 | class StubWrapperDescriptor(Stub): 666 | 667 | ''' 668 | Stub a wrapper-descriptor. Only works when we can fetch it by name. Because 669 | the w-d object doesn't contain both the instance ref and the attribute name 670 | to be able to look it up. Used for mocking object.__init__ and related 671 | builtin methods when subclasses that don't overload those. 672 | ''' 673 | 674 | def __init__(self, obj, attr_name): 675 | ''' 676 | Initialize with an object that is a method wrapper. 677 | ''' 678 | super(StubWrapperDescriptor, self).__init__(obj, attr_name) 679 | self._orig = getattr(self._obj, self._attr) 680 | setattr(self._obj, self._attr, self) 681 | 682 | @property 683 | def name(self): 684 | return "%s.%s" % (self._obj.__name__, self._attr) 685 | 686 | def call_orig(self, *args, **kwargs): 687 | ''' 688 | Calls the original function. 689 | ''' 690 | return self._orig(self._obj, *args, **kwargs) 691 | 692 | def _teardown(self): 693 | ''' 694 | Replace the original method. 695 | ''' 696 | setattr(self._obj, self._attr, self._orig) 697 | --------------------------------------------------------------------------------