├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── exam.sublime-project ├── exam ├── __init__.py ├── asserts.py ├── cases.py ├── decorators.py ├── fixtures.py ├── helpers.py ├── mock.py └── objects.py ├── setup.py └── tests ├── __init__.py ├── dummy.py ├── test_asserts.py ├── test_cases.py ├── test_decorators.py ├── test_exam.py ├── test_helpers.py ├── test_mock.py └── test_objects.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | *.egg 4 | *.egg-info 5 | dist/ 6 | *.sublime-workspace 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | - "3.2" 7 | - "3.3" 8 | - "pypy" 9 | 10 | install: python setup.py develop 11 | script: make test 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | group :development do 4 | gem 'guard', '1.4.0' 5 | gem 'rb-readline' 6 | gem 'rb-fsevent', :require => false 7 | gem 'guard-shell' 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | guard (1.4.0) 5 | listen (>= 0.4.2) 6 | thor (>= 0.14.6) 7 | guard-shell (0.5.1) 8 | guard (>= 1.1.0) 9 | listen (0.5.3) 10 | rb-fsevent (0.9.2) 11 | rb-readline (0.4.2) 12 | thor (0.16.0) 13 | 14 | PLATFORMS 15 | ruby 16 | 17 | DEPENDENCIES 18 | guard (= 1.4.0) 19 | guard-shell 20 | rb-fsevent 21 | rb-readline 22 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :shell do 2 | watch(/^(tests|exam)(.*)\.py$/) do |match| 3 | puts `python setup.py nosetests` 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2012 Jeff Pollard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include tests *.py 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = $(shell python setup.py --version) 2 | 3 | test: 4 | python setup.py nosetests 5 | 6 | release: 7 | git tag $(VERSION) 8 | git push origin $(VERSION) 9 | git push origin master 10 | python setup.py sdist upload 11 | 12 | watch: 13 | bundle exec guard 14 | 15 | .PHONY: test release watch 16 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://api.travis-ci.org/Fluxx/exam.png?branch=master 2 | :target: http://travis-ci.org/fluxx/exam 3 | 4 | #### 5 | Exam 6 | #### 7 | 8 | .. image:: https://dl.dropbox.com/u/3663715/exam.jpeg 9 | 10 | Exam is a Python toolkit for writing better tests. It aims to remove a lot of the boiler plate testing code one often writes, while still following Python conventions and adhering to the unit testing interface. 11 | 12 | Installation 13 | ------------ 14 | 15 | A simple ``pip install exam`` should do the trick. 16 | 17 | Rationale 18 | --------- 19 | 20 | Aside from the obvious "does the code work?", writings tests has many additional goals and benefits: 21 | 22 | 1. If written semantically, reading tests can help demonstrate how the code is supposed to work to other developers. 23 | 2. If quick running, tests provide feedback during development that your changes are working or not having an adverse side effects. 24 | 3. If they're easy to write correctly, developers will write more tests and they will be of a higher quality. 25 | 26 | Unfortunately, the common pattern for writing Python unit tests tends to not offer any of these advantages. Often times results in inefficient and unnecessarily obtuse testing code. Additionally, common uses of the `mock` library can often result in repetitive boiler-plate code or inefficiency during test runs. 27 | 28 | `exam` aims to improve the state of Python test writing by providing a toolkit of useful functionality to make writing quick, correct and useful tests and as painless as possible. 29 | 30 | Usage 31 | -------- 32 | 33 | Exam features a collection of useful modules: 34 | 35 | ``exam.decorators`` 36 | ~~~~~~~~~~~~~~~~~~~ 37 | 38 | Exam has some useful decorators to make your tests easier to write and understand. To utilize the ``@before``, ``@after``, ``@around`` and ``@patcher`` decorators, you must mixin the ``exam.cases.Exam`` class into your test case. It implements the appropriate ``setUp()`` and ``tearDown()`` methods necessary to make the decorators work. 39 | 40 | Note that the ``@fixture`` decorator works without needing to be defined inside of an Exam class. Still, it's a best practice to add the ``Exam`` mixin to your test cases. 41 | 42 | All of the decorators in ``exam.decorators``, as well as the ``Exam`` test case are available for import from the main ``exam`` package as well. I.e.: 43 | 44 | .. code:: python 45 | 46 | from exam import Exam 47 | from exam import fixture, before, after, around, patcher 48 | 49 | ``exam.decorators.fixture`` 50 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 51 | 52 | The ``@fixture`` decorator turns a method into a property (similar to the ``@property`` decorator, but also memoizes the return value). This lets you reference the property in your tests, i.e. ``self.grounds``, and it will always reference the exact same instance every time. 53 | 54 | .. code:: python 55 | 56 | from exam.decorators import fixture 57 | from exam.cases import Exam 58 | 59 | class MyTest(Exam, TestCase): 60 | 61 | @fixture 62 | def user(self): 63 | return User(name='jeff') 64 | 65 | def test_user_name_is_jeff(self): 66 | assert self.user.name == 'jeff' 67 | 68 | As you can see, ``self.user`` was used to reference the ``user`` property defined above. 69 | 70 | If all your fixture method is doing is constructing a new instance of type or calling a class method, exam provides a shorthand inline ``fixture`` syntax for constructing fixture objects. Simply set a class variable equal to ``fixture(type_or_class_method)`` and exam will automatically call your type or class method. 71 | 72 | .. code:: python 73 | 74 | from exam.decorators import fixture 75 | from exam.cases import Exam 76 | 77 | class MyTest(Exam, TestCase): 78 | 79 | user = fixture(User, name='jeff') 80 | 81 | def test_user_name_is_jeff(self): 82 | assert self.user.name == 'jeff' 83 | 84 | Any ``*args`` or ``**kwargs`` passed to ``fixture(type_or_class_method)`` will be passed to the ``type_or_class_method`` when called. 85 | 86 | 87 | ``exam.decorators.before`` 88 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 89 | 90 | The ``@before`` decorator adds the method to the list of methods which are run as part of the class's ``setUp()`` routine. 91 | 92 | .. code:: python 93 | 94 | from exam.decorators import before 95 | from exam.cases import Exam 96 | 97 | class MyTest(Exam, TestCase): 98 | 99 | @before 100 | def reset_database(self): 101 | mydb.reset() 102 | 103 | 104 | ``@before`` also hooks works through subclasses - that is to say, if a parent class has a ``@before`` hook in it, and you subclass it and define a 2nd ``@before`` hook in it, both ``@before`` hooks will be called. Exam runs the parent's ``@before`` hook first, then runs the childs'. Also, if your override a `@before` hook in your child class, the overridden method is run when the rest of the child classes `@before` hooks are run. 105 | 106 | For example, with hooks defined as such: 107 | 108 | .. code:: python 109 | 110 | from exam.decorators import before 111 | from exam.cases import Exam 112 | 113 | class MyTest(Exam, TestCase): 114 | 115 | @before 116 | def reset_database(self): 117 | print 'parent reset_db' 118 | 119 | @before 120 | def parent_hook(self): 121 | print 'parent hook' 122 | 123 | 124 | class RedisTest(MyTest): 125 | 126 | @before 127 | def reset_database(self): 128 | print 'child reset_db' 129 | 130 | @before 131 | def child_hook(self): 132 | print 'child hook' 133 | 134 | When Exam runs these hooks, the output would be: 135 | 136 | .. code:: python 137 | 138 | "prent hook" 139 | "child reset_db" 140 | "child hook" 141 | 142 | As you can see even though the parent class defines a ``reset_database``, because the child class overwrote it, the child's version is run instead, and also at the same time as the rest of the child's ``@before`` hooks. 143 | 144 | ``@before`` hooks can also be constructed with other functions in your test case, decorating actual test methods. When this strategy is used, Exam will run the function ``@before`` is constructed with before running that particular test method. 145 | 146 | .. code:: python 147 | 148 | from exam.decorators import before, fixture 149 | from exam.cases import Exam 150 | 151 | from myapp import User 152 | 153 | class MyTest(Exam, TestCase): 154 | 155 | user = fixture(User) 156 | 157 | @before 158 | def create_user(self): 159 | self.user.create() 160 | 161 | def confirm_user(self): 162 | self.user.confirm() 163 | 164 | @before(confirm_user) 165 | def test_confirmed_users_have_no_token(self): 166 | self.assertFalse(self.user.token) 167 | 168 | def test_user_display_name_exists(self): 169 | self.assertTrue(self.user.display_name) 170 | 171 | In the above example, the ``confirm_user`` method is run immediately before the ``test_confirmed_users_have_no_token`` method, but **not** the ``test_user_display_name_exists`` method. The ``@before`` globally decorated ``create_user`` method still runs before each test method. 172 | 173 | ``@before`` can also be constructed with multiple functions to call before running the test method: 174 | 175 | .. code:: python 176 | 177 | class MyTest(Exam, TestCase): 178 | 179 | @before(func1, func2) 180 | def test_does_things(self): 181 | does_things() 182 | 183 | In the above example, ``func1`` and ``func2`` are called in order before ``test_does_things`` is run. 184 | 185 | ``exam.decorators.after`` 186 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 187 | 188 | The compliment to ``@before``, ``@after`` adds the method to the list of methods which are run as part of the class's ``tearDown()`` routine. Like ``@before``, ``@after`` runs parent class ``@after`` hooks before running ones defined in child classes. 189 | 190 | .. code:: python 191 | 192 | from exam.decorators import after 193 | from exam.cases import Exam 194 | 195 | class MyTest(Exam, TestCase): 196 | 197 | @after 198 | def remove_temp_files(self): 199 | myapp.remove_temp_files() 200 | 201 | 202 | ``exam.decorators.around`` 203 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 204 | 205 | Methods decorated with ``@around`` act as a context manager around each test method. In your around method, you're responsible for calling ``yield`` where you want the test case to run: 206 | 207 | .. code:: python 208 | 209 | from exam.decorators import around 210 | from exam.cases import Exam 211 | 212 | class MyTest(Exam, TestCase): 213 | 214 | @around 215 | def run_in_transaction(self): 216 | db.begin_transaction() 217 | yield # Run the test 218 | db.rollback_transaction() 219 | 220 | ``@around`` also follows the same parent/child ordering rules as ``@before`` and ``@after``, so parent ``@arounds`` will run (up until the ``yield`` statement), then child ``@around``s will run. After the test method has finished, however, the rest of the child's ``@around`` will run, and then the parents'. This is done to preserve the normal behavior of nesting with context managers. 221 | 222 | 223 | ``exam.decorators.patcher`` 224 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 225 | 226 | The ``@patcher`` decorator is shorthand for the following boiler plate code: 227 | 228 | .. code:: python 229 | 230 | from mock import patch 231 | 232 | def setUp(self): 233 | self.stats_patcher = patch('mylib.stats', new=dummy_stats) 234 | self.stats = self.stats_patcher.start() 235 | 236 | def tearDown(self): 237 | self.stats_patcher.stop() 238 | 239 | Often, manually controlling a patch's start/stop is done to provide a test case property (here, ``self.stats``) for the mock object you are patching with. This is handy if you want the mock to have default behavior for most tests, but change it slightly for certain ones -- i.e absorb all calls most of the time, but for certain tests have it raise an exception. 240 | 241 | Using the ``@patcher`` decorator, the above code can simply be written as: 242 | 243 | .. code:: python 244 | 245 | from exam.decorators import patcher 246 | from exam.cases import Exam 247 | 248 | class MyTest(Exam, TestCase): 249 | 250 | @patcher('mylib.stats') 251 | def stats(self): 252 | return dummy_stats 253 | 254 | Exam takes care of starting and stopping the patcher appropriately, as well as constructing the ``patch`` object with the return value from the decorated method. 255 | 256 | If you're happy with the default constructed mock object for a patch (``MagicMock``), then ``patcher`` can simply be used as an inline as a function inside the class body. This method still starts and stops the patcher when needed, and returns the constructed ``MagicMock`` object, which you can set as a class attribute. Exam will add the ``MagicMock`` object to the test case as an instance attribute automatically. 257 | 258 | .. code:: python 259 | 260 | from exam.decorators import patcher 261 | from exam.cases import Exam 262 | 263 | class MyTest(Exam, TestCase): 264 | 265 | logger = patcher('coffee.logger') 266 | 267 | 268 | ``exam.decorators.patcher.object`` 269 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 270 | 271 | The ``patcher.object`` decorator provides the same features as the ``patcher`` decorator, but works with patching attributes of objects (similar to mock's ``mock.patch.object``). For example, here is how you would use patcher to patch the ``objects`` property of the ``User`` class: 272 | 273 | .. code:: python 274 | 275 | from exam.decorators import patcher 276 | from exam.cases import Exam 277 | 278 | from myapp import User 279 | 280 | class MyTest(Exam, TestCase): 281 | 282 | manager = patcher.object(User, 'objects') 283 | 284 | As with the vanilla ``patcher``, in your test case, ``self.manager`` will be the mock object that ``User.objects`` was patched with. 285 | 286 | 287 | ``exam.helpers`` 288 | ~~~~~~~~~~~~~~~~ 289 | 290 | The ``helpers`` module features a collection of helper methods for common testing patterns: 291 | 292 | ``exam.helpers.track`` 293 | ^^^^^^^^^^^^^^^^^^^^^^ 294 | 295 | The ``track`` helper is intended to assist in tracking call orders of independent mock objects. ``track`` is called with kwargs, where the key is the mock name (a string) and the value is the mock object you want to track. ``track`` returns a newly constructed ``MagicMock`` object, with each mock object attached at a attribute named after the mock name. 296 | 297 | For example, below ``track()`` creates a new mock with ``tracker.cool` as the ``cool_mock`` and ``tracker.heat`` as the ``heat_mock``. 298 | 299 | .. code:: python 300 | 301 | from exam.helpers import track 302 | 303 | @mock.patch('coffee.roast.heat') 304 | @mock.patch('coffee.roast.cool') 305 | def test_roasting_heats_then_cools_beans(self, cool_mock, heat_mock): 306 | tracker = track(heat=heat_mock, cool=cool_mock) 307 | roast.perform() 308 | tracker.assert_has_calls([mock.call.heat(), mock.call.cool()]) 309 | 310 | ``exam.helpers.rm_f`` 311 | ^^^^^^^^^^^^^^^^^^^^^ 312 | 313 | This is a simple helper that just removes all folders and files at a path: 314 | 315 | .. code:: python 316 | 317 | from exam.helpers import rm_f 318 | 319 | rm_f('/folder/i/do/not/care/about') 320 | 321 | ``exam.helpers.mock_import`` 322 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 323 | 324 | Removes most of the boiler plate code needed to mock imports, which usually consists of making a ``patch.dict`` from ``sys.modules``. Instead, the ``patch_import`` helper can simply be used as a decorator or context manager for when certain modules are imported. 325 | 326 | .. code:: python 327 | 328 | from exam.helpers import mock_import 329 | 330 | with mock_import('os.path') as my_os_path: 331 | import os.path as imported_os_path 332 | assert my_os_path is imported_os_path 333 | 334 | ``mock_import`` can also be used as a decorator, which passed the mock value to 335 | the testing method (like a normal ``@patch``) decorator: 336 | 337 | .. code:: python 338 | 339 | from exam.helpers import mock_import 340 | 341 | @mock_import('os.path') 342 | def test_method(self): 343 | import os.path as imported_os_path 344 | assert my_os_path is imported_os_path 345 | 346 | ``exam.helpers.effect`` 347 | ^^^^^^^^^^^^^^^^^^^^^^^ 348 | 349 | Helper class that is itself callable, whose return values when called are configured via the tuples passed in to the constructor. Useful to build ``side_effect`` callables for Mock objects. Raises TypeError if called with arguments that it was not configured with: 350 | 351 | >>> from exam.objects import call, effect 352 | >>> side_effect = effect((call(1), 'with 1'), (call(2), 'with 2')) 353 | >>> side_effect(1) 354 | 'with 1' 355 | >>> side_effect(2) 356 | 'with 2' 357 | 358 | Call argument equality is checked via equality (==) of the ``call``` object, which is the 0th item of the configuration tuple passed in to the ``effect`` constructor. By default, ``call`` objects are just ``mock.call`` objects. 359 | 360 | If you would like to customize this behavior, subclass `effect` and redefine your own `call_class` class variable. I.e. 361 | 362 | .. code:: python 363 | 364 | class myeffect(effect): 365 | call_class = my_call_class 366 | 367 | ``exam.mock`` 368 | ~~~~~~~~~~~~~ 369 | 370 | Exam has a subclass of the normal ``mock.Mock`` object that adds a few more useful methods to your mock objects. Use it in place of a normal ``Mock`` object: 371 | 372 | .. code:: python 373 | 374 | from exam.mock import Mock 375 | 376 | mock_user = Mock(spec=User) 377 | 378 | The subclass has the following extra methods: 379 | 380 | * ``assert_called()`` - Asserts the mock was called at least once. 381 | * ``assert_not_called()`` - Asserts the mock has never been called. 382 | * ``assert_not_called_with(*args, **kwargs)`` - Asserts the mock was not most recently called with the specified ``*args`` and ``**kwargs``. 383 | * ``assert_not_called_once_with(*args, **kwargs)`` - Asserts the mock has only every been called once with the specified ``*args`` and ``**kwargs``. 384 | * ``assert_not_any_call(*args, **kwargs)`` - Asserts the mock has never been called with the specified ``*args`` and ``**kwargs``. 385 | 386 | ``exam.fixtures`` 387 | ~~~~~~~~~~~~~~~~~ 388 | 389 | Helpful fixtures that you may want to use in your tests: 390 | 391 | * ``exam.fixtures.two_px_square_image`` - Image data as a string of a 2px square image. 392 | * ``exam.fixtures.one_px_spacer`` - Image data as a string of a 1px square spacer image. 393 | 394 | ``exam.objects`` 395 | ~~~~~~~~~~~~~~~~ 396 | 397 | Useful objects for use in testing: 398 | 399 | ``exam.objects.noop`` - callable object that always returns ``None``. no matter how it was called. 400 | 401 | ``exam.asserts`` 402 | ~~~~~~~~~~~~~~~~ 403 | 404 | The `asserts` module contains an `AssertsMixin` class, which is mixed into the main `Exam` test case mixin. It contains additional asserts beyond the ones in Python's `unittest`. 405 | 406 | ``assertChanges`` 407 | ^^^^^^^^^^^^^^^^^ 408 | 409 | Used when you want to assert that a section of code changes a value. For example, imagine if you had a function that changed a soldier's rank. 410 | 411 | To properly test this, you should save that soldier's rank to a temporary variable, then run the function to change the rank, and then finally assert that the rank is the new expected value, as well as **not** the old value: 412 | 413 | .. code:: python 414 | 415 | test_changes_rank(self): 416 | old_rank = self.soldier.rank 417 | promote(self.soldier, 'general') 418 | self.assertEqual(self.soldier.rank, 'general') 419 | self.assertNotEqual(self.soldier.rank, old_rank) 420 | 421 | Checking the old rank is not the same is the new rank is important. If, for some reason there is a bug or something to where ``self.soldier`` is created with the rank of ``general``, but ``promote`` is not working, this test would still pass! 422 | 423 | To solve this, you can use Exam's ``assertChanges``: 424 | 425 | .. code:: python 426 | 427 | def test_changes_rank(self): 428 | with self.assertChanges(getattr, self.soldier, 'rank', after='general'): 429 | promote(self.soldier, 'general') 430 | 431 | This assert is doing a few things. 432 | 433 | 1. It asserts that the rank once the context is run is the expected ``general``. 434 | 2. It asserts that the context **changes** the value of ``self.soldier.rank``. 435 | 3. It doesn't actually care what the old value of ``self.soldier.rank`` was, as long as it changed when the context was run. 436 | 437 | The definition of ``assertChanges`` is: 438 | 439 | .. code:: python 440 | 441 | def assertChanges(thing, *args, **kwargs) 442 | 443 | 1. You pass it a ``thing``, which which be a callable. 444 | 2. ``assertChanges`` then calls your ``thing`` with any ``*args`` and ``**kwargs`` additionally passed in and captures the value as the "before" value. 445 | 3. The context is run, and then the callable is captured again as the "after" value. 446 | 4. If before and after are not different, an ``AssertionError`` is raised. 447 | 5. Additionally, if the special kwarg ``before`` or ``after`` are passed, those values are extracted and saved. In this case an ``AssertionError`` can also be raised if the "before" and/or "after" values provided do not match their extracted values. 448 | 449 | ``assertDoesNotChange`` 450 | ^^^^^^^^^^^^^^^^^^^^^^^ 451 | 452 | Similar to ``assertChanges``, ``assertDoesNotChange`` asserts that the code inside the context does not change the value from the callable: 453 | 454 | .. code:: python 455 | 456 | def test_does_not_change_rank(self): 457 | with self.assertDoesNotChange(getattr, self.soldier, 'rank'): 458 | self.soldier.march() 459 | 460 | Unlike ``assertChanges``, ``assertDoesNotChange`` does not take ``before`` or ``after`` kwargs. It simply asserts that the value of the callable did not change when the context was run. 461 | 462 | License 463 | ------- 464 | 465 | Exam is MIT licensed. Please see the ``LICENSE`` file for details. 466 | -------------------------------------------------------------------------------- /exam.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "folder_exclude_patterns": [ 5 | "*.egg", 6 | "build", 7 | "dist", 8 | "*.egg-info", 9 | "doc/_*", 10 | ".ropeproject" 11 | ], 12 | "file_exclude_patterns": [ 13 | "*.egg", 14 | "*.sublime-workspace", 15 | "*_pb2.py" 16 | ], 17 | "path": "." 18 | } 19 | ], 20 | "settings": { 21 | "tab_size": 4, 22 | "translate_tabs_to_spaces": true, 23 | "trim_trailing_white_space_on_save": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /exam/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from exam.cases import Exam # NOQA 4 | from exam.helpers import intercept # NOQA 5 | from exam.decorators import before, after, around, fixture, patcher # NOQA 6 | -------------------------------------------------------------------------------- /exam/asserts.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from operator import eq, ne 3 | 4 | 5 | IRRELEVANT = object() 6 | 7 | 8 | class ChangeWatcher(object): 9 | 10 | POSTCONDITION_FAILURE_MESSAGE = { 11 | ne: 'Value did not change', 12 | eq: 'Value changed from {before} to {after}', 13 | 'invalid': 'Value changed to {after}, not {expected_after}' 14 | } 15 | 16 | def __init__(self, comparator, check, *args, **kwargs): 17 | self.check = check 18 | self.comparator = comparator 19 | 20 | self.args = args 21 | self.kwargs = kwargs 22 | 23 | self.expected_before = kwargs.pop('before', IRRELEVANT) 24 | self.expected_after = kwargs.pop('after', IRRELEVANT) 25 | 26 | def __enter__(self): 27 | self.before = self.__apply() 28 | 29 | if not self.expected_before is IRRELEVANT: 30 | check = self.comparator(self.before, self.expected_before) 31 | message = "Value before is {before}, not {expected_before}" 32 | 33 | assert not check, message.format(**vars(self)) 34 | 35 | def __exit__(self, exec_type, exec_value, traceback): 36 | if exec_type is not None: 37 | return False # reraises original exception 38 | 39 | self.after = self.__apply() 40 | 41 | met_precondition = self.comparator(self.before, self.after) 42 | after_value_matches = self.after == self.expected_after 43 | 44 | # Changed when it wasn't supposed to, or, didn't change when it was 45 | if not met_precondition: 46 | self.__raise_postcondition_error(self.comparator) 47 | # Do care about the after value, but it wasn't equal 48 | elif self.expected_after is not IRRELEVANT and not after_value_matches: 49 | self.__raise_postcondition_error('invalid') 50 | 51 | def __apply(self): 52 | return self.check(*self.args, **self.kwargs) 53 | 54 | def __raise_postcondition_error(self, key): 55 | message = self.POSTCONDITION_FAILURE_MESSAGE[key] 56 | raise AssertionError(message.format(**vars(self))) 57 | 58 | 59 | class AssertsMixin(object): 60 | assertChanges = partial(ChangeWatcher, ne) 61 | assertDoesNotChange = partial( 62 | ChangeWatcher, 63 | eq, 64 | before=IRRELEVANT, 65 | after=IRRELEVANT 66 | ) 67 | -------------------------------------------------------------------------------- /exam/cases.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from exam.decorators import before, after, around, patcher # NOQA 4 | from exam.objects import noop # NOQA 5 | from exam.asserts import AssertsMixin 6 | 7 | import inspect 8 | 9 | 10 | class MultipleGeneratorsContextManager(object): 11 | 12 | def __init__(self, *generators): 13 | self.generators = generators 14 | 15 | def __enter__(self, *args, **kwargs): 16 | [next(g) for g in self.generators] 17 | 18 | def __exit__(self, *args, **kwargs): 19 | for generator in reversed(self.generators): 20 | try: 21 | next(generator) 22 | except StopIteration: 23 | pass 24 | 25 | 26 | class Exam(AssertsMixin): 27 | 28 | @before 29 | def __setup_patchers(self): 30 | for attr, patchr in self.__attrs_of_type(patcher): 31 | patch_object = patchr.build_patch(self) 32 | setattr(self, attr, patch_object.start()) 33 | self.addCleanup(patch_object.stop) 34 | 35 | def __attrs_of_type(self, kind): 36 | for base in reversed(inspect.getmro(type(self))): 37 | for attr, class_value in vars(base).items(): 38 | resolved_value = getattr(type(self), attr, False) 39 | 40 | if type(resolved_value) is not kind: 41 | continue 42 | # If the attribute inside of this base is not the exact same 43 | # value as the one in type(self), that means that it's been 44 | # overwritten somewhere down the line and we shall skip it 45 | elif class_value is not resolved_value: 46 | continue 47 | else: 48 | yield attr, resolved_value 49 | 50 | def __run_hooks(self, hook): 51 | for _, value in self.__attrs_of_type(hook): 52 | value(self) 53 | 54 | def run(self, *args, **kwargs): 55 | generators = (value(self) for _, value in self.__attrs_of_type(around)) 56 | with MultipleGeneratorsContextManager(*generators): 57 | self.__run_hooks(before) 58 | getattr(super(Exam, self), 'run', noop)(*args, **kwargs) 59 | self.__run_hooks(after) 60 | -------------------------------------------------------------------------------- /exam/decorators.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from mock import patch 4 | from functools import partial, wraps 5 | import types 6 | 7 | import exam.cases 8 | 9 | 10 | class fixture(object): 11 | 12 | def __init__(self, thing, *args, **kwargs): 13 | self.thing = thing 14 | self.args = args 15 | self.kwargs = kwargs 16 | 17 | def __get__(self, testcase, type=None): 18 | if not testcase: 19 | # Test case fixture was accesse as a class property, so just return 20 | # this fixture itself. 21 | return self 22 | elif self not in testcase.__dict__: 23 | # If this fixture is not present in the test case's __dict__, 24 | # freshly apply this fixture and store that in the dict, keyed by 25 | # self 26 | application = self.__apply(testcase)(*self.args, **self.kwargs) 27 | testcase.__dict__[self] = application 28 | 29 | return testcase.__dict__[self] 30 | 31 | def __apply(self, testcase): 32 | # If self.thing is a method type, it means that the function is already 33 | # bound to a class and therefore we should treat it just like a normal 34 | # functuion and return it. 35 | if type(self.thing) in (type, types.MethodType): 36 | return self.thing 37 | # If not, it means that's it's a vanilla function, 38 | # so either a decorated instance method in the test case 39 | # body or a lambda. In either of those 40 | # cases, it's called with the test case instance (self) to the author. 41 | else: 42 | return partial(self.thing, testcase) 43 | 44 | 45 | class base(object): 46 | 47 | def __init__(self, *things): 48 | self.init_callables = things 49 | 50 | def __call__(self, instance): 51 | return self.init_callables[0](instance) 52 | 53 | 54 | class before(base): 55 | 56 | def __call__(self, thing): 57 | # There a couple possible situations at this point: 58 | # 59 | # If ``thing`` is an instance of a test case, this means that we 60 | # ``init_callable`` is the function we want to decorate. As such, 61 | # simply call that callable with the instance. 62 | if isinstance(thing, exam.cases.Exam): 63 | return self.init_callables[0](thing) 64 | # If ``thing is not an instance of the test case, it means thi before 65 | # hook was constructed with a callable that we need to run before 66 | # actually running the decorated function. 67 | # It also means that ``thing`` is the function we're 68 | # decorating, so we need to return a callable that 69 | # accepts a test case instance and, when called, calls the 70 | # ``init_callable`` first, followed by the actual function we are 71 | # decorating. 72 | else: 73 | @wraps(thing) 74 | def inner(testcase): 75 | [f(testcase) for f in self.init_callables] 76 | thing(testcase) 77 | 78 | return inner 79 | 80 | 81 | class after(base): 82 | pass 83 | 84 | 85 | class around(base): 86 | pass 87 | 88 | 89 | class patcher(object): 90 | 91 | def __init__(self, *args, **kwargs): 92 | self.args = args 93 | self.kwargs = kwargs 94 | self.func = None 95 | self.patch_func = patch 96 | 97 | def __call__(self, func): 98 | self.func = func 99 | return self 100 | 101 | def build_patch(self, instance): 102 | if self.func: 103 | self.kwargs['new'] = self.func(instance) 104 | 105 | return self.patch_func(*self.args, **self.kwargs) 106 | 107 | @classmethod 108 | def object(cls, *args, **kwargs): 109 | instance = cls(*args, **kwargs) 110 | instance.patch_func = patch.object 111 | return instance 112 | -------------------------------------------------------------------------------- /exam/fixtures.py: -------------------------------------------------------------------------------- 1 | #: A string representation of a 2px square GIF, suitable for use in PIL. 2 | two_px_square_image = ( 3 | 'GIF87a\x02\x00\x02\x00\xb3\x00\x00\x00\x00\x00\xff\xff\xff\x00\x00' + 4 | '\x00\x00\x00\x00\x00\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00' + 5 | '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + 6 | '\x00\x00\x00\x00\x00\x00\x00\x00,\x00\x00\x00\x00\x02\x00\x02\x00' + 7 | '\x00\x04\x04\x10\x94\x02"\x00;' 8 | ) 9 | 10 | #: A string representation of a 1px square GIF, suitable for use in PIL. 11 | one_px_spacer = ( 12 | 'GIF89a\x01\x00\x01\x00\x80\x00\x00\xdb\xdf\xef\x00\x00\x00!\xf9\x04' + 13 | '\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D' + 14 | '\x01\x00;' 15 | ) 16 | -------------------------------------------------------------------------------- /exam/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import shutil 4 | import os 5 | import functools 6 | 7 | from mock import MagicMock, patch, call 8 | 9 | 10 | def rm_f(path): 11 | try: 12 | # Assume it's a directory 13 | shutil.rmtree(path, ignore_errors=True) 14 | except OSError: 15 | # Directory delete failed, so it's likely a file 16 | os.remove(path) 17 | 18 | 19 | def track(**mocks): 20 | tracker = MagicMock() 21 | 22 | for name, mocker in mocks.items(): 23 | tracker.attach_mock(mocker, name) 24 | 25 | return tracker 26 | 27 | 28 | def intercept(obj, methodname, wrapper): 29 | """ 30 | Wraps an existing method on an object with the provided generator, which 31 | will be "sent" the value when it yields control. 32 | 33 | :: 34 | 35 | >>> def ensure_primary_key_is_set(): 36 | ... assert model.pk is None 37 | ... saved = yield 38 | ... aasert model is saved 39 | ... assert model.pk is not None 40 | ... 41 | >>> intercept(model, 'save', ensure_primary_key_is_set) 42 | >>> model.save() 43 | 44 | :param obj: the object that has the method to be wrapped 45 | :type obj: :class:`object` 46 | :param methodname: the name of the method that will be wrapped 47 | :type methodname: :class:`str` 48 | :param wrapper: the wrapper 49 | :type wrapper: generator callable 50 | """ 51 | original = getattr(obj, methodname) 52 | 53 | def replacement(*args, **kwargs): 54 | wrapfn = wrapper(*args, **kwargs) 55 | wrapfn.send(None) 56 | result = original(*args, **kwargs) 57 | try: 58 | wrapfn.send(result) 59 | except StopIteration: 60 | return result 61 | else: 62 | raise AssertionError('Generator did not stop') 63 | 64 | def unwrap(): 65 | """ 66 | Restores the method to it's original (unwrapped) state. 67 | """ 68 | setattr(obj, methodname, original) 69 | 70 | replacement.unwrap = unwrap 71 | 72 | setattr(obj, methodname, replacement) 73 | 74 | 75 | class mock_import(patch.dict): 76 | 77 | FROM_X_GET_Y = lambda s, x, y: getattr(x, y) 78 | 79 | def __init__(self, path): 80 | self.mock = MagicMock() 81 | self.path = path 82 | self.modules = {self.base: self.mock} 83 | 84 | for i in range(len(self.remainder)): 85 | tail_parts = self.remainder[0:i + 1] 86 | key = '.'.join([self.base] + tail_parts) 87 | reduction = functools.reduce(self.FROM_X_GET_Y, 88 | tail_parts, self.mock) 89 | self.modules[key] = reduction 90 | 91 | super(mock_import, self).__init__('sys.modules', self.modules) 92 | 93 | @property 94 | def base(self): 95 | return self.path.split('.')[0] 96 | 97 | @property 98 | def remainder(self): 99 | return self.path.split('.')[1:] 100 | 101 | def __enter__(self): 102 | super(mock_import, self).__enter__() 103 | return self.modules[self.path] 104 | 105 | def __call__(self, func): 106 | super(mock_import, self).__call__(func) 107 | 108 | @functools.wraps(func) 109 | def inner(*args, **kwargs): 110 | args = list(args) 111 | args.insert(1, self.modules[self.path]) 112 | 113 | with self: 114 | func(*args, **kwargs) 115 | 116 | return inner 117 | 118 | 119 | class effect(list): 120 | """ 121 | Helper class that is itself callable, whose return values when called are 122 | configured via the tuples passed in to the constructor. Useful to build 123 | ``side_effect`` callables for Mock objects. Raises TypeError if 124 | called with arguments that it was not configured with: 125 | 126 | >>> from exam.objects import call, effect 127 | >>> side_effect = effect((call(1), 'with 1'), (call(2), 'with 2')) 128 | >>> side_effect(1) 129 | 'with 1' 130 | >>> side_effect(2) 131 | 'with 2' 132 | 133 | Call argument equality is checked via equality (==) 134 | of the ``call``` object, which is the 0th item of the configuration 135 | tuple passed in to the ``effect`` constructor. 136 | By default, ``call`` objects are just ``mock.call`` objects. 137 | 138 | If you would like to customize this behavior, 139 | subclass `effect` and redefine your own `call_class` 140 | class variable. I.e. 141 | 142 | class myeffect(effect): 143 | call_class = my_call_class 144 | """ 145 | 146 | call_class = call 147 | 148 | def __init__(self, *calls): 149 | """ 150 | :param calls: Two-item tuple containing call and the return value. 151 | :type calls: :class:`effect.call_class` 152 | """ 153 | super(effect, self).__init__(calls) 154 | 155 | def __call__(self, *args, **kwargs): 156 | this_call = self.call_class(*args, **kwargs) 157 | 158 | for call_obj, return_value in self: 159 | if call_obj == this_call: 160 | return return_value 161 | 162 | raise TypeError('Unknown effect for: %r, %r' % (args, kwargs)) 163 | -------------------------------------------------------------------------------- /exam/mock.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from mock import Mock as BaseMock 4 | from mock import call 5 | 6 | 7 | class Mock(BaseMock): 8 | 9 | def assert_called(self): 10 | assert self.called 11 | 12 | def assert_not_called(self): 13 | assert not self.called 14 | 15 | def assert_not_called_with(self, *args, **kwargs): 16 | assert not call(*args, **kwargs) == self.call_args 17 | 18 | def assert_not_called_once_with(self, *args, **kwargs): 19 | assert len(self.__calls_matching(*args, **kwargs)) is not 1 20 | 21 | def assert_not_any_call(self, *args, **kwargs): 22 | assert len(self.__calls_matching(*args, **kwargs)) is 0 23 | 24 | def __calls_matching(self, *args, **kwargs): 25 | calls_match = lambda other_call: call(*args, **kwargs) == other_call 26 | return list(filter(calls_match, self.call_args_list)) 27 | -------------------------------------------------------------------------------- /exam/objects.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | 4 | def always(value): 5 | return lambda *a, **k: value 6 | 7 | noop = no_op = always(None) 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from setuptools import setup, find_packages 5 | 6 | try: 7 | import multiprocessing # NOQA 8 | except ImportError: 9 | pass 10 | 11 | install_requires = ['mock'] 12 | lint_requires = ['pep8', 'pyflakes'] 13 | tests_require = ['nose'] 14 | 15 | if sys.version_info < (2, 7): 16 | tests_require.append('unittest2') 17 | 18 | setup_requires = [] 19 | if 'nosetests' in sys.argv[1:]: 20 | setup_requires.append('nose') 21 | 22 | setup( 23 | name='exam', 24 | version='0.10.6', 25 | author='Jeff Pollard', 26 | author_email='jeff.pollard@gmail.com', 27 | url='https://github.com/fluxx/exam', 28 | description='Helpers for better testing.', 29 | license='MIT', 30 | packages=find_packages(exclude=['tests', 'tests.*']), 31 | install_requires=install_requires, 32 | tests_require=tests_require, 33 | setup_requires=setup_requires, 34 | extras_require={ 35 | 'test': tests_require, 36 | 'all': install_requires + tests_require, 37 | 'docs': ['sphinx'] + tests_require, 38 | 'lint': lint_requires 39 | }, 40 | zip_safe=False, 41 | test_suite='nose.collector', 42 | classifiers=[ 43 | "Programming Language :: Python", 44 | "Programming Language :: Python :: 2", 45 | "Programming Language :: Python :: 2.6", 46 | "Programming Language :: Python :: 2.7", 47 | "Programming Language :: Python :: 3", 48 | "Programming Language :: Python :: 3.2", 49 | "Programming Language :: Python :: 3.3", 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | if sys.version_info < (2, 7): 5 | from unittest2 import TestCase # NOQA 6 | else: 7 | from unittest import TestCase # NOQA 8 | -------------------------------------------------------------------------------- /tests/dummy.py: -------------------------------------------------------------------------------- 1 | #: Module purely exists to test patching things. 2 | thing = True 3 | it = lambda: False 4 | 5 | 6 | def get_thing(): 7 | global thing 8 | return thing 9 | 10 | 11 | def get_it(): 12 | global it 13 | return it 14 | 15 | 16 | def get_prop(): 17 | return ThingClass.prop 18 | 19 | 20 | class ThingClass(object): 21 | prop = True 22 | -------------------------------------------------------------------------------- /tests/test_asserts.py: -------------------------------------------------------------------------------- 1 | from tests import TestCase 2 | 3 | from exam import Exam, fixture 4 | from exam.asserts import AssertsMixin 5 | 6 | 7 | class AssertChangesMixin(Exam, TestCase): 8 | 9 | case = fixture(AssertsMixin) 10 | thing = fixture(list) 11 | 12 | def no_op_context(self, *args, **kwargs): 13 | with self.case.assertChanges(len, self.thing, *args, **kwargs): 14 | pass 15 | 16 | def test_checks_change_on_callable_passed(self): 17 | with self.case.assertChanges(len, self.thing, before=0, after=1): 18 | self.thing.append(1) 19 | 20 | def test_after_check_asserts_ends_on_after_value(self): 21 | self.thing.append(1) 22 | with self.case.assertChanges(len, self.thing, after=2): 23 | self.thing.append(1) 24 | 25 | def test_before_check_asserts_starts_on_before_value(self): 26 | self.thing.append(1) 27 | with self.case.assertChanges(len, self.thing, before=1): 28 | self.thing.append(1) 29 | self.thing.append(2) 30 | 31 | def test_verifies_value_must_change_no_matter_what(self): 32 | self.thing.append(1) 33 | 34 | with self.assertRaises(AssertionError): 35 | self.no_op_context(after=1) 36 | 37 | with self.assertRaises(AssertionError): 38 | self.no_op_context(before=1) 39 | 40 | with self.assertRaises(AssertionError): 41 | self.no_op_context() 42 | 43 | def test_reraises_exception_if_raised_in_context(self): 44 | with self.assertRaises(NameError): 45 | with self.case.assertChanges(len, self.thing, after=5): 46 | self.thing.append(1) 47 | undefined_name 48 | 49 | def test_does_not_change_passes_if_no_change_was_made(self): 50 | with self.assertDoesNotChange(len, self.thing): 51 | pass 52 | 53 | def test_raises_assertion_error_if_value_changes(self): 54 | msg = 'Value changed from 0 to 1' 55 | with self.assertRaisesRegexp(AssertionError, msg): 56 | with self.assertDoesNotChange(len, self.thing): 57 | self.thing.append(1) 58 | 59 | def test_assertion_error_mentions_unexpected_result_at_after(self): 60 | msg = 'Value changed to 1, not 3' 61 | with self.assertRaisesRegexp(AssertionError, msg): 62 | with self.assertChanges(len, self.thing, after=3): 63 | self.thing.append(1) 64 | -------------------------------------------------------------------------------- /tests/test_cases.py: -------------------------------------------------------------------------------- 1 | from mock import sentinel 2 | from tests import TestCase 3 | 4 | from exam.decorators import before, after, around, patcher 5 | from exam.cases import Exam 6 | 7 | from tests.dummy import get_thing, get_it, get_prop, ThingClass 8 | 9 | 10 | class SimpleTestCase(object): 11 | """ 12 | Meant to act like a typical unittest.TestCase 13 | """ 14 | 15 | def __init__(self): 16 | self.cleanups = [] 17 | self.setups = 0 18 | self.teardowns = 0 19 | 20 | def setUp(self): 21 | self.setups += 1 22 | 23 | def tearDown(self): 24 | self.teardowns += 1 25 | 26 | def run(self, *args, **kwargs): 27 | # At this point in time, exam has run its before hooks and has super'd 28 | # to the TestCase (us), so, capture the state of calls 29 | self.calls_before_run = list(self.calls) 30 | self.vars_when_run = vars(self) 31 | 32 | def addCleanup(self, func): 33 | self.cleanups.append(func) 34 | 35 | 36 | class BaseTestCase(Exam, SimpleTestCase): 37 | """ 38 | Meant to act like a test case a typical user would have. 39 | """ 40 | def __init__(self, *args, **kwargs): 41 | self.calls = [] 42 | super(BaseTestCase, self).__init__(*args, **kwargs) 43 | 44 | def setUp(self): 45 | """ 46 | Exists only to prove that adding a setUp method to a test case does not 47 | break Exam. 48 | """ 49 | pass 50 | 51 | def tearDown(self): 52 | """ 53 | Exists only to prove that adding a tearDown method to a test case does 54 | not break Exam. 55 | """ 56 | pass 57 | 58 | 59 | class CaseWithBeforeHook(BaseTestCase): 60 | 61 | @before 62 | def run_before(self): 63 | self.calls.append('run before') 64 | 65 | 66 | class CaseWithDecoratedBeforeHook(BaseTestCase): 67 | 68 | def setup_some_state(self): 69 | self.state = True 70 | 71 | def setup_some_other_state(self): 72 | self.other_state = True 73 | 74 | @before(setup_some_state) 75 | def should_have_run_before(self): 76 | pass 77 | 78 | @before(setup_some_state, setup_some_other_state) 79 | def should_have_run_both_states(self): 80 | pass 81 | 82 | 83 | class SubclassWithBeforeHook(CaseWithBeforeHook): 84 | 85 | @before 86 | def subclass_run_before(self): 87 | self.calls.append('subclass run before') 88 | 89 | 90 | class CaseWithAfterHook(CaseWithBeforeHook): 91 | 92 | @after 93 | def run_after(self): 94 | self.calls.append('run after') 95 | 96 | 97 | class SubclassCaseWithAfterHook(CaseWithAfterHook): 98 | 99 | @after 100 | def subclass_run_after(self): 101 | self.calls.append('subclass run after') 102 | 103 | 104 | class CaseWithAroundHook(BaseTestCase): 105 | 106 | @around 107 | def run_around(self): 108 | self.calls.append('run around before') 109 | yield 110 | self.calls.append('run around after') 111 | 112 | 113 | class SubclassCaseWithAroundHook(BaseTestCase): 114 | 115 | @around 116 | def subclass_run_around(self): 117 | self.calls.append('subclass run around before') 118 | yield 119 | self.calls.append('subclass run around after') 120 | 121 | 122 | class CaseWithPatcher(BaseTestCase): 123 | 124 | @patcher('tests.dummy.thing') 125 | def dummy_thing(self): 126 | return sentinel.mock 127 | 128 | dummy_it = patcher('tests.dummy.it', return_value=12) 129 | 130 | 131 | class SubclassedCaseWithPatcher(CaseWithPatcher): 132 | pass 133 | 134 | 135 | class CaseWithPatcherObject(BaseTestCase): 136 | 137 | @patcher.object(ThingClass, 'prop') 138 | def dummy_thing(self): 139 | return 15 140 | 141 | 142 | class SubclassedCaseWithPatcherObject(CaseWithPatcherObject): 143 | pass 144 | 145 | 146 | # TODO: Make the subclass checking just be a subclass of the test case 147 | class TestExam(Exam, TestCase): 148 | 149 | def test_assert_changes_is_asserts_mixin_assert_changes(self): 150 | from exam.asserts import AssertsMixin 151 | self.assertEqual(AssertsMixin.assertChanges, Exam.assertChanges) 152 | 153 | def test_before_runs_method_before_test_case(self): 154 | case = CaseWithBeforeHook() 155 | self.assertEqual(case.calls, []) 156 | case.run() 157 | self.assertEqual(case.calls_before_run, ['run before']) 158 | 159 | def test_before_decorator_runs_func_before_function(self): 160 | case = CaseWithDecoratedBeforeHook() 161 | self.assertFalse(hasattr(case, 'state')) 162 | case.should_have_run_before() 163 | self.assertTrue(case.state) 164 | 165 | def test_before_decorator_runs_multiple_funcs(self): 166 | case = CaseWithDecoratedBeforeHook() 167 | self.assertFalse(hasattr(case, 'state')) 168 | self.assertFalse(hasattr(case, 'other_state')) 169 | case.should_have_run_both_states() 170 | self.assertTrue(case.state) 171 | self.assertTrue(case.other_state) 172 | 173 | def test_before_decorator_does_not_squash_func_name(self): 174 | self.assertEqual( 175 | CaseWithDecoratedBeforeHook.should_have_run_before.__name__, 176 | 'should_have_run_before' 177 | ) 178 | 179 | def test_after_adds_each_method_after_test_case(self): 180 | case = CaseWithAfterHook() 181 | self.assertEqual(case.calls, []) 182 | case.run() 183 | self.assertEqual(case.calls, ['run before', 'run after']) 184 | 185 | def test_around_calls_methods_before_and_after_run(self): 186 | case = CaseWithAroundHook() 187 | self.assertEqual(case.calls, []) 188 | case.run() 189 | self.assertEqual(case.calls_before_run, ['run around before']) 190 | self.assertEqual(case.calls, ['run around before', 'run around after']) 191 | 192 | def test_before_works_on_subclasses(self): 193 | case = SubclassWithBeforeHook() 194 | self.assertEqual(case.calls, []) 195 | 196 | case.run() 197 | 198 | # The only concern with ordering here is that the parent class's 199 | # @before hook fired before it's parents. The actual order of the 200 | # @before hooks within a level of class is irrelevant. 201 | self.assertEqual(case.calls, ['run before', 'subclass run before']) 202 | 203 | def test_after_works_on_subclasses(self): 204 | case = SubclassCaseWithAfterHook() 205 | self.assertEqual(case.calls, []) 206 | 207 | case.run() 208 | 209 | self.assertEqual(case.calls_before_run, ['run before']) 210 | self.assertEqual(case.calls, 211 | ['run before', 'run after', 'subclass run after']) 212 | 213 | def test_around_works_with_subclasses(self): 214 | case = SubclassCaseWithAroundHook() 215 | self.assertEqual(case.calls, []) 216 | 217 | case.run() 218 | 219 | self.assertEqual(case.calls_before_run, ['subclass run around before']) 220 | self.assertEqual(case.calls, 221 | ['subclass run around before', 222 | 'subclass run around after']) 223 | 224 | def test_patcher_start_value_is_added_to_case_dict_on_run(self): 225 | case = CaseWithPatcher() 226 | case.run() 227 | self.assertEqual(case.vars_when_run['dummy_thing'], sentinel.mock) 228 | 229 | def test_patcher_patches_object_on_setup_and_adds_patcher_to_cleanup(self): 230 | case = CaseWithPatcher() 231 | 232 | self.assertNotEqual(get_thing(), sentinel.mock) 233 | 234 | case.run() 235 | 236 | self.assertEqual(get_thing(), sentinel.mock) 237 | [cleanup() for cleanup in case.cleanups] 238 | self.assertNotEqual(get_thing(), sentinel.mock) 239 | 240 | def test_patcher_lifecycle_works_on_subclasses(self): 241 | case = SubclassedCaseWithPatcher() 242 | 243 | self.assertNotEqual(get_thing(), sentinel.mock) 244 | 245 | case.run() 246 | 247 | self.assertEqual(get_thing(), sentinel.mock) 248 | [cleanup() for cleanup in case.cleanups] 249 | self.assertNotEqual(get_thing(), sentinel.mock) 250 | 251 | def test_patcher_patches_with_a_magic_mock_if_no_function_decorated(self): 252 | case = CaseWithPatcher() 253 | 254 | self.assertNotEqual(get_it()(), 12) 255 | case.run() 256 | self.assertEqual(get_it()(), 12) 257 | 258 | case.cleanups[0]() 259 | self.assertNotEqual(get_thing(), 12) 260 | 261 | def test_patcher_object_patches_object(self): 262 | case = CaseWithPatcherObject() 263 | self.assertNotEqual(get_prop(), 15) 264 | 265 | case.run() 266 | self.assertEqual(get_prop(), 15) 267 | 268 | [cleanup() for cleanup in case.cleanups] 269 | self.assertNotEqual(get_prop(), 15) 270 | 271 | def test_patcher_object_works_with_subclasses(self): 272 | case = SubclassedCaseWithPatcherObject() 273 | 274 | self.assertNotEqual(get_prop(), 15) 275 | case.run() 276 | self.assertEqual(get_prop(), 15) 277 | 278 | [cleanup() for cleanup in case.cleanups] 279 | self.assertNotEqual(get_prop(), 15) 280 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | from tests import TestCase 2 | 3 | from exam.decorators import fixture 4 | 5 | 6 | class Outer(object): 7 | 8 | @classmethod 9 | def meth(cls): 10 | return cls, 'from method' 11 | 12 | @classmethod 13 | def reflective_meth(cls, arg): 14 | return cls, arg 15 | 16 | 17 | class Dummy(object): 18 | 19 | outside = 'value from outside' 20 | 21 | @fixture 22 | def number(self): 23 | return 42 24 | 25 | @fixture 26 | def obj(self): 27 | return object() 28 | 29 | inline = fixture(int, 5) 30 | inline_func = fixture(lambda self: self.outside) 31 | inline_func_with_args = fixture(lambda *a, **k: (a, k), 1, 2, a=3) 32 | inline_from_method = fixture(Outer.meth) 33 | 34 | inline_from_method_with_arg_1 = fixture(Outer.reflective_meth, 1) 35 | inline_from_method_with_arg_2 = fixture(Outer.reflective_meth, 2) 36 | 37 | 38 | class ExtendedDummy(Dummy): 39 | 40 | @fixture 41 | def number(self): 42 | return 42 + 42 43 | 44 | 45 | class TestFixture(TestCase): 46 | 47 | def test_converts_method_to_property(self): 48 | self.assertEqual(Dummy().number, 42) 49 | 50 | def test_caches_property_on_same_instance(self): 51 | instance = Dummy() 52 | self.assertEqual(instance.obj, instance.obj) 53 | 54 | def test_gives_different_object_on_separate_instances(self): 55 | self.assertNotEqual(Dummy().obj, Dummy().obj) 56 | 57 | def test_can_be_used_inline_with_a_function(self): 58 | self.assertEqual(Dummy().inline_func, 'value from outside') 59 | 60 | def test_can_be_used_with_a_callable_that_takes_args(self): 61 | inst = Dummy() 62 | self.assertEqual(inst.inline_func_with_args, ((inst, 1, 2), dict(a=3))) 63 | 64 | def test_can_be_used_with_class_method(self): 65 | self.assertEqual(Dummy().inline_from_method, (Outer, 'from method')) 66 | 67 | def test_if_passed_type_builds_new_object(self): 68 | self.assertEqual(Dummy().inline, 5) 69 | 70 | def test_override_in_subclass_overrides_value(self): 71 | self.assertEqual(ExtendedDummy().number, 42 + 42) 72 | 73 | def test_captures_identical_funcs_with_args_separatly(self): 74 | instance = Dummy() 75 | 76 | first = instance.inline_from_method_with_arg_1 77 | second = instance.inline_from_method_with_arg_2 78 | 79 | self.assertNotEqual(first, second) 80 | 81 | def test_clas_access_returns_fixture_itself(self): 82 | self.assertEqual(getattr(Dummy, 'number'), Dummy.number) 83 | -------------------------------------------------------------------------------- /tests/test_exam.py: -------------------------------------------------------------------------------- 1 | from tests import TestCase 2 | 3 | 4 | import exam 5 | 6 | 7 | class TestExam(TestCase): 8 | 9 | DECORATORS = ('fixture', 'before', 'after', 'around', 'patcher') 10 | 11 | def test_exam_is_cases_exam(self): 12 | from exam.cases import Exam 13 | self.assertEqual(exam.Exam, Exam) 14 | 15 | def test_imports_all_the_decorators(self): 16 | import exam.decorators 17 | 18 | for decorator in self.DECORATORS: 19 | from_decorators = getattr(exam.decorators, decorator) 20 | from_root = getattr(exam, decorator) 21 | 22 | self.assertEqual(from_root, from_decorators) 23 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | from tests import TestCase 2 | from mock import patch, Mock, sentinel 3 | 4 | from exam.helpers import intercept, rm_f, track, mock_import, call, effect 5 | from exam.decorators import fixture 6 | 7 | 8 | @patch('exam.helpers.shutil') 9 | class TestRmrf(TestCase): 10 | 11 | path = '/path/to/folder' 12 | 13 | def test_calls_shutil_rmtreee(self, shutil): 14 | rm_f(self.path) 15 | shutil.rmtree.assert_called_once_with(self.path, ignore_errors=True) 16 | 17 | @patch('exam.helpers.os') 18 | def test_on_os_errors_calls_os_remove(self, os, shutil): 19 | shutil.rmtree.side_effect = OSError 20 | rm_f(self.path) 21 | os.remove.assert_called_once_with(self.path) 22 | 23 | 24 | class TestTrack(TestCase): 25 | 26 | @fixture 27 | def foo_mock(self): 28 | return Mock() 29 | 30 | @fixture 31 | def bar_mock(self): 32 | return Mock() 33 | 34 | def test_makes_new_mock_and_attaches_each_kwarg_to_it(self): 35 | tracker = track(foo=self.foo_mock, bar=self.bar_mock) 36 | self.assertEqual(tracker.foo, self.foo_mock) 37 | self.assertEqual(tracker.bar, self.bar_mock) 38 | 39 | 40 | class TestMockImport(TestCase): 41 | 42 | def test_is_a_context_manager_that_yields_patched_import(self): 43 | with mock_import('foo') as mock_foo: 44 | import foo 45 | self.assertEqual(foo, mock_foo) 46 | 47 | def test_mocks_import_for_packages(self): 48 | with mock_import('foo.bar.baz') as mock_baz: 49 | import foo.bar.baz 50 | self.assertEqual(foo.bar.baz, mock_baz) 51 | 52 | @mock_import('foo') 53 | def test_can_be_used_as_a_decorator_too(self, mock_foo): 54 | import foo 55 | self.assertEqual(foo, mock_foo) 56 | 57 | @mock_import('foo') 58 | @mock_import('bar') 59 | def test_stacked_adds_args_bottom_up(self, mock_bar, mock_foo): 60 | import foo 61 | import bar 62 | self.assertEqual(mock_bar, bar) 63 | self.assertEqual(mock_foo, foo) 64 | 65 | 66 | class TestIntercept(TestCase): 67 | 68 | class Example(object): 69 | def method(self, positional, keyword): 70 | return sentinel.METHOD_RESULT 71 | 72 | def test_intercept(self): 73 | ex = self.Example() 74 | 75 | def counter(positional, keyword): 76 | assert positional is sentinel.POSITIONAL_ARGUMENT 77 | assert keyword is sentinel.KEYWORD_ARGUMENT 78 | result = yield 79 | assert result is sentinel.METHOD_RESULT 80 | counter.calls += 1 81 | 82 | counter.calls = 0 83 | 84 | intercept(ex, 'method', counter) 85 | self.assertEqual(counter.calls, 0) 86 | assert ex.method( 87 | sentinel.POSITIONAL_ARGUMENT, 88 | keyword=sentinel.KEYWORD_ARGUMENT) is sentinel.METHOD_RESULT 89 | self.assertEqual(counter.calls, 1) 90 | 91 | ex.method.unwrap() 92 | assert ex.method( 93 | sentinel.POSITIONAL_ARGUMENT, 94 | keyword=sentinel.KEYWORD_ARGUMENT) is sentinel.METHOD_RESULT 95 | self.assertEqual(counter.calls, 1) 96 | 97 | 98 | class TestEffect(TestCase): 99 | 100 | def test_creates_callable_mapped_to_config_dict(self): 101 | config = [ 102 | (call(1), 2), 103 | (call('a'), 3), 104 | (call(1, b=2), 4), 105 | (call(c=3), 5) 106 | ] 107 | side_effecet = effect(*config) 108 | 109 | self.assertEqual(side_effecet(1), 2) 110 | self.assertEqual(side_effecet('a'), 3) 111 | self.assertEqual(side_effecet(1, b=2), 4) 112 | self.assertEqual(side_effecet(c=3), 5) 113 | 114 | def test_raises_type_error_when_called_with_unknown_args(self): 115 | side_effect = effect((call(1), 5)) 116 | self.assertRaises(TypeError, side_effect, 'junk') 117 | 118 | def test_can_be_used_with_mutable_data_structs(self): 119 | side_effect = effect((call([1, 2, 3]), 'list')) 120 | self.assertEqual(side_effect([1, 2, 3]), 'list') 121 | -------------------------------------------------------------------------------- /tests/test_mock.py: -------------------------------------------------------------------------------- 1 | from tests import TestCase 2 | 3 | from exam.mock import Mock 4 | from exam.decorators import fixture, before 5 | 6 | 7 | class MockTest(TestCase): 8 | 9 | mock = fixture(Mock) 10 | 11 | @before 12 | def assert_mock_clean(self): 13 | self.mock.assert_not_called() 14 | 15 | def test_assert_called_asserts_mock_was_called(self): 16 | self.assertRaises(AssertionError, self.mock.assert_called) 17 | 18 | self.mock() 19 | self.mock.assert_called() 20 | 21 | self.mock.reset_mock() 22 | self.assertRaises(AssertionError, self.mock.assert_called) 23 | 24 | def test_assert_not_called_asserts_mock_was_not_called(self): 25 | self.mock() 26 | self.assertRaises(AssertionError, self.mock.assert_not_called) 27 | 28 | self.mock.reset_mock() 29 | self.mock.assert_not_called() 30 | 31 | def test_assert_not_called_with_asserts_not_called_with_args(self): 32 | self.mock(1, 2, three=4) 33 | self.mock.assert_called_with(1, 2, three=4) 34 | 35 | self.mock.assert_not_called_with(1, 2, four=5) 36 | self.mock.assert_not_called_with(1, three=5) 37 | self.mock.assert_not_called_with() 38 | 39 | with self.assertRaises(AssertionError): 40 | self.mock.assert_not_called_with(1, 2, three=4) 41 | 42 | self.mock('foo') 43 | self.mock.assert_not_called_with(1, 2, three=4) # not the latest call 44 | 45 | def test_assert_not_called_once_with_asserts_one_call_with_args(self): 46 | self.mock.assert_not_called_once_with(1, 2, three=4) # 0 times 47 | 48 | self.mock(1, 2, three=4) 49 | 50 | with self.assertRaises(AssertionError): 51 | self.mock.assert_not_called_once_with(1, 2, three=4) # 1 time 52 | 53 | self.mock(1, 2, three=4) 54 | self.mock.assert_not_called_once_with(1, 2, three=4) # 2 times 55 | 56 | def test_assert_not_any_call_asserts_never_called_with_args(self): 57 | self.mock.assert_not_any_call(1, 2, three=4) 58 | 59 | self.mock(1, 2, three=4) 60 | 61 | with self.assertRaises(AssertionError): 62 | self.mock.assert_not_any_call(1, 2, three=4) 63 | 64 | self.mock('foo') 65 | 66 | with self.assertRaises(AssertionError): 67 | # Even though it's not the latest, it was previously called with 68 | # these args 69 | self.mock.assert_not_any_call(1, 2, three=4) 70 | -------------------------------------------------------------------------------- /tests/test_objects.py: -------------------------------------------------------------------------------- 1 | from mock import sentinel 2 | from tests import TestCase 3 | 4 | from exam.objects import always, noop 5 | 6 | 7 | class TestAlways(TestCase): 8 | 9 | def test_always_returns_identity(self): 10 | fn = always(sentinel.RESULT_VALUE) 11 | assert fn() is sentinel.RESULT_VALUE 12 | assert fn(1, key='value') is sentinel.RESULT_VALUE 13 | 14 | def test_can_be_called_with_anything(self): 15 | noop() 16 | noop(1) 17 | noop(key='val') 18 | noop(1, key='val') 19 | noop(1, 2, 3, key='val') 20 | noop(1, 2, 3, key='val', another='thing') 21 | 22 | def test_returns_none(self): 23 | self.assertIsNone(noop()) 24 | --------------------------------------------------------------------------------