├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── appendices ├── outcomes │ └── test_outcomes.py ├── packaging │ ├── some_module_proj │ │ ├── setup.py │ │ └── some_module.py │ ├── some_package_proj │ │ ├── setup.py │ │ └── src │ │ │ └── some_package │ │ │ ├── __init__.py │ │ │ └── some_module.py │ └── some_package_proj_v2 │ │ ├── CHANGELOG.rst │ │ ├── LICENSE │ │ ├── README.rst │ │ ├── setup.py │ │ └── src │ │ └── some_package │ │ ├── __init__.py │ │ └── some_module.py ├── xdist │ ├── test_not_parallel.py │ └── test_parallel.py └── xunit │ ├── test_mixed_fixtures.py │ └── test_xUnit_fixtures.py ├── ch2 └── tasks_proj │ ├── CHANGELOG.rst │ └── tests │ ├── func │ ├── __init__.py │ ├── test_add.py │ ├── test_add_variety.py │ ├── test_api_exceptions.py │ ├── test_unique_id_1.py │ ├── test_unique_id_2.py │ ├── test_unique_id_3.py │ └── test_unique_id_4.py │ ├── pytest.ini │ └── unit │ ├── __init__.py │ ├── test_task.py │ └── test_task_fail.py ├── ch3 ├── a │ └── tasks_proj │ │ ├── CHANGELOG.rst │ │ └── tests │ │ ├── conftest.py │ │ ├── func │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_add_variety.py │ │ ├── test_api_exceptions.py │ │ └── test_unique_id.py │ │ ├── pytest.ini │ │ └── unit │ │ ├── __init__.py │ │ └── test_task.py ├── b │ └── tasks_proj │ │ ├── CHANGELOG.rst │ │ └── tests │ │ ├── conftest.py │ │ ├── func │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_add_variety.py │ │ ├── test_add_variety2.py │ │ ├── test_api_exceptions.py │ │ └── test_unique_id.py │ │ ├── pytest.ini │ │ └── unit │ │ ├── __init__.py │ │ └── test_task.py ├── c │ └── tasks_proj │ │ ├── CHANGELOG.rst │ │ └── tests │ │ ├── conftest.py │ │ ├── func │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_add_variety.py │ │ ├── test_add_variety2.py │ │ ├── test_api_exceptions.py │ │ └── test_unique_id.py │ │ ├── pytest.ini │ │ └── unit │ │ ├── __init__.py │ │ └── test_task.py ├── test_autouse.py ├── test_fixtures.py ├── test_rename_fixture.py └── test_scope.py ├── ch4 ├── authors │ ├── conftest.py │ └── test_authors.py ├── cache │ ├── test_few_failures.py │ ├── test_pass_fail.py │ ├── test_slower.py │ └── test_slower_2.py ├── cap │ ├── test_capfd.py │ └── test_capsys.py ├── dt │ ├── 1 │ │ └── unnecessary_math.py │ ├── 2 │ │ └── unnecessary_math.py │ └── 3 │ │ ├── conftest.py │ │ └── unnecessary_math.py ├── monkey │ ├── cheese.py │ ├── test_cheese.py │ └── test_monkey.py ├── pytestconfig │ ├── conftest.py │ └── test_config.py ├── test_tmpdir.py └── test_warnings.py ├── ch5 ├── a │ └── tasks_proj │ │ ├── CHANGELOG.rst │ │ └── tests │ │ ├── conftest.py │ │ ├── func │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_add_variety.py │ │ ├── test_add_variety2.py │ │ ├── test_api_exceptions.py │ │ └── test_unique_id.py │ │ ├── pytest.ini │ │ └── unit │ │ ├── __init__.py │ │ └── test_task.py ├── b │ └── tasks_proj │ │ ├── CHANGELOG.rst │ │ └── tests │ │ ├── conftest.py │ │ ├── func │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_add_variety.py │ │ ├── test_add_variety2.py │ │ ├── test_api_exceptions.py │ │ └── test_unique_id.py │ │ ├── pytest.ini │ │ └── unit │ │ ├── __init__.py │ │ └── test_task.py ├── c │ └── tasks_proj │ │ ├── CHANGELOG.rst │ │ └── tests │ │ ├── conftest.py │ │ ├── func │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_add_variety.py │ │ ├── test_add_variety2.py │ │ ├── test_api_exceptions.py │ │ └── test_unique_id.py │ │ ├── pytest.ini │ │ └── unit │ │ ├── __init__.py │ │ └── test_task.py └── pytest-nice │ ├── LICENSE │ ├── README.rst │ ├── pytest_nice.py │ ├── setup.py │ └── tests │ ├── conftest.py │ └── test_nice.py ├── ch6 ├── a │ └── tasks_proj │ │ ├── CHANGELOG.rst │ │ └── tests │ │ ├── conftest.py │ │ ├── func │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_add_variety.py │ │ ├── test_add_variety2.py │ │ ├── test_api_exceptions.py │ │ └── test_unique_id.py │ │ └── unit │ │ ├── __init__.py │ │ └── test_task.py ├── b │ └── tasks_proj │ │ ├── CHANGELOG.rst │ │ └── tests │ │ ├── conftest.py │ │ ├── func │ │ ├── __init__.py │ │ ├── test_add.py │ │ ├── test_add_variety.py │ │ ├── test_add_variety2.py │ │ ├── test_api_exceptions.py │ │ └── test_unique_id.py │ │ ├── pytest.ini │ │ └── unit │ │ ├── __init__.py │ │ └── test_task.py ├── dups │ ├── a │ │ └── test_foo.py │ └── b │ │ └── test_foo.py ├── dups_fixed │ ├── a │ │ ├── __init__.py │ │ └── test_foo.py │ └── b │ │ ├── __init__.py │ │ └── test_foo.py └── format │ ├── pytest.ini │ ├── setup.cfg │ └── tox.ini ├── ch7 ├── jenkins │ ├── run_tests.bash │ └── run_tests_cov.bash ├── tasks_proj_v2 │ ├── CHANGELOG.rst │ ├── LICENSE │ ├── MANIFEST.in │ ├── setup.py │ ├── src │ │ └── tasks │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── cli.py │ │ │ ├── config.py │ │ │ ├── tasksdb_pymongo.py │ │ │ └── tasksdb_tinydb.py │ ├── tests │ │ ├── conftest.py │ │ ├── func │ │ │ ├── __init__.py │ │ │ ├── test_add.py │ │ │ ├── test_add_variety.py │ │ │ ├── test_add_variety2.py │ │ │ ├── test_api_exceptions.py │ │ │ └── test_unique_id.py │ │ └── unit │ │ │ ├── __init__.py │ │ │ ├── test_cli.py │ │ │ └── test_task.py │ └── tox.ini └── unittest │ ├── conftest.py │ ├── test_delete_pytest.py │ ├── test_delete_unittest.py │ ├── test_delete_unittest_fix.py │ └── test_delete_unittest_fix2.py └── tasks_proj ├── LICENSE ├── MANIFEST.in ├── setup.py ├── src └── tasks │ ├── __init__.py │ ├── api.py │ ├── cli.py │ ├── config.py │ ├── tasksdb_pymongo.py │ └── tasksdb_tinydb.py └── tests ├── conftest.py ├── func ├── __init__.py └── test_add.py ├── pytest.ini └── unit ├── __init__.py └── test_task.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Created by https://www.gitignore.io/api/visualstudiocode 132 | # Edit at https://www.gitignore.io/?templates=visualstudiocode 133 | 134 | ### VisualStudioCode ### 135 | .vscode/* 136 | !.vscode/settings.json 137 | !.vscode/tasks.json 138 | !.vscode/launch.json 139 | !.vscode/extensions.json 140 | 141 | ### VisualStudioCode Patch ### 142 | # Ignore all local history of files 143 | .history 144 | 145 | # End of https://www.gitignore.io/api/visualstudiocode 146 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "venv/bin/python", 3 | "python.linting.pylintEnabled": true, 4 | "python.linting.enabled": true, 5 | "cSpell.ignoreWords": [ 6 | "asdict", 7 | "builtin" 8 | ], 9 | "python.testing.unittestEnabled": false, 10 | "python.testing.pytestEnabled": true, 11 | "python.testing.cwd": "", 12 | "python.testing.pytestArgs": [ 13 | "ch2" 14 | ], 15 | "python.testing.nosetestsEnabled": false 16 | } -------------------------------------------------------------------------------- /appendices/outcomes/test_outcomes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_pass(): 5 | assert 1 == 1 6 | 7 | 8 | def test_fail(): 9 | assert 1 == 2 10 | 11 | 12 | @pytest.mark.xfail() 13 | def test_xfail(): 14 | assert 1 == 2 15 | 16 | 17 | @pytest.mark.xfail() 18 | def test_xpass(): 19 | assert 1 == 1 20 | 21 | 22 | @pytest.mark.skip() 23 | def test_skip(): 24 | pass 25 | 26 | 27 | @pytest.fixture() 28 | def flaky_fixture(): 29 | assert 1 == 2 30 | 31 | 32 | def test_error(flaky_fixture): 33 | pass 34 | -------------------------------------------------------------------------------- /appendices/packaging/some_module_proj/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name="some_module", py_modules=["some_module"]) 4 | -------------------------------------------------------------------------------- /appendices/packaging/some_module_proj/some_module.py: -------------------------------------------------------------------------------- 1 | def some_func(): 2 | return 42 3 | -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="some_package", packages=find_packages(where="src"), package_dir={"": "src"}, 5 | ) 6 | -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj/src/some_package/__init__.py: -------------------------------------------------------------------------------- 1 | from some_package.some_module import * 2 | -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj/src/some_package/some_module.py: -------------------------------------------------------------------------------- 1 | def some_func(): 2 | return 42 3 | -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj_v2/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ------------------------------------------------------ 5 | 6 | 1.0 7 | --- 8 | 9 | Changes: 10 | ~~~~~~~~ 11 | 12 | - Initial version. 13 | -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj_v2/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The Pragmatic Programmers, LLC 2 | 3 | All rights reserved. 4 | 5 | Copyrights apply to this source code. 6 | 7 | You may use the source code in your own projects, however the source code 8 | may not be used to create commercial training material, courses, books, 9 | articles, and the like. We make no guarantees that this source code is fit 10 | for any purpose. 11 | -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj_v2/README.rst: -------------------------------------------------------------------------------- 1 | ==================================================== 2 | some_package: Demonstrate packaging and distribution 3 | ==================================================== 4 | 5 | ``some_package`` is the Python package to demonstrate how easy it is to create installable, maintainable, shareable packages and distributions. 6 | 7 | It contains one function, ``some_func()``. 8 | 9 | >>> import some_package 10 | >>> some_package.some_func() 11 | 42 12 | 13 | That's it, really -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj_v2/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="some_package", 5 | description="Demonstrate packaging and distribution", 6 | version="1.0", 7 | author="Brian Okken", 8 | author_email="brian@pythontesting.net", 9 | url="https://pragprog.com/book/bopytest/python-testing-with-pytest", 10 | packages=find_packages(where="src"), 11 | package_dir={"": "src"}, 12 | ) 13 | -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj_v2/src/some_package/__init__.py: -------------------------------------------------------------------------------- 1 | from some_package.some_module import * 2 | -------------------------------------------------------------------------------- /appendices/packaging/some_package_proj_v2/src/some_package/some_module.py: -------------------------------------------------------------------------------- 1 | def some_func(): 2 | return 42 3 | -------------------------------------------------------------------------------- /appendices/xdist/test_not_parallel.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | 4 | 5 | @pytest.fixture(scope='function') 6 | def some_resource(): 7 | x = [] 8 | return x 9 | 10 | 11 | def test_1(some_resource): 12 | time.sleep(1) 13 | 14 | 15 | def test_2(some_resource): 16 | time.sleep(1) 17 | 18 | 19 | def test_3(some_resource): 20 | time.sleep(1) 21 | 22 | 23 | def test_4(some_resource): 24 | time.sleep(1) 25 | -------------------------------------------------------------------------------- /appendices/xdist/test_parallel.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | 4 | 5 | @pytest.mark.parametrize("x", list(range(10))) 6 | def test_something(x): 7 | time.sleep(1) 8 | -------------------------------------------------------------------------------- /appendices/xunit/test_mixed_fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def setup_module(): 5 | print('\nsetup_module() - xUnit') 6 | 7 | 8 | def teardown_module(): 9 | print('teardown_module() - xUnit') 10 | 11 | 12 | def setup_function(): 13 | print('setup_function() - xUnit') 14 | 15 | 16 | def teardown_function(): 17 | print('teardown_function() - xUnit\n') 18 | 19 | 20 | @pytest.fixture(scope='module') 21 | def module_fixture(): 22 | print('module_fixture() setup - pytest') 23 | yield 24 | print('module_fixture() teardown - pytest') 25 | 26 | 27 | @pytest.fixture(scope='function') 28 | def function_fixture(): 29 | print('function_fixture() setup - pytest') 30 | yield 31 | print('function_fixture() teardown - pytest') 32 | 33 | 34 | def test_1(module_fixture, function_fixture): 35 | print('test_1()') 36 | 37 | 38 | def test_2(module_fixture, function_fixture): 39 | print('test_2()') 40 | -------------------------------------------------------------------------------- /appendices/xunit/test_xUnit_fixtures.py: -------------------------------------------------------------------------------- 1 | def setup_module(module): 2 | print(f'\nsetup_module() for {module.__name__}') 3 | 4 | 5 | def teardown_module(module): 6 | print(f'teardown_module() for {module.__name__}') 7 | 8 | 9 | def setup_function(function): 10 | print(f'setup_function() for {function.__name__}') 11 | 12 | 13 | def teardown_function(function): 14 | print(f'teardown_function() for {function.__name__}') 15 | 16 | 17 | def test_1(): 18 | print('test_1()') 19 | 20 | 21 | def test_2(): 22 | print('test_2()') 23 | 24 | 25 | class TestClass: 26 | @classmethod 27 | def setup_class(cls): 28 | print(f'setup_class() for class {cls.__name__}') 29 | 30 | @classmethod 31 | def teardown_class(cls): 32 | print(f'teardown_class() for {cls.__name__}') 33 | 34 | def setup_method(self, method): 35 | print(f'setup_method() for {method.__name__}') 36 | 37 | def teardown_method(self, method): 38 | print(f'teardown_method() for {method.__name__}') 39 | 40 | def test_3(self): 41 | print('test_3()') 42 | 43 | def test_4(self): 44 | print('test_4()') 45 | -------------------------------------------------------------------------------- /ch2/tasks_proj/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ----------------------------------------------------------------- 5 | 6 | 0.1.0 (ch2/tasks_proj/tests) 7 | ---------------------------- 8 | 9 | Changes to tests: 10 | ~~~~~~~~~~~~~~~~~ 11 | 12 | - added tests/unit/test_Task.py 13 | - a few tests to demonstrate running tests 14 | 15 | - added tests/unit/test_Task_fail.py 16 | - demonstrate test failure 17 | 18 | - added tests/func/test_api_exceptions.py 19 | - testing for expected exceptions 20 | 21 | - added tests/func/test_add.py 22 | - testing ``tasks.add()`` 23 | - demonstrate user defined markers 24 | 25 | - added tests/func/test_unique_id_1.py 26 | - initial tests for ``tasks.unique_id()``. 27 | 28 | - added tests/func/test_unique_id_2.py 29 | - demonstrate ``@pytest.mark.skip()``. 30 | 31 | - added tests/func/test_unique_id_3.py : 32 | - demonstrate ``@pytest.mark.skipif()``. 33 | 34 | - added tests/func/test_unique_id_4.py 35 | - demonstrate ``@pytest.mark.xfail()``. 36 | 37 | - added tests/func/test_add_variety.py 38 | - demonstrate ``@pytest.mark.parametrize`` on functions and classes. 39 | 40 | 41 | --------------------------------------------------- 42 | 43 | 0.1.0 44 | ----- 45 | 46 | Changes: 47 | ~~~~~~~~ 48 | 49 | - Initial version. 50 | 51 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task('do something') 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task('sit in chair', owner='me', done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | 33 | @pytest.fixture(autouse=True) 34 | def initialized_tasks_db(tmpdir): 35 | """Connect to db before testing, disconnect after.""" 36 | # Setup : start db 37 | tasks.start_tasks_db(str(tmpdir), 'tiny') 38 | 39 | yield # this is where the testing happens 40 | 41 | # Teardown : stop db 42 | tasks.stop_tasks_db() 43 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | 3 | import pytest 4 | import tasks 5 | 6 | 7 | def test_add_raises(): 8 | """add() should raise an exception with wrong type param.""" 9 | with pytest.raises(TypeError): 10 | tasks.add(task="not a Task object") 11 | 12 | 13 | @pytest.mark.smoke 14 | def test_list_raises(): 15 | """list() should raise an exception with wrong type param.""" 16 | with pytest.raises(TypeError): 17 | tasks.list_tasks(owner=123) 18 | 19 | 20 | @pytest.mark.get 21 | @pytest.mark.smoke 22 | def test_get_raises(): 23 | """get() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.get(task_id="123") 26 | 27 | 28 | class TestUpdate: 29 | """Test expected exceptions with tasks.update().""" 30 | 31 | def test_bad_id(self): 32 | """A non-int id should raise an exception.""" 33 | with pytest.raises(TypeError): 34 | tasks.update(task_id={"dict instead": 1}, task=tasks.Task()) 35 | 36 | def test_bad_task(self): 37 | """A non-Task task should raise an exception.""" 38 | with pytest.raises(TypeError): 39 | tasks.update(task_id=1, task="not a task") 40 | 41 | 42 | def test_delete_raises(): 43 | """delete() should raise an exception with wrong type param.""" 44 | with pytest.raises(TypeError): 45 | tasks.delete(task_id=(1, 2, 3)) 46 | 47 | 48 | def test_start_tasks_db_raises(): 49 | """Make sure unsupported db raises an exception.""" 50 | with pytest.raises(ValueError) as excinfo: 51 | tasks.start_tasks_db("some/great/path", "mysql") 52 | exception_msg = excinfo.value.args[0] 53 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 54 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/func/test_unique_id_1.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import pytest 4 | import tasks 5 | 6 | 7 | def test_unique_id(): 8 | """Calling unique_id() twice should return different numbers.""" 9 | id_1 = tasks.unique_id() 10 | id_2 = tasks.unique_id() 11 | assert id_1 != id_2 12 | 13 | 14 | @pytest.fixture(autouse=True) 15 | def initialized_tasks_db(tmpdir): 16 | """Connect to db before testing, disconnect after.""" 17 | tasks.start_tasks_db(str(tmpdir), "tiny") 18 | yield 19 | tasks.stop_tasks_db() 20 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/func/test_unique_id_2.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.mark.skip(reason="misunderstood the API") 9 | def test_unique_id_1(): 10 | """Calling unique_id() twice should return different numbers.""" 11 | id_1 = tasks.unique_id() 12 | id_2 = tasks.unique_id() 13 | assert id_1 != id_2 14 | 15 | 16 | def test_unique_id_2(): 17 | """unique_id() should return an unused id.""" 18 | ids = [] 19 | ids.append(tasks.add(Task("one"))) 20 | ids.append(tasks.add(Task("two"))) 21 | ids.append(tasks.add(Task("three"))) 22 | # grab a unique id 23 | uid = tasks.unique_id() 24 | # make sure it isn't in the list of existing ids 25 | assert uid not in ids 26 | 27 | 28 | @pytest.fixture(autouse=True) 29 | def initialized_tasks_db(tmpdir): 30 | """Connect to db before testing, disconnect after.""" 31 | tasks.start_tasks_db(str(tmpdir), "tiny") 32 | yield 33 | tasks.stop_tasks_db() 34 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/func/test_unique_id_3.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.mark.skipif( 9 | tasks.__version__ < "0.2.0", reason="not supported until version 0.2.0" 10 | ) 11 | def test_unique_id_1(): 12 | """Calling unique_id() twice should return different numbers.""" 13 | id_1 = tasks.unique_id() 14 | id_2 = tasks.unique_id() 15 | assert id_1 != id_2 16 | 17 | 18 | def test_unique_id_2(): 19 | """unique_id() should return an unused id.""" 20 | ids = [] 21 | ids.append(tasks.add(Task("one"))) 22 | ids.append(tasks.add(Task("two"))) 23 | ids.append(tasks.add(Task("three"))) 24 | # grab a unique id 25 | uid = tasks.unique_id() 26 | # make sure it isn't in the list of existing ids 27 | assert uid not in ids 28 | 29 | 30 | @pytest.fixture(autouse=True) 31 | def initialized_tasks_db(tmpdir): 32 | """Connect to db before testing, disconnect after.""" 33 | tasks.start_tasks_db(str(tmpdir), "tiny") 34 | yield 35 | tasks.stop_tasks_db() 36 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/func/test_unique_id_4.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.mark.xfail( 9 | tasks.__version__ < "0.2.0", reason="not supported until version 0.2.0" 10 | ) 11 | def test_unique_id_1(): 12 | """Calling unique_id() twice should return different numbers.""" 13 | id_1 = tasks.unique_id() 14 | id_2 = tasks.unique_id() 15 | assert id_1 != id_2 16 | 17 | 18 | @pytest.mark.xfail() 19 | def test_unique_id_is_a_duck(): 20 | """Demonstrate xfail.""" 21 | uid = tasks.unique_id() 22 | assert uid == "a duck" 23 | 24 | 25 | @pytest.mark.xfail() 26 | def test_unique_id_not_a_duck(): 27 | """Demonstrate xpass.""" 28 | uid = tasks.unique_id() 29 | assert uid != "a duck" 30 | 31 | 32 | def test_unique_id_2(): 33 | """unique_id() should return an unused id.""" 34 | ids = [] 35 | ids.append(tasks.add(Task("one"))) 36 | ids.append(tasks.add(Task("two"))) 37 | ids.append(tasks.add(Task("three"))) 38 | # grab a unique id 39 | uid = tasks.unique_id() 40 | # make sure it isn't in the list of existing ids 41 | assert uid not in ids 42 | 43 | 44 | @pytest.fixture(autouse=True) 45 | def initialized_tasks_db(tmpdir): 46 | """Connect to db before testing, disconnect after.""" 47 | tasks.start_tasks_db(str(tmpdir), "tiny") 48 | yield 49 | tasks.stop_tasks_db() 50 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch2/tasks_proj/tests/unit/test_task_fail.py: -------------------------------------------------------------------------------- 1 | """Use the Task type to show test failures.""" 2 | from tasks import Task 3 | 4 | 5 | def test_task_equality(): 6 | """Different tasks should not be equal.""" 7 | t1 = Task('sit there', 'brian') 8 | t2 = Task('do something', 'okken') 9 | assert t1 == t2 10 | 11 | 12 | def test_dict_equality(): 13 | """Different tasks compared as dicts should not be equal.""" 14 | t1_dict = Task('make sandwich', 'okken')._asdict() 15 | t2_dict = Task('make sandwich', 'okkem')._asdict() 16 | assert t1_dict == t2_dict 17 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ---------------------------------------------------- 5 | 6 | 0.1.0 (ch3/a/tasks_proj/tests) 7 | ---------------------------- 8 | 9 | Changes to tests: 10 | ~~~~~~~~~~~~~~~~~ 11 | 12 | - add tests/conftest.py with fixtures: 13 | - tasks_just_a_few : 3 tasks in a tuple 14 | - tasks_mult_per_owner : 9 tasks with 3 owners 15 | - tasks_db : connection to db, using TinyDB 16 | - db_with_3_tasks : db prefilled with 3 tasks 17 | - db_with_multi_per_owner : db prefilled with 9 tasks 18 | 19 | - modify to use fixtures: 20 | - test_add.py 21 | - test_add_variety.py 22 | - test_api_exceptions.py 23 | - test_unique_id.py 24 | 25 | - remove tests/unit/test_task_fail.py 26 | - it was just to demo failures 27 | 28 | - remove tests/func/test_unique_id_1.py 29 | - remove tests/func/test_unique_id_2.py 30 | - remove tests/func/test_unique_id_3.py 31 | - remove tests/func/test_unique_id_4.py 32 | - add tests/func/test_unique_id.py 33 | - just need one unique_id test. 34 | 35 | 36 | ---------------------------------------------------- 37 | 38 | 0.1.0 (ch2/tasks_proj/tests) 39 | ---------------------------- 40 | 41 | Changes to tests: 42 | ~~~~~~~~~~~~~~~~~ 43 | 44 | - added tests/unit/test_Task.py 45 | - a few tests to demonstrate running tests 46 | 47 | - added tests/unit/test_Task_fail.py 48 | - demonstrate test failure 49 | 50 | - added tests/func/test_api_exceptions.py 51 | - testing for expected exceptions 52 | 53 | - added tests/func/test_add.py 54 | - testing ``tasks.add()`` 55 | - demonstrate user defined markers 56 | 57 | - added tests/func/test_unique_id_1.py 58 | - initial tests for ``tasks.unique_id()``. 59 | 60 | - added tests/func/test_unique_id_2.py 61 | - demonstrate ``@pytest.mark.skip()``. 62 | 63 | - added tests/func/test_unique_id_3.py : 64 | - demonstrate ``@pytest.mark.skipif()``. 65 | 66 | - added tests/func/test_unique_id_4.py 67 | - demonstrate ``@pytest.mark.xfail()``. 68 | 69 | - added tests/func/test_add_variety.py 70 | - demonstrate ``@pytest.mark.parametrize`` on functions and classes. 71 | 72 | 73 | ----------------------------------------------------- 74 | 75 | 0.1.0 76 | ----- 77 | 78 | Changes: 79 | ~~~~~~~~ 80 | 81 | - Initial version. 82 | 83 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.fixture() 9 | def tasks_db(tmpdir): 10 | """Connect to db before tests, disconnect after.""" 11 | # Setup : start db 12 | tasks.start_tasks_db(str(tmpdir), "tiny") 13 | 14 | yield # this is where the testing happens 15 | 16 | # Teardown : stop db 17 | tasks.stop_tasks_db() 18 | 19 | 20 | # Reminder of Task constructor interface 21 | # Task(summary=None, owner=None, done=False, id=None) 22 | # summary is required 23 | # owner and done are optional 24 | # id is set by database 25 | 26 | 27 | @pytest.fixture() 28 | def tasks_just_a_few(): 29 | """All summaries and owners are unique.""" 30 | return ( 31 | Task("Write some code", "Brian", True), 32 | Task("Code review Brian's code", "Katie", False), 33 | Task("Fix what Brian did", "Michelle", False), 34 | ) 35 | 36 | 37 | @pytest.fixture() 38 | def tasks_mult_per_owner(): 39 | """Several owners with several tasks each.""" 40 | return ( 41 | Task("Make a cookie", "Raphael"), 42 | Task("Use an emoji", "Raphael"), 43 | Task("Move to Berlin", "Raphael"), 44 | Task("Create", "Michelle"), 45 | Task("Inspire", "Michelle"), 46 | Task("Encourage", "Michelle"), 47 | Task("Do a handstand", "Daniel"), 48 | Task("Write some books", "Daniel"), 49 | Task("Eat ice cream", "Daniel"), 50 | ) 51 | 52 | 53 | @pytest.fixture() 54 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 55 | """Connected db with 3 tasks, all unique.""" 56 | for t in tasks_just_a_few: 57 | tasks.add(t) 58 | 59 | 60 | @pytest.fixture() 61 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 62 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 63 | for t in tasks_mult_per_owner: 64 | tasks.add(t) 65 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task("do something") 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task("sit in chair", owner="me", done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task("throw a party")) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | 3 | import pytest 4 | import tasks 5 | 6 | 7 | def test_add_raises(): 8 | """add() should raise an exception with wrong type param.""" 9 | with pytest.raises(TypeError): 10 | tasks.add(task='not a Task object') 11 | 12 | 13 | @pytest.mark.smoke 14 | def test_list_raises(): 15 | """list() should raise an exception with wrong type param.""" 16 | with pytest.raises(TypeError): 17 | tasks.list_tasks(owner=123) 18 | 19 | 20 | @pytest.mark.get 21 | @pytest.mark.smoke 22 | def test_get_raises(): 23 | """get() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.get(task_id='123') 26 | 27 | 28 | class TestUpdate(): 29 | """Test expected exceptions with tasks.update().""" 30 | 31 | def test_bad_id(self): 32 | """A non-int id should raise an excption.""" 33 | with pytest.raises(TypeError): 34 | tasks.update(task_id={'dict instead': 1}, 35 | task=tasks.Task()) 36 | 37 | def test_bad_task(self): 38 | """A non-Task task should raise an excption.""" 39 | with pytest.raises(TypeError): 40 | tasks.update(task_id=1, task='not a task') 41 | 42 | 43 | def test_delete_raises(): 44 | """delete() should raise an exception with wrong type param.""" 45 | with pytest.raises(TypeError): 46 | tasks.delete(task_id=(1, 2, 3)) 47 | 48 | 49 | def test_start_tasks_db_raises(): 50 | """Make sure unsupported db raises an exception.""" 51 | with pytest.raises(ValueError) as excinfo: 52 | tasks.start_tasks_db('some/great/path', 'mysql') 53 | exception_msg = excinfo.value.args[0] 54 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 55 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch3/a/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ---------------------------------------------------- 5 | 6 | 0.1.0 (ch3/b/tasks_proj/tests) 7 | ------------------------------ 8 | 9 | Changes to tests: 10 | ~~~~~~~~~~~~~~~~~ 11 | 12 | - modify tests/conftest.py: 13 | - Create a session scope fixture ``tasks_db_session`` 14 | that connects to db. 15 | - Have ``tasks_db`` fixture use ``tasks_db_session`` and 16 | just clean out db between tests. 17 | 18 | - add tests/func/test_add_variety2.py 19 | - demonstrate paramterized fixtures 20 | 21 | 22 | ---------------------------------------------------- 23 | 24 | 0.1.0 (ch3/a/tasks_proj/tests) 25 | ------------------------------ 26 | 27 | Changes to tests: 28 | ~~~~~~~~~~~~~~~~~ 29 | 30 | - add tests/conftest.py with fixtures: 31 | - tasks_just_a_few : 3 tasks in a tuple 32 | - tasks_mult_per_owner : 9 tasks with 3 owners 33 | - tasks_db : connection to db, using TinyDB 34 | - db_with_3_tasks : db prefilled with 3 tasks 35 | - db_with_multi_per_owner : db prefilled with 9 tasks 36 | 37 | - modify to use fixtures: 38 | - test_add.py 39 | - test_add_variety.py 40 | - test_api_exceptions.py 41 | - test_unique_id.py 42 | 43 | - remove tests/unit/test_task_fail.py 44 | - it was just to demo failures 45 | 46 | - remove tests/func/test_unique_id_1.py 47 | - remove tests/func/test_unique_id_2.py 48 | - remove tests/func/test_unique_id_3.py 49 | - remove tests/func/test_unique_id_4.py 50 | - add tests/func/test_unique_id.py 51 | - just need one unique_id test. 52 | 53 | 54 | ---------------------------------------------------- 55 | 56 | 0.1.0 (ch2/tasks_proj/tests) 57 | ---------------------------- 58 | 59 | Changes to tests: 60 | ~~~~~~~~~~~~~~~~~ 61 | 62 | - added tests/unit/test_Task.py 63 | - a few tests to demonstrate running tests 64 | 65 | - added tests/unit/test_Task_fail.py 66 | - demonstrate test failure 67 | 68 | - added tests/func/test_api_exceptions.py 69 | - testing for expected exceptions 70 | 71 | - added tests/func/test_add.py 72 | - testing ``tasks.add()`` 73 | - demonstrate user defined markers 74 | 75 | - added tests/func/test_unique_id_1.py 76 | - initial tests for ``tasks.unique_id()``. 77 | 78 | - added tests/func/test_unique_id_2.py 79 | - demonstrate ``@pytest.mark.skip()``. 80 | 81 | - added tests/func/test_unique_id_3.py : 82 | - demonstrate ``@pytest.mark.skipif()``. 83 | 84 | - added tests/func/test_unique_id_4.py 85 | - demonstrate ``@pytest.mark.xfail()``. 86 | 87 | - added tests/func/test_add_variety.py 88 | - demonstrate ``@pytest.mark.parametrize`` on functions and classes. 89 | 90 | 91 | ----------------------------------------------------- 92 | 93 | 0.1.0 94 | ----- 95 | 96 | Changes: 97 | ~~~~~~~~ 98 | 99 | - Initial version. 100 | 101 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def tasks_db_session(tmpdir_factory): 10 | """Connect to db before tests, disconnect after.""" 11 | temp_dir = tmpdir_factory.mktemp("temp") 12 | tasks.start_tasks_db(str(temp_dir), "tiny") 13 | yield 14 | tasks.stop_tasks_db() 15 | 16 | 17 | @pytest.fixture() 18 | def tasks_db(tasks_db_session): 19 | """An empty tasks db.""" 20 | tasks.delete_all() 21 | 22 | 23 | # Reminder of Task constructor interface 24 | # Task(summary=None, owner=None, done=False, id=None) 25 | # summary is required 26 | # owner and done are optional 27 | # id is set by database 28 | 29 | 30 | @pytest.fixture(scope="session") 31 | def tasks_just_a_few(): 32 | """All summaries and owners are unique.""" 33 | return ( 34 | Task("Write some code", "Brian", True), 35 | Task("Code review Brian's code", "Katie", False), 36 | Task("Fix what Brian did", "Michelle", False), 37 | ) 38 | 39 | 40 | @pytest.fixture(scope="session") 41 | def tasks_mult_per_owner(): 42 | """Several owners with several tasks each.""" 43 | return ( 44 | Task("Make a cookie", "Raphael"), 45 | Task("Use an emoji", "Raphael"), 46 | Task("Move to Berlin", "Raphael"), 47 | Task("Create", "Michelle"), 48 | Task("Inspire", "Michelle"), 49 | Task("Encourage", "Michelle"), 50 | Task("Do a handstand", "Daniel"), 51 | Task("Write some books", "Daniel"), 52 | Task("Eat ice cream", "Daniel"), 53 | ) 54 | 55 | 56 | @pytest.fixture() 57 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 58 | """Connected db with 3 tasks, all unique.""" 59 | for t in tasks_just_a_few: 60 | tasks.add(t) 61 | 62 | 63 | @pytest.fixture() 64 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 65 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 66 | for t in tasks_mult_per_owner: 67 | tasks.add(t) 68 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task("do something") 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task("sit in chair", owner="me", done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task("throw a party")) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/func/test_add_variety.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_1(tasks_db): 9 | """tasks.get() using id returned from add() works.""" 10 | task = Task('breathe', 'BRIAN', True) 11 | task_id = tasks.add(task) 12 | t_from_db = tasks.get(task_id) 13 | # everything but the id should be the same 14 | assert equivalent(t_from_db, task) 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | # Compare everything but the id field 20 | return ((t1.summary == t2.summary) and 21 | (t1.owner == t2.owner) and 22 | (t1.done == t2.done)) 23 | 24 | 25 | @pytest.mark.parametrize('task', 26 | [Task('sleep', done=True), 27 | Task('wake', 'brian'), 28 | Task('breathe', 'BRIAN', True), 29 | Task('exercise', 'BrIaN', False)]) 30 | def test_add_2(tasks_db, task): 31 | """Demonstrate parametrize with one parameter.""" 32 | task_id = tasks.add(task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, task) 35 | 36 | 37 | @pytest.mark.parametrize('summary, owner, done', 38 | [('sleep', None, False), 39 | ('wake', 'brian', False), 40 | ('breathe', 'BRIAN', True), 41 | ('eat eggs', 'BrIaN', False), 42 | ]) 43 | def test_add_3(tasks_db, summary, owner, done): 44 | """Demonstrate parametrize with multiple parameters.""" 45 | task = Task(summary, owner, done) 46 | task_id = tasks.add(task) 47 | t_from_db = tasks.get(task_id) 48 | assert equivalent(t_from_db, task) 49 | 50 | 51 | tasks_to_try = (Task('sleep', done=True), 52 | Task('wake', 'brian'), 53 | Task('breathe', 'BRIAN', True), 54 | Task('exercise', 'BrIaN', False)) 55 | 56 | 57 | @pytest.mark.parametrize('task', tasks_to_try) 58 | def test_add_4(tasks_db, task): 59 | """Slightly different take.""" 60 | task_id = tasks.add(task) 61 | t_from_db = tasks.get(task_id) 62 | assert equivalent(t_from_db, task) 63 | 64 | 65 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 66 | for t in tasks_to_try] 67 | 68 | 69 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 70 | def test_add_5(tasks_db, task): 71 | """Demonstrate ids.""" 72 | task_id = tasks.add(task) 73 | t_from_db = tasks.get(task_id) 74 | assert equivalent(t_from_db, task) 75 | 76 | 77 | @pytest.mark.parametrize('task', [ 78 | pytest.param(Task('create'), id='just summary'), 79 | pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 80 | pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 81 | def test_add_6(task): 82 | """Demonstrate pytest.param and id.""" 83 | task_id = tasks.add(task) 84 | t_from_db = tasks.get(task_id) 85 | assert equivalent(t_from_db, task) 86 | 87 | 88 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 89 | class TestAdd(): 90 | """Demonstrate paramterize and test classes.""" 91 | 92 | def test_equivalent(self, tasks_db, task): 93 | """Similar test, just within a class.""" 94 | task_id = tasks.add(task) 95 | t_from_db = tasks.get(task_id) 96 | assert equivalent(t_from_db, task) 97 | 98 | def test_valid_id(self, tasks_db, task): 99 | """We can use the same data for multiple tests.""" 100 | task_id = tasks.add(task) 101 | t_from_db = tasks.get(task_id) 102 | assert t_from_db.id == task_id 103 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/func/test_add_variety2.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | tasks_to_try = ( 8 | Task("sleep", done=True), 9 | Task("wake", "brian"), 10 | Task("breathe", "BRIAN", True), 11 | Task("exercise", "BrIaN", False), 12 | ) 13 | 14 | task_ids = ["Task({},{},{})".format(t.summary, t.owner, t.done) for t in tasks_to_try] 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | return ( 20 | (t1.summary == t2.summary) and (t1.owner == t2.owner) and (t1.done == t2.done) 21 | ) 22 | 23 | 24 | @pytest.fixture(params=tasks_to_try) 25 | def a_task(request): 26 | """Using no ids.""" 27 | return request.param 28 | 29 | 30 | def test_add_a(tasks_db, a_task): 31 | """Using a_task fixture (no ids).""" 32 | task_id = tasks.add(a_task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, a_task) 35 | 36 | 37 | @pytest.fixture(params=tasks_to_try, ids=task_ids) 38 | def b_task(request): 39 | """Using a list of ids.""" 40 | return request.param 41 | 42 | 43 | def test_add_b(tasks_db, b_task): 44 | """Using b_task fixture, with ids.""" 45 | task_id = tasks.add(b_task) 46 | t_from_db = tasks.get(task_id) 47 | assert equivalent(t_from_db, b_task) 48 | 49 | 50 | def id_func(fixture_value): 51 | """A function for generating ids.""" 52 | t = fixture_value 53 | return "Task({},{},{})".format(t.summary, t.owner, t.done) 54 | 55 | 56 | @pytest.fixture(params=tasks_to_try, ids=id_func) 57 | def c_task(request): 58 | """Using a function (id_func) to generate ids.""" 59 | return request.param 60 | 61 | 62 | def test_add_c(tasks_db, c_task): 63 | """Use fixture with generated ids.""" 64 | task_id = tasks.add(c_task) 65 | t_from_db = tasks.get(task_id) 66 | assert equivalent(t_from_db, c_task) 67 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | 3 | import pytest 4 | import tasks 5 | 6 | 7 | def test_add_raises(): 8 | """add() should raise an exception with wrong type param.""" 9 | with pytest.raises(TypeError): 10 | tasks.add(task='not a Task object') 11 | 12 | 13 | @pytest.mark.smoke 14 | def test_list_raises(): 15 | """list() should raise an exception with wrong type param.""" 16 | with pytest.raises(TypeError): 17 | tasks.list_tasks(owner=123) 18 | 19 | 20 | @pytest.mark.get 21 | @pytest.mark.smoke 22 | def test_get_raises(): 23 | """get() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.get(task_id='123') 26 | 27 | 28 | class TestUpdate(): 29 | """Test expected exceptions with tasks.update().""" 30 | 31 | def test_bad_id(self): 32 | """A non-int id should raise an excption.""" 33 | with pytest.raises(TypeError): 34 | tasks.update(task_id={'dict instead': 1}, 35 | task=tasks.Task()) 36 | 37 | def test_bad_task(self): 38 | """A non-Task task should raise an excption.""" 39 | with pytest.raises(TypeError): 40 | tasks.update(task_id=1, task='not a task') 41 | 42 | 43 | def test_delete_raises(): 44 | """delete() should raise an exception with wrong type param.""" 45 | with pytest.raises(TypeError): 46 | tasks.delete(task_id=(1, 2, 3)) 47 | 48 | 49 | def test_start_tasks_db_raises(): 50 | """Make sure unsupported db raises an exception.""" 51 | with pytest.raises(ValueError) as excinfo: 52 | tasks.start_tasks_db('some/great/path', 'mysql') 53 | exception_msg = excinfo.value.args[0] 54 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 55 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch3/b/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ---------------------------------------------------- 5 | 6 | 0.1.0 (ch3/c/tasks_proj/tests) 7 | ------------------------------ 8 | 9 | Changes to tests: 10 | ~~~~~~~~~~~~~~~~~ 11 | 12 | - modify tests/conftest.py: 13 | - parametrize ``tasks_db_session`` to test both TinyDB and MongoDB. 14 | 15 | Known Issues: 16 | ~~~~~~~~~~~~~ 17 | 18 | - Lots of tests fail. 19 | - possibly due to some problem with task_id with the MongoDB version 20 | 21 | ---------------------------------------------------- 22 | 23 | 0.1.0 (ch3/b/tasks_proj/tests) 24 | ------------------------------ 25 | 26 | Changes to tests: 27 | ~~~~~~~~~~~~~~~~~ 28 | 29 | - modify tests/conftest.py: 30 | - Create a session scope fixture ``tasks_db_session`` 31 | that connects to db. 32 | - Have ``tasks_db`` fixture use ``tasks_db_session`` and 33 | just clean out db between tests. 34 | 35 | - add tests/func/test_add_variety2.py 36 | - demonstrate paramterized fixtures 37 | 38 | 39 | ---------------------------------------------------- 40 | 41 | 0.1.0 (ch3/a/tasks_proj/tests) 42 | ------------------------------ 43 | 44 | Changes to tests: 45 | ~~~~~~~~~~~~~~~~~ 46 | 47 | - add tests/conftest.py with fixtures: 48 | - tasks_just_a_few : 3 tasks in a tuple 49 | - tasks_mult_per_owner : 9 tasks with 3 owners 50 | - tasks_db : connection to db, using TinyDB 51 | - db_with_3_tasks : db prefilled with 3 tasks 52 | - db_with_multi_per_owner : db prefilled with 9 tasks 53 | 54 | - modify to use fixtures: 55 | - test_add.py 56 | - test_add_variety.py 57 | - test_api_exceptions.py 58 | - test_unique_id.py 59 | 60 | - remove tests/unit/test_task_fail.py 61 | - it was just to demo failures 62 | 63 | - remove tests/func/test_unique_id_1.py 64 | - remove tests/func/test_unique_id_2.py 65 | - remove tests/func/test_unique_id_3.py 66 | - remove tests/func/test_unique_id_4.py 67 | - add tests/func/test_unique_id.py 68 | - just need one unique_id test. 69 | 70 | 71 | ---------------------------------------------------- 72 | 73 | 0.1.0 (ch2/tasks_proj/tests) 74 | ---------------------------- 75 | 76 | Changes to tests: 77 | ~~~~~~~~~~~~~~~~~ 78 | 79 | - added tests/unit/test_Task.py 80 | - a few tests to demonstrate running tests 81 | 82 | - added tests/unit/test_Task_fail.py 83 | - demonstrate test failure 84 | 85 | - added tests/func/test_api_exceptions.py 86 | - testing for expected exceptions 87 | 88 | - added tests/func/test_add.py 89 | - testing ``tasks.add()`` 90 | - demonstrate user defined markers 91 | 92 | - added tests/func/test_unique_id_1.py 93 | - initial tests for ``tasks.unique_id()``. 94 | 95 | - added tests/func/test_unique_id_2.py 96 | - demonstrate ``@pytest.mark.skip()``. 97 | 98 | - added tests/func/test_unique_id_3.py : 99 | - demonstrate ``@pytest.mark.skipif()``. 100 | 101 | - added tests/func/test_unique_id_4.py 102 | - demonstrate ``@pytest.mark.xfail()``. 103 | 104 | - added tests/func/test_add_variety.py 105 | - demonstrate ``@pytest.mark.parametrize`` on functions and classes. 106 | 107 | 108 | ------------------------------------------------------ 109 | 110 | 0.1.0 111 | ----- 112 | 113 | Changes: 114 | ~~~~~~~~ 115 | 116 | - Initial version. 117 | 118 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | # @pytest.fixture(scope='session', params=['tiny',]) 9 | @pytest.fixture(scope="session", params=["tiny", "mongo"]) 10 | def tasks_db_session(tmpdir_factory, request): 11 | """Connect to db before tests, disconnect after.""" 12 | temp_dir = tmpdir_factory.mktemp("temp") 13 | tasks.start_tasks_db(str(temp_dir), request.param) 14 | yield # this is where the testing happens 15 | tasks.stop_tasks_db() 16 | 17 | 18 | @pytest.fixture() 19 | def tasks_db(tasks_db_session): 20 | """An empty tasks db.""" 21 | tasks.delete_all() 22 | 23 | 24 | # Reminder of Task constructor interface 25 | # Task(summary=None, owner=None, done=False, id=None) 26 | # Don't set id, it's set by database 27 | # owner and done are optional 28 | 29 | 30 | @pytest.fixture(scope="session") 31 | def tasks_just_a_few(): 32 | """All summaries and owners are unique.""" 33 | return ( 34 | Task("Write some code", "Brian", True), 35 | Task("Code review Brian's code", "Katie", False), 36 | Task("Fix what Brian did", "Michelle", False), 37 | ) 38 | 39 | 40 | @pytest.fixture(scope="session") 41 | def tasks_mult_per_owner(): 42 | """Several owners with several tasks each.""" 43 | return ( 44 | Task("Make a cookie", "Raphael"), 45 | Task("Use an emoji", "Raphael"), 46 | Task("Move to Berlin", "Raphael"), 47 | Task("Create", "Michelle"), 48 | Task("Inspire", "Michelle"), 49 | Task("Encourage", "Michelle"), 50 | Task("Do a handstand", "Daniel"), 51 | Task("Write some books", "Daniel"), 52 | Task("Eat ice cream", "Daniel"), 53 | ) 54 | 55 | 56 | @pytest.fixture() 57 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 58 | """Connected db with 3 tasks, all unique.""" 59 | for t in tasks_just_a_few: 60 | tasks.add(t) 61 | 62 | 63 | @pytest.fixture() 64 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 65 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 66 | for t in tasks_mult_per_owner: 67 | tasks.add(t) 68 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task('do something') 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task('sit in chair', owner='me', done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task('throw a party')) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/func/test_add_variety.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_1(tasks_db): 9 | """tasks.get() using id returned from add() works.""" 10 | task = Task('breathe', 'BRIAN', True) 11 | task_id = tasks.add(task) 12 | t_from_db = tasks.get(task_id) 13 | # everything but the id should be the same 14 | assert equivalent(t_from_db, task) 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | # Compare everything but the id field 20 | return ((t1.summary == t2.summary) and 21 | (t1.owner == t2.owner) and 22 | (t1.done == t2.done)) 23 | 24 | 25 | @pytest.mark.parametrize('task', 26 | [Task('sleep', done=True), 27 | Task('wake', 'brian'), 28 | Task('breathe', 'BRIAN', True), 29 | Task('exercise', 'BrIaN', False)]) 30 | def test_add_2(tasks_db, task): 31 | """Demonstrate parametrize with one parameter.""" 32 | task_id = tasks.add(task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, task) 35 | 36 | 37 | @pytest.mark.parametrize('summary, owner, done', 38 | [('sleep', None, False), 39 | ('wake', 'brian', False), 40 | ('breathe', 'BRIAN', True), 41 | ('eat eggs', 'BrIaN', False), 42 | ]) 43 | def test_add_3(tasks_db, summary, owner, done): 44 | """Demonstrate parametrize with multiple parameters.""" 45 | task = Task(summary, owner, done) 46 | task_id = tasks.add(task) 47 | t_from_db = tasks.get(task_id) 48 | assert equivalent(t_from_db, task) 49 | 50 | 51 | tasks_to_try = (Task('sleep', done=True), 52 | Task('wake', 'brian'), 53 | Task('breathe', 'BRIAN', True), 54 | Task('exercise', 'BrIaN', False)) 55 | 56 | 57 | @pytest.mark.parametrize('task', tasks_to_try) 58 | def test_add_4(tasks_db, task): 59 | """Slightly different take.""" 60 | task_id = tasks.add(task) 61 | t_from_db = tasks.get(task_id) 62 | assert equivalent(t_from_db, task) 63 | 64 | 65 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 66 | for t in tasks_to_try] 67 | 68 | 69 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 70 | def test_add_5(tasks_db, task): 71 | """Demonstrate ids.""" 72 | task_id = tasks.add(task) 73 | t_from_db = tasks.get(task_id) 74 | assert equivalent(t_from_db, task) 75 | 76 | 77 | @pytest.mark.parametrize('task', [ 78 | pytest.param(Task('create'), id='just summary'), 79 | pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 80 | pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 81 | def test_add_6(task): 82 | """Demonstrate pytest.param and id.""" 83 | task_id = tasks.add(task) 84 | t_from_db = tasks.get(task_id) 85 | assert equivalent(t_from_db, task) 86 | 87 | 88 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 89 | class TestAdd(): 90 | """Demonstrate paramterize and test classes.""" 91 | 92 | def test_equivalent(self, tasks_db, task): 93 | """Similar test, just within a class.""" 94 | task_id = tasks.add(task) 95 | t_from_db = tasks.get(task_id) 96 | assert equivalent(t_from_db, task) 97 | 98 | def test_valid_id(self, tasks_db, task): 99 | """We can use the same data for multiple tests.""" 100 | task_id = tasks.add(task) 101 | t_from_db = tasks.get(task_id) 102 | assert t_from_db.id == task_id 103 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/func/test_add_variety2.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | tasks_to_try = (Task('sleep', done=True), 8 | Task('wake', 'brian'), 9 | Task('breathe', 'BRIAN', True), 10 | Task('exercise', 'BrIaN', False)) 11 | 12 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 13 | for t in tasks_to_try] 14 | 15 | 16 | def equivalent(t1, t2): 17 | """Check two tasks for equivalence.""" 18 | return ((t1.summary == t2.summary) and 19 | (t1.owner == t2.owner) and 20 | (t1.done == t2.done)) 21 | 22 | 23 | @pytest.fixture(params=tasks_to_try) 24 | def a_task(request): 25 | return request.param 26 | 27 | 28 | def test_add_a(tasks_db, a_task): 29 | task_id = tasks.add(a_task) 30 | t_from_db = tasks.get(task_id) 31 | assert equivalent(t_from_db, a_task) 32 | 33 | 34 | @pytest.fixture(params=tasks_to_try, ids=task_ids) 35 | def b_task(request): 36 | return request.param 37 | 38 | 39 | def test_add_b(tasks_db, b_task): 40 | task_id = tasks.add(b_task) 41 | t_from_db = tasks.get(task_id) 42 | assert equivalent(t_from_db, b_task) 43 | 44 | 45 | def id_func(fixture_value): 46 | """A function for generating ids.""" 47 | t = fixture_value 48 | return 'Task({},{},{})'.format(t.summary, t.owner, t.done) 49 | 50 | 51 | @pytest.fixture(params=tasks_to_try, ids=id_func) 52 | def c_task(request): 53 | """Using a function (id_func) to generate ids.""" 54 | return request.param 55 | 56 | 57 | def test_add_c(tasks_db, c_task): 58 | """Use fixture with generated ids.""" 59 | task_id = tasks.add(c_task) 60 | t_from_db = tasks.get(task_id) 61 | assert equivalent(t_from_db, c_task) 62 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | 3 | import pytest 4 | import tasks 5 | 6 | 7 | def test_add_raises(): 8 | """add() should raise an exception with wrong type param.""" 9 | with pytest.raises(TypeError): 10 | tasks.add(task='not a Task object') 11 | 12 | 13 | @pytest.mark.smoke 14 | def test_list_raises(): 15 | """list() should raise an exception with wrong type param.""" 16 | with pytest.raises(TypeError): 17 | tasks.list_tasks(owner=123) 18 | 19 | 20 | @pytest.mark.get 21 | @pytest.mark.smoke 22 | def test_get_raises(): 23 | """get() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.get(task_id='123') 26 | 27 | 28 | class TestUpdate(): 29 | """Test expected exceptions with tasks.update().""" 30 | 31 | def test_bad_id(self): 32 | """A non-int id should raise an excption.""" 33 | with pytest.raises(TypeError): 34 | tasks.update(task_id={'dict instead': 1}, 35 | task=tasks.Task()) 36 | 37 | def test_bad_task(self): 38 | """A non-Task task should raise an excption.""" 39 | with pytest.raises(TypeError): 40 | tasks.update(task_id=1, task='not a task') 41 | 42 | 43 | def test_delete_raises(): 44 | """delete() should raise an exception with wrong type param.""" 45 | with pytest.raises(TypeError): 46 | tasks.delete(task_id=(1, 2, 3)) 47 | 48 | 49 | def test_start_tasks_db_raises(): 50 | """Make sure unsupported db raises an exception.""" 51 | with pytest.raises(ValueError) as excinfo: 52 | tasks.start_tasks_db('some/great/path', 'mysql') 53 | exception_msg = excinfo.value.args[0] 54 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 55 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch3/c/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch3/test_autouse.py: -------------------------------------------------------------------------------- 1 | """Demonstrate autouse fixtures.""" 2 | 3 | import time 4 | import pytest 5 | 6 | 7 | @pytest.fixture(autouse=True, scope="session") 8 | def footer_session_scope(): 9 | """Report the time at the end of a session.""" 10 | yield 11 | now = time.time() 12 | print("--") 13 | print("finished : {}".format(time.strftime("%d %b %X", time.localtime(now)))) 14 | print("-----------------") 15 | 16 | 17 | @pytest.fixture(autouse=True) 18 | def footer_function_scope(): 19 | """Report test durations after each function.""" 20 | start = time.time() 21 | yield 22 | stop = time.time() 23 | delta = stop - start 24 | print("\ntest duration : {:0.3} seconds".format(delta)) 25 | 26 | 27 | def test_1(): 28 | """Simulate long-ish running test.""" 29 | time.sleep(1) 30 | 31 | 32 | def test_2(): 33 | """Simulate slightly longer test.""" 34 | time.sleep(1.23) 35 | -------------------------------------------------------------------------------- /ch3/test_fixtures.py: -------------------------------------------------------------------------------- 1 | """Demonstrate simple fixtures.""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture() 7 | def some_data(): 8 | """Return answer to ultimate question.""" 9 | return 42 10 | 11 | 12 | def test_some_data(some_data): 13 | """Use fixture return value in a test.""" 14 | assert some_data == 42 15 | 16 | 17 | @pytest.fixture() 18 | def some_other_data(): 19 | """Raise an exception from fixture.""" 20 | return 1 / 0 21 | 22 | 23 | def test_other_data(some_other_data): 24 | """Try to use failing fixture.""" 25 | assert some_data == 42 26 | 27 | 28 | @pytest.fixture() 29 | def a_tuple(): 30 | """Return something more interesting.""" 31 | return (1, "foo", None, {"bar": 23}) 32 | 33 | 34 | def test_a_tuple(a_tuple): 35 | """Demo the a_tuple fixture.""" 36 | assert a_tuple[3]["bar"] == 32 37 | -------------------------------------------------------------------------------- /ch3/test_rename_fixture.py: -------------------------------------------------------------------------------- 1 | """Demonstrate fixture renaming.""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(name="lue") 7 | def ultimate_answer_to_life_the_universe_and_everything(): 8 | """Return ultimate answer.""" 9 | return 42 10 | 11 | 12 | def test_everything(lue): 13 | """Use the shorter name.""" 14 | assert lue == 42 15 | -------------------------------------------------------------------------------- /ch3/test_scope.py: -------------------------------------------------------------------------------- 1 | """Demo fixture scope.""" 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function") 7 | def func_scope(): 8 | """A function scope fixture.""" 9 | 10 | 11 | @pytest.fixture(scope="module") 12 | def mod_scope(): 13 | """A module scope fixture.""" 14 | 15 | 16 | @pytest.fixture(scope="session") 17 | def sess_scope(): 18 | """A session scope fixture.""" 19 | 20 | 21 | @pytest.fixture(scope="class") 22 | def class_scope(): 23 | """A class scope fixture.""" 24 | 25 | 26 | def test_1(sess_scope, mod_scope, func_scope): 27 | """Test using session, module, and function scope fixtures.""" 28 | 29 | 30 | def test_2(sess_scope, mod_scope, func_scope): 31 | """Demo is more fun with multiple tests.""" 32 | 33 | 34 | @pytest.mark.usefixtures("class_scope") 35 | class TestSomething: 36 | """Demo class scope fixtures.""" 37 | 38 | def test_3(self): 39 | """Test using a class scope fixture.""" 40 | 41 | def test_4(self): 42 | """Again, multiple tests are more fun.""" 43 | -------------------------------------------------------------------------------- /ch4/authors/conftest.py: -------------------------------------------------------------------------------- 1 | """Demonstrate tmpdir_factory.""" 2 | 3 | import json 4 | import pytest 5 | 6 | 7 | @pytest.fixture(scope="module") 8 | def author_file_json(tmpdir_factory): 9 | """Write some authors to a data file.""" 10 | python_author_data = { 11 | "Ned": {"City": "Boston"}, 12 | "Brian": {"City": "Portland"}, 13 | "Luciano": {"City": "Sau Paulo"}, 14 | } 15 | 16 | file = tmpdir_factory.mktemp("data").join("author_file.json") 17 | print("file:{}".format(str(file))) 18 | 19 | with file.open("w") as f: 20 | json.dump(python_author_data, f) 21 | return file 22 | -------------------------------------------------------------------------------- /ch4/authors/test_authors.py: -------------------------------------------------------------------------------- 1 | """Some tests that use temp data files.""" 2 | import json 3 | 4 | 5 | def test_brian_in_portland(author_file_json): 6 | """A test that uses a data file.""" 7 | with author_file_json.open() as f: 8 | authors = json.load(f) 9 | assert authors["Brian"]["City"] == "Portland" 10 | 11 | 12 | def test_all_have_cities(author_file_json): 13 | """Same file is used for both tests.""" 14 | with author_file_json.open() as f: 15 | authors = json.load(f) 16 | for a in authors: 17 | assert len(authors[a]["City"]) > 0 18 | -------------------------------------------------------------------------------- /ch4/cache/test_few_failures.py: -------------------------------------------------------------------------------- 1 | """Demonstrate -lf and -ff with failing tests.""" 2 | 3 | import pytest 4 | from pytest import approx 5 | 6 | 7 | testdata = [ 8 | # x, y, expected 9 | (1.01, 2.01, 3.02), 10 | (1e25, 1e23, 1.1e25), 11 | (1.23, 3.21, 4.44), 12 | (0.1, 0.2, 0.3), 13 | (1e25, 1e24, 1.1e25) 14 | ] 15 | 16 | 17 | @pytest.mark.parametrize("x,y,expected", testdata) 18 | def test_a(x, y, expected): 19 | """Demo approx().""" 20 | sum_ = x + y 21 | assert sum_ == approx(expected) 22 | -------------------------------------------------------------------------------- /ch4/cache/test_pass_fail.py: -------------------------------------------------------------------------------- 1 | def test_this_passes(): 2 | assert 1 == 1 3 | 4 | 5 | def test_this_fails(): 6 | assert 1 == 2 7 | -------------------------------------------------------------------------------- /ch4/cache/test_slower.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | import time 4 | import pytest 5 | 6 | 7 | @pytest.fixture(autouse=True) 8 | def check_duration(request, cache): 9 | key = create_key(request.node.nodeid) 10 | start_time = datetime.datetime.now() 11 | yield 12 | stop_time = datetime.datetime.now() 13 | this_duration = (stop_time - start_time).total_seconds() 14 | last_duration = cache.get(key, None) 15 | cache.set(key, this_duration) 16 | if last_duration is not None: 17 | errorstring = "test duration over 2x last duration" 18 | assert this_duration <= last_duration * 2, errorstring 19 | 20 | 21 | def create_key(nodeid): 22 | """nodeids can have colons; keys become filenames within .pytest_cache; replace 23 | colons with something filename safe""" 24 | return "duration/" + nodeid.replace(":", "_") 25 | 26 | 27 | @pytest.mark.parametrize("i", range(5)) 28 | def test_slow_stuff(i): 29 | time.sleep(random.random()) 30 | -------------------------------------------------------------------------------- /ch4/cache/test_slower_2.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | import time 4 | from collections import namedtuple 5 | import pytest 6 | 7 | Duration = namedtuple("Duration", ["current", "last"]) 8 | 9 | 10 | @pytest.fixture(scope="session") 11 | def duration_cache(request): 12 | """We can't use `cache` fixture because it has function scope. However the `cache` 13 | fixture simply returns `request.config.cache`, which is available in any scope.""" 14 | key = "duration/testdurations" 15 | d = Duration({}, request.config.cache.get(key, {})) 16 | yield d 17 | request.config.cache.set(key, d.current) 18 | 19 | 20 | @pytest.fixture(autouse=True) 21 | def check_duration(request, duration_cache): 22 | d = duration_cache 23 | nodeid = request.node.nodeid 24 | start_time = datetime.datetime.now() 25 | yield 26 | duration = (datetime.datetime.now() - start_time).total_seconds() 27 | d.current[nodeid] = duration 28 | if d.last.get(nodeid, None) is not None: 29 | errorstring = "test duration over 2x last duration" 30 | assert duration <= (d.last[nodeid] * 2), errorstring 31 | 32 | 33 | @pytest.mark.parametrize("i", range(5)) 34 | def test_slow_stuff(i): 35 | time.sleep(random.random()) 36 | -------------------------------------------------------------------------------- /ch4/cap/test_capfd.py: -------------------------------------------------------------------------------- 1 | def greeting(name): 2 | print('Hi,', name) 3 | 4 | 5 | def test_greeting(capfd): 6 | greeting('Brian') 7 | out, err = capfd.readouterr() 8 | assert out == 'Hi, Brian\n' 9 | 10 | 11 | def test_multiline(capfd): 12 | greeting('Brian') 13 | greeting('Nerd') 14 | out, err = capfd.readouterr() 15 | assert out == 'Hi, Brian\nHi, Nerd\n' 16 | 17 | 18 | def test_disabling_capturing(capfd): 19 | print('this output is captured') 20 | with capfd.disabled(): 21 | print('output not captured') 22 | print('this output is also captured') 23 | -------------------------------------------------------------------------------- /ch4/cap/test_capsys.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sys 3 | import pytest 4 | 5 | 6 | def greeting(name): 7 | print("Hi, {}".format(name)) 8 | 9 | 10 | def test_greeting(capsys): 11 | greeting("Earthling") 12 | out, err = capsys.readouterr() 13 | assert out == "Hi, Earthling\n" 14 | assert err == "" 15 | 16 | greeting("Brian") 17 | greeting("Nerd") 18 | out, err = capsys.readouterr() 19 | assert out == "Hi, Brian\nHi, Nerd\n" 20 | assert err == "" 21 | 22 | 23 | def yikes(problem): 24 | print("YIKES! {}".format(problem), file=sys.stderr) 25 | 26 | 27 | def test_yikes(capsys): 28 | yikes("Out of coffee!") 29 | out, err = capsys.readouterr() 30 | assert out == "" 31 | assert "Out of coffee!" in err 32 | 33 | 34 | def test_capsys_disabled(capsys): 35 | """pytest usually captures the output from your tests and the code under test, 36 | including print statements. However, you may want to allow some output to make it 37 | through the default pytest output capture, to print some things without printing 38 | everything. Use `with capsys.disabled()` to temporarily let output get past the 39 | capture mechanism - to always print it""" 40 | with capsys.disabled(): 41 | print("\nalways print this") 42 | 43 | # Normal print, usually captured is only seen in the output when we pass in the -s 44 | # flag, which is a shortcut for --capture=no, turning off output capture 45 | print("normal print, usually captured") 46 | 47 | 48 | @pytest.mark.parametrize("i", range(40)) 49 | def test_for_fun(i, capsys): 50 | if random.randint(1, 10) == 2: 51 | with capsys.disabled(): 52 | sys.stdout.write("F") 53 | -------------------------------------------------------------------------------- /ch4/dt/1/unnecessary_math.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines multiply(a, b) and divide(a, b). 3 | 4 | >>> import unnecessary_math as um 5 | 6 | Here's how you use multiply: 7 | 8 | >>> um.multiply(4, 3) 9 | 12 10 | >>> um.multiply('a', 3) 11 | 'aaa' 12 | 13 | 14 | Here's how you use divide: 15 | 16 | >>> um.divide(10, 5) 17 | 2.0 18 | """ 19 | 20 | 21 | def multiply(a, b): 22 | """ 23 | Returns a multiplied by b. 24 | 25 | >>> um.multiply(4, 3) 26 | 12 27 | >>> um.multiply('a', 3) 28 | 'aaa' 29 | """ 30 | return a * b 31 | 32 | 33 | def divide(a, b): 34 | """ 35 | Returns a divided by b. 36 | 37 | >>> um.divide(10, 5) 38 | 2.0 39 | """ 40 | return a / b 41 | -------------------------------------------------------------------------------- /ch4/dt/2/unnecessary_math.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines multiply(a, b) and divide(a, b). 3 | 4 | >>> import unnecessary_math as um 5 | 6 | Here's how you use multiply: 7 | 8 | >>> um.multiply(4, 3) 9 | 12 10 | >>> um.multiply('a', 3) 11 | 'aaa' 12 | 13 | Here's how you use divide: 14 | 15 | >>> um.divide(10, 5) 16 | 2.0 17 | """ 18 | 19 | 20 | def multiply(a, b): 21 | """ 22 | Returns a multiplied by b. 23 | 24 | >>> import unnecessary_math as um 25 | >>> um.multiply(4, 3) 26 | 12 27 | >>> um.multiply('a', 3) 28 | 'aaa' 29 | """ 30 | return a * b 31 | 32 | 33 | def divide(a, b): 34 | """ 35 | Returns a divided by b. 36 | 37 | >>> import unnecessary_math as um 38 | >>> um.divide(10, 5) 39 | 2.0 40 | """ 41 | return a / b 42 | -------------------------------------------------------------------------------- /ch4/dt/3/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unnecessary_math 3 | 4 | 5 | @pytest.fixture(autouse=True) 6 | def add_um(doctest_namespace): 7 | doctest_namespace["um"] = unnecessary_math 8 | -------------------------------------------------------------------------------- /ch4/dt/3/unnecessary_math.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines multiply(a, b) and divide(a, b). 3 | 4 | >>> import unnecessary_math as um 5 | 6 | Here's how you use multiply: 7 | 8 | >>> um.multiply(4, 3) 9 | 12 10 | >>> um.multiply('a', 3) 11 | 'aaa' 12 | 13 | Here's how you use divide: 14 | 15 | >>> um.divide(10, 5) 16 | 2.0 17 | """ 18 | 19 | 20 | def multiply(a, b): 21 | """ 22 | Returns a multiplied by b. 23 | 24 | >>> um.multiply(4, 3) 25 | 12 26 | >>> um.multiply('a', 3) 27 | 'aaa' 28 | """ 29 | return a * b 30 | 31 | 32 | def divide(a, b): 33 | """ 34 | Returns a divided by b. 35 | 36 | >>> um.divide(10, 5) 37 | 2.0 38 | """ 39 | return a / b 40 | -------------------------------------------------------------------------------- /ch4/monkey/cheese.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | 5 | def read_cheese_preferences(): 6 | full_path = os.path.expanduser("~/.cheese.json") 7 | with open(full_path, "r") as f: 8 | prefs = json.load(f) 9 | return prefs 10 | 11 | 12 | def write_cheese_preferences(prefs): 13 | full_path = os.path.expanduser("~/.cheese.json") 14 | with open(full_path, "w") as f: 15 | json.dump(prefs, f, indent=4) 16 | 17 | 18 | def write_default_cheese_preferences(): 19 | write_cheese_preferences(_default_prefs) 20 | 21 | 22 | _default_prefs = { 23 | "slicing": ["manchego", "sharp cheddar"], 24 | "spreadable": [ 25 | "Saint Andre", 26 | "camembert", 27 | "bucheron", 28 | "goat", 29 | "humbolt fog", 30 | "cambozola", 31 | ], 32 | "salads": ["crumbled feta"], 33 | } 34 | 35 | if __name__ == "__main__": 36 | write_default_cheese_preferences() 37 | -------------------------------------------------------------------------------- /ch4/monkey/test_cheese.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import cheese 3 | 4 | 5 | def test_def_prefs_full(): 6 | cheese.write_default_cheese_preferences() 7 | expected = cheese._default_prefs 8 | actual = cheese.read_cheese_preferences() 9 | assert expected == actual 10 | 11 | 12 | def test_def_prefs_change_home(tmpdir, monkeypatch): 13 | monkeypatch.setenv("HOME", tmpdir.mkdir("home")) 14 | cheese.write_default_cheese_preferences() 15 | expected = cheese._default_prefs 16 | actual = cheese.read_cheese_preferences() 17 | assert expected == actual 18 | 19 | 20 | def test_def_prefs_change_expanduser(tmpdir, monkeypatch): 21 | fake_home_dir = tmpdir.mkdir("home") 22 | monkeypatch.setattr( 23 | cheese.os.path, "expanduser", (lambda x: x.replace("~", str(fake_home_dir))) 24 | ) 25 | cheese.write_default_cheese_preferences() 26 | expected = cheese._default_prefs 27 | actual = cheese.read_cheese_preferences() 28 | assert expected == actual 29 | 30 | 31 | def test_def_prefs_change_defaults(tmpdir, monkeypatch): 32 | # write the file once 33 | fake_home_dir = tmpdir.mkdir("home") 34 | monkeypatch.setattr( 35 | cheese.os.path, "expanduser", (lambda x: x.replace("~", str(fake_home_dir))) 36 | ) 37 | cheese.write_default_cheese_preferences() 38 | defaults_before = copy.deepcopy(cheese._default_prefs) 39 | 40 | # change the defaults 41 | monkeypatch.setitem(cheese._default_prefs, "slicing", ["provolone"]) 42 | monkeypatch.setitem(cheese._default_prefs, "spreadable", ["brie"]) 43 | monkeypatch.setitem(cheese._default_prefs, "salads", ["pepper jack"]) 44 | defaults_modified = cheese._default_prefs 45 | 46 | # write it again with modified defaults 47 | cheese.write_default_cheese_preferences() 48 | 49 | # read, and check 50 | actual = cheese.read_cheese_preferences() 51 | assert defaults_modified == actual 52 | assert defaults_modified != defaults_before 53 | -------------------------------------------------------------------------------- /ch4/monkey/test_monkey.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | 4 | 5 | class Point(object): 6 | def __init__(self, x, y): 7 | self.x = x 8 | self.y = y 9 | print('Point.__init__()') 10 | 11 | def __repr__(self): 12 | return 'Point({},{})'.format(self.x, self.y) 13 | 14 | def __str__(self): 15 | return '({},{})'.format(self.x, self.y) 16 | 17 | 18 | @pytest.fixture(name='a_point', scope='module') 19 | def point_fixture(): 20 | return Point(1, 2) 21 | 22 | 23 | def test_point_repr(a_point, monkeypatch): 24 | monkeypatch.setattr(a_point, 'x', 10) 25 | assert repr(a_point) == 'Point(10,2)' 26 | 27 | 28 | def test_point_str(a_point, monkeypatch): 29 | monkeypatch.setattr(a_point, 'y', 20) 30 | assert str(a_point) == '(1,20)' 31 | 32 | 33 | def test_point_missing_x(a_point, monkeypatch): 34 | monkeypatch.delattr(a_point, 'x') 35 | with pytest.raises(AttributeError): 36 | str(a_point) 37 | 38 | 39 | def test_env(monkeypatch): 40 | monkeypatch.setenv('HOME', '/Users/foo') 41 | assert os.environ['HOME'] == '/Users/foo' 42 | 43 | 44 | def test_prepend(monkeypatch): 45 | monkeypatch.setenv('PATH', '/Users/okken/bin', ':') 46 | assert os.environ['PATH'].startswith('/Users/okken/bin') 47 | -------------------------------------------------------------------------------- /ch4/pytestconfig/conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_addoption(parser): 2 | parser.addoption("--myopt", action="store_true", help="some boolean option") 3 | parser.addoption("--foo", action="store", default="bar", help="foo: bar or baz") 4 | -------------------------------------------------------------------------------- /ch4/pytestconfig/test_config.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_option(pytestconfig): 5 | """Access custom options from a test.""" 6 | print('"foo" set to:', pytestconfig.getoption("foo")) 7 | print('"myopt" set to:', pytestconfig.getoption("myopt")) 8 | 9 | 10 | @pytest.fixture() 11 | def foo(pytestconfig): 12 | """Access `pytestconfig` from a fixture.""" 13 | return pytestconfig.option.foo 14 | 15 | 16 | @pytest.fixture() 17 | def myopt(pytestconfig): 18 | """Make fixture for the option name.""" 19 | return pytestconfig.option.myopt 20 | 21 | 22 | def test_fixtures_for_options(foo, myopt): 23 | print('"foo" set to:', foo) 24 | print('"myopt" set to:', myopt) 25 | 26 | 27 | def test_pytestconfig(pytestconfig): 28 | """Access builtin options as well as information about how pytest was started (the 29 | directory, the arguments, and so on).""" 30 | print("args :", pytestconfig.args) 31 | print("inifile :", pytestconfig.inifile) 32 | print("invocation_dir :", pytestconfig.invocation_dir) 33 | print("rootdir :", pytestconfig.rootdir) 34 | print("-k EXPRESSION :", pytestconfig.getoption("keyword")) 35 | print("-v, --verbose :", pytestconfig.getoption("verbose")) 36 | print("-q, --quiet :", pytestconfig.getoption("quiet")) 37 | print("-l, --showlocals:", pytestconfig.getoption("showlocals")) 38 | print("--tb=style :", pytestconfig.getoption("tbstyle")) 39 | 40 | 41 | def test_legacy(request): 42 | print('\n"foo" set to:', request.config.getoption("foo")) 43 | print('"myopt" set to:', request.config.getoption("myopt")) 44 | print('"keyword" set to:', request.config.getoption("keyword")) 45 | -------------------------------------------------------------------------------- /ch4/test_tmpdir.py: -------------------------------------------------------------------------------- 1 | def test_tmpdir(tmpdir): 2 | # tmpdir already has a path name associated with it 3 | # join() extends the path to include a filename 4 | # the file is created when it's written to 5 | a_file = tmpdir.join("something.txt") 6 | 7 | # you can create directories 8 | a_sub_dir = tmpdir.mkdir("anything") 9 | 10 | # you can create files in directories (created when written) 11 | another_file = a_sub_dir.join("something_else.txt") 12 | 13 | # this write creates 'something.txt' 14 | a_file.write("contents may settle during shipping") 15 | 16 | # this write creates 'anything/something_else.txt' 17 | another_file.write("something different") 18 | 19 | # you can read the files as well 20 | assert a_file.read() == "contents may settle during shipping" 21 | assert another_file.read() == "something different" 22 | 23 | 24 | def test_tmpdir_factory(tmpdir_factory): 25 | # you should start with making a directory 26 | # a_dir acts like the object returned from the tmpdir fixture 27 | a_dir = tmpdir_factory.mktemp("mydir") 28 | 29 | # base_temp will be the parent dir of 'mydir' 30 | # you don't have to use getbasetemp() 31 | # using it here just to show that it's available 32 | base_temp = tmpdir_factory.getbasetemp() 33 | print("base:", base_temp) 34 | 35 | # the rest of this test looks the same as the 'test_tmpdir()' 36 | # example except I'm using a_dir instead of tmpdir 37 | 38 | a_file = a_dir.join("something.txt") 39 | a_sub_dir = a_dir.mkdir("anything") 40 | another_file = a_sub_dir.join("something_else.txt") 41 | 42 | a_file.write("contents may settle during shipping") 43 | another_file.write("something different") 44 | 45 | assert a_file.read() == "contents may settle during shipping" 46 | assert another_file.read() == "something different" 47 | -------------------------------------------------------------------------------- /ch4/test_warnings.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import pytest 3 | 4 | 5 | def lame_function(): 6 | warnings.warn("Please stop using this", DeprecationWarning) 7 | # rest of function 8 | 9 | 10 | def test_lame_function(recwarn): 11 | lame_function() 12 | assert len(recwarn) == 1 13 | w = recwarn.pop() 14 | assert w.category == DeprecationWarning 15 | assert str(w.message) == "Please stop using this" 16 | 17 | 18 | def test_lame_function_2(): 19 | with pytest.warns(None) as warning_list: 20 | lame_function() 21 | 22 | assert len(warning_list) == 1 23 | w = warning_list.pop() 24 | assert w.category == DeprecationWarning 25 | assert str(w.message) == "Please stop using this" 26 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.fixture(scope='session') 9 | def tasks_db_session(tmpdir_factory, request): 10 | """Connect to db before tests, disconnect after.""" 11 | temp_dir = tmpdir_factory.mktemp('temp') 12 | tasks.start_tasks_db(str(temp_dir), 'tiny') 13 | yield # this is where the testing happens 14 | tasks.stop_tasks_db() 15 | 16 | 17 | @pytest.fixture() 18 | def tasks_db(tasks_db_session): 19 | """An empty tasks db.""" 20 | tasks.delete_all() 21 | 22 | 23 | # Reminder of Task constructor interface 24 | # Task(summary=None, owner=None, done=False, id=None) 25 | # Don't set id, it's set by database 26 | # owner and done are optional 27 | 28 | 29 | @pytest.fixture() 30 | def tasks_just_a_few(): 31 | """All summaries and owners are unique.""" 32 | return ( 33 | Task('Write some code', 'Brian', True), 34 | Task("Code review Brian's code", 'Katie', False), 35 | Task('Fix what Brian did', 'Michelle', False)) 36 | 37 | 38 | @pytest.fixture() 39 | def tasks_mult_per_owner(): 40 | """Several owners with several tasks each.""" 41 | return ( 42 | Task('Make a cookie', 'Raphael'), 43 | Task('Use an emoji', 'Raphael'), 44 | Task('Move to Berlin', 'Raphael'), 45 | 46 | Task('Create', 'Michelle'), 47 | Task('Inspire', 'Michelle'), 48 | Task('Encourage', 'Michelle'), 49 | 50 | Task('Do a handstand', 'Daniel'), 51 | Task('Write some books', 'Daniel'), 52 | Task('Eat ice cream', 'Daniel')) 53 | 54 | 55 | @pytest.fixture() 56 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 57 | """Connected db with 3 tasks, all unique.""" 58 | for t in tasks_just_a_few: 59 | tasks.add(t) 60 | 61 | 62 | @pytest.fixture() 63 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 64 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 65 | for t in tasks_mult_per_owner: 66 | tasks.add(t) 67 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task('do something') 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task('sit in chair', owner='me', done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task('throw a party')) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/func/test_add_variety.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_1(tasks_db): 9 | """tasks.get() using id returned from add() works.""" 10 | task = Task('breathe', 'BRIAN', True) 11 | task_id = tasks.add(task) 12 | t_from_db = tasks.get(task_id) 13 | # everything but the id should be the same 14 | assert equivalent(t_from_db, task) 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | # Compare everything but the id field 20 | return ((t1.summary == t2.summary) and 21 | (t1.owner == t2.owner) and 22 | (t1.done == t2.done)) 23 | 24 | 25 | @pytest.mark.parametrize('task', 26 | [Task('sleep', done=True), 27 | Task('wake', 'brian'), 28 | Task('breathe', 'BRIAN', True), 29 | Task('exercise', 'BrIaN', False)]) 30 | def test_add_2(tasks_db, task): 31 | """Demonstrate parametrize with one parameter.""" 32 | task_id = tasks.add(task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, task) 35 | 36 | 37 | @pytest.mark.parametrize('summary, owner, done', 38 | [('sleep', None, False), 39 | ('wake', 'brian', False), 40 | ('breathe', 'BRIAN', True), 41 | ('eat eggs', 'BrIaN', False), 42 | ]) 43 | def test_add_3(tasks_db, summary, owner, done): 44 | """Demonstrate parametrize with multiple parameters.""" 45 | task = Task(summary, owner, done) 46 | task_id = tasks.add(task) 47 | t_from_db = tasks.get(task_id) 48 | assert equivalent(t_from_db, task) 49 | 50 | 51 | tasks_to_try = (Task('sleep', done=True), 52 | Task('wake', 'brian'), 53 | Task('breathe', 'BRIAN', True), 54 | Task('exercise', 'BrIaN', False)) 55 | 56 | 57 | @pytest.mark.parametrize('task', tasks_to_try) 58 | def test_add_4(tasks_db, task): 59 | """Slightly different take.""" 60 | task_id = tasks.add(task) 61 | t_from_db = tasks.get(task_id) 62 | assert equivalent(t_from_db, task) 63 | 64 | 65 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 66 | for t in tasks_to_try] 67 | 68 | 69 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 70 | def test_add_5(tasks_db, task): 71 | """Demonstrate ids.""" 72 | task_id = tasks.add(task) 73 | t_from_db = tasks.get(task_id) 74 | assert equivalent(t_from_db, task) 75 | 76 | 77 | @pytest.mark.parametrize('task', [ 78 | pytest.param(Task('create'), id='just summary'), 79 | pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 80 | pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 81 | def test_add_6(task): 82 | """Demonstrate pytest.param and id.""" 83 | task_id = tasks.add(task) 84 | t_from_db = tasks.get(task_id) 85 | assert equivalent(t_from_db, task) 86 | 87 | 88 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 89 | class TestAdd(): 90 | """Demonstrate paramterize and test classes.""" 91 | 92 | def test_equivalent(self, tasks_db, task): 93 | """Similar test, just within a class.""" 94 | task_id = tasks.add(task) 95 | t_from_db = tasks.get(task_id) 96 | assert equivalent(t_from_db, task) 97 | 98 | def test_valid_id(self, tasks_db, task): 99 | """We can use the same data for multiple tests.""" 100 | task_id = tasks.add(task) 101 | t_from_db = tasks.get(task_id) 102 | assert t_from_db.id == task_id 103 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/func/test_add_variety2.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | tasks_to_try = (Task('sleep', done=True), 8 | Task('wake', 'brian'), 9 | Task('breathe', 'BRIAN', True), 10 | Task('exercise', 'BrIaN', False)) 11 | 12 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 13 | for t in tasks_to_try] 14 | 15 | 16 | def equivalent(t1, t2): 17 | """Check two tasks for equivalence.""" 18 | return ((t1.summary == t2.summary) and 19 | (t1.owner == t2.owner) and 20 | (t1.done == t2.done)) 21 | 22 | 23 | @pytest.fixture(params=tasks_to_try) 24 | def a_task(request): 25 | """Using no ids.""" 26 | return request.param 27 | 28 | 29 | def test_add_a(tasks_db, a_task): 30 | """Using a_task fixture (no ids).""" 31 | task_id = tasks.add(a_task) 32 | t_from_db = tasks.get(task_id) 33 | assert equivalent(t_from_db, a_task) 34 | 35 | 36 | @pytest.fixture(params=tasks_to_try, ids=task_ids) 37 | def b_task(request): 38 | """Using a list of ids.""" 39 | return request.param 40 | 41 | 42 | def test_add_b(tasks_db, b_task): 43 | """Using b_task fixture, with ids.""" 44 | task_id = tasks.add(b_task) 45 | t_from_db = tasks.get(task_id) 46 | assert equivalent(t_from_db, b_task) 47 | 48 | 49 | def id_func(fixture_value): 50 | """A function for generating ids.""" 51 | t = fixture_value 52 | return 'Task({},{},{})'.format(t.summary, t.owner, t.done) 53 | 54 | 55 | @pytest.fixture(params=tasks_to_try, ids=id_func) 56 | def c_task(request): 57 | """Using a function (id_func) to generate ids.""" 58 | return request.param 59 | 60 | 61 | def test_add_c(tasks_db, c_task): 62 | """Use fixture with generated ids.""" 63 | task_id = tasks.add(c_task) 64 | t_from_db = tasks.get(task_id) 65 | assert equivalent(t_from_db, c_task) 66 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | import pytest 3 | import tasks 4 | from tasks import Task 5 | 6 | 7 | @pytest.mark.usefixtures("tasks_db") 8 | class TestAdd: 9 | """Tests related to tasks.add().""" 10 | 11 | def test_missing_summary(self): 12 | """Should raise an exception if summary missing.""" 13 | with pytest.raises(ValueError): 14 | tasks.add(Task(owner="bob")) 15 | 16 | def test_done_not_bool(self): 17 | """Should raise an exception if done is not a bool.""" 18 | with pytest.raises(ValueError): 19 | tasks.add(Task(summary="summary", done="True")) 20 | 21 | 22 | def test_add_raises(): 23 | """add() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.add(task="not a Task object") 26 | 27 | 28 | @pytest.mark.smoke 29 | def test_list_raises(): 30 | """list() should raise an exception with wrong type param.""" 31 | with pytest.raises(TypeError): 32 | tasks.list_tasks(owner=123) 33 | 34 | 35 | @pytest.mark.get 36 | @pytest.mark.smoke 37 | def test_get_raises(): 38 | """get() should raise an exception with wrong type param.""" 39 | with pytest.raises(TypeError): 40 | tasks.get(task_id="123") 41 | 42 | 43 | class TestUpdate: 44 | """Test expected exceptions with tasks.update().""" 45 | 46 | def test_bad_id(self): 47 | """A non-int id should raise an exception.""" 48 | with pytest.raises(TypeError): 49 | tasks.update(task_id={"dict instead": 1}, task=tasks.Task()) 50 | 51 | def test_bad_task(self): 52 | """A non-Task task should raise an exception.""" 53 | with pytest.raises(TypeError): 54 | tasks.update(task_id=1, task="not a task") 55 | 56 | 57 | def test_delete_raises(): 58 | """delete() should raise an exception with wrong type param.""" 59 | with pytest.raises(TypeError): 60 | tasks.delete(task_id=(1, 2, 3)) 61 | 62 | 63 | def test_start_tasks_db_raises(): 64 | """Make sure unsupported db raises an exception.""" 65 | with pytest.raises(ValueError) as excinfo: 66 | tasks.start_tasks_db("some/great/path", "mysql") 67 | exception_msg = excinfo.value.args[0] 68 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 69 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch5/a/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.fixture(scope='session') 9 | def tasks_db_session(tmpdir_factory, request): 10 | """Connect to db before tests, disconnect after.""" 11 | temp_dir = tmpdir_factory.mktemp('temp') 12 | tasks.start_tasks_db(str(temp_dir), 'tiny') 13 | yield # this is where the testing happens 14 | tasks.stop_tasks_db() 15 | 16 | 17 | @pytest.fixture() 18 | def tasks_db(tasks_db_session): 19 | """An empty tasks db.""" 20 | tasks.delete_all() 21 | 22 | 23 | # Reminder of Task constructor interface 24 | # Task(summary=None, owner=None, done=False, id=None) 25 | # Don't set id, it's set by database 26 | # owner and done are optional 27 | 28 | 29 | @pytest.fixture() 30 | def tasks_just_a_few(): 31 | """All summaries and owners are unique.""" 32 | return ( 33 | Task('Write some code', 'Brian', True), 34 | Task("Code review Brian's code", 'Katie', False), 35 | Task('Fix what Brian did', 'Michelle', False)) 36 | 37 | 38 | @pytest.fixture() 39 | def tasks_mult_per_owner(): 40 | """Several owners with several tasks each.""" 41 | return ( 42 | Task('Make a cookie', 'Raphael'), 43 | Task('Use an emoji', 'Raphael'), 44 | Task('Move to Berlin', 'Raphael'), 45 | 46 | Task('Create', 'Michelle'), 47 | Task('Inspire', 'Michelle'), 48 | Task('Encourage', 'Michelle'), 49 | 50 | Task('Do a handstand', 'Daniel'), 51 | Task('Write some books', 'Daniel'), 52 | Task('Eat ice cream', 'Daniel')) 53 | 54 | 55 | @pytest.fixture() 56 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 57 | """Connected db with 3 tasks, all unique.""" 58 | for t in tasks_just_a_few: 59 | tasks.add(t) 60 | 61 | 62 | @pytest.fixture() 63 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 64 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 65 | for t in tasks_mult_per_owner: 66 | tasks.add(t) 67 | 68 | 69 | def pytest_report_header(): 70 | """Thank tester for running tests.""" 71 | return "Thanks for running the tests." 72 | 73 | 74 | def pytest_report_teststatus(report): 75 | """Turn failures into opportunities.""" 76 | if report.when == 'call' and report.failed: 77 | return (report.outcome, 'O', 'OPPORTUNITY for improvement') 78 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task('do something') 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task('sit in chair', owner='me', done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task('throw a party')) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/func/test_add_variety.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_1(tasks_db): 9 | """tasks.get() using id returned from add() works.""" 10 | task = Task('breathe', 'BRIAN', True) 11 | task_id = tasks.add(task) 12 | t_from_db = tasks.get(task_id) 13 | # everything but the id should be the same 14 | assert equivalent(t_from_db, task) 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | # Compare everything but the id field 20 | return ((t1.summary == t2.summary) and 21 | (t1.owner == t2.owner) and 22 | (t1.done == t2.done)) 23 | 24 | 25 | @pytest.mark.parametrize('task', 26 | [Task('sleep', done=True), 27 | Task('wake', 'brian'), 28 | Task('breathe', 'BRIAN', True), 29 | Task('exercise', 'BrIaN', False)]) 30 | def test_add_2(tasks_db, task): 31 | """Demonstrate parametrize with one parameter.""" 32 | task_id = tasks.add(task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, task) 35 | 36 | 37 | @pytest.mark.parametrize('summary, owner, done', 38 | [('sleep', None, False), 39 | ('wake', 'brian', False), 40 | ('breathe', 'BRIAN', True), 41 | ('eat eggs', 'BrIaN', False), 42 | ]) 43 | def test_add_3(tasks_db, summary, owner, done): 44 | """Demonstrate parametrize with multiple parameters.""" 45 | task = Task(summary, owner, done) 46 | task_id = tasks.add(task) 47 | t_from_db = tasks.get(task_id) 48 | assert equivalent(t_from_db, task) 49 | 50 | 51 | tasks_to_try = (Task('sleep', done=True), 52 | Task('wake', 'brian'), 53 | Task('breathe', 'BRIAN', True), 54 | Task('exercise', 'BrIaN', False)) 55 | 56 | 57 | @pytest.mark.parametrize('task', tasks_to_try) 58 | def test_add_4(tasks_db, task): 59 | """Slightly different take.""" 60 | task_id = tasks.add(task) 61 | t_from_db = tasks.get(task_id) 62 | assert equivalent(t_from_db, task) 63 | 64 | 65 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 66 | for t in tasks_to_try] 67 | 68 | 69 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 70 | def test_add_5(tasks_db, task): 71 | """Demonstrate ids.""" 72 | task_id = tasks.add(task) 73 | t_from_db = tasks.get(task_id) 74 | assert equivalent(t_from_db, task) 75 | 76 | 77 | @pytest.mark.parametrize('task', [ 78 | pytest.param(Task('create'), id='just summary'), 79 | pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 80 | pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 81 | def test_add_6(task): 82 | """Demonstrate pytest.param and id.""" 83 | task_id = tasks.add(task) 84 | t_from_db = tasks.get(task_id) 85 | assert equivalent(t_from_db, task) 86 | 87 | 88 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 89 | class TestAdd(): 90 | """Demonstrate paramterize and test classes.""" 91 | 92 | def test_equivalent(self, tasks_db, task): 93 | """Similar test, just within a class.""" 94 | task_id = tasks.add(task) 95 | t_from_db = tasks.get(task_id) 96 | assert equivalent(t_from_db, task) 97 | 98 | def test_valid_id(self, tasks_db, task): 99 | """We can use the same data for multiple tests.""" 100 | task_id = tasks.add(task) 101 | t_from_db = tasks.get(task_id) 102 | assert t_from_db.id == task_id 103 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/func/test_add_variety2.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | tasks_to_try = (Task('sleep', done=True), 8 | Task('wake', 'brian'), 9 | Task('breathe', 'BRIAN', True), 10 | Task('exercise', 'BrIaN', False)) 11 | 12 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 13 | for t in tasks_to_try] 14 | 15 | 16 | def equivalent(t1, t2): 17 | """Check two tasks for equivalence.""" 18 | return ((t1.summary == t2.summary) and 19 | (t1.owner == t2.owner) and 20 | (t1.done == t2.done)) 21 | 22 | 23 | @pytest.fixture(params=tasks_to_try) 24 | def a_task(request): 25 | """Using no ids.""" 26 | return request.param 27 | 28 | 29 | def test_add_a(tasks_db, a_task): 30 | """Using a_task fixture (no ids).""" 31 | task_id = tasks.add(a_task) 32 | t_from_db = tasks.get(task_id) 33 | assert equivalent(t_from_db, a_task) 34 | 35 | 36 | @pytest.fixture(params=tasks_to_try, ids=task_ids) 37 | def b_task(request): 38 | """Using a list of ids.""" 39 | return request.param 40 | 41 | 42 | def test_add_b(tasks_db, b_task): 43 | """Using b_task fixture, with ids.""" 44 | task_id = tasks.add(b_task) 45 | t_from_db = tasks.get(task_id) 46 | assert equivalent(t_from_db, b_task) 47 | 48 | 49 | def id_func(fixture_value): 50 | """A function for generating ids.""" 51 | t = fixture_value 52 | return 'Task({},{},{})'.format(t.summary, t.owner, t.done) 53 | 54 | 55 | @pytest.fixture(params=tasks_to_try, ids=id_func) 56 | def c_task(request): 57 | """Using a function (id_func) to generate ids.""" 58 | return request.param 59 | 60 | 61 | def test_add_c(tasks_db, c_task): 62 | """Use fixture with generated ids.""" 63 | task_id = tasks.add(c_task) 64 | t_from_db = tasks.get(task_id) 65 | assert equivalent(t_from_db, c_task) 66 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | import pytest 3 | import tasks 4 | from tasks import Task 5 | 6 | 7 | @pytest.mark.usefixtures("tasks_db") 8 | class TestAdd: 9 | """Tests related to tasks.add().""" 10 | 11 | def test_missing_summary(self): 12 | """Should raise an exception if summary missing.""" 13 | with pytest.raises(ValueError): 14 | tasks.add(Task(owner="bob")) 15 | 16 | def test_done_not_bool(self): 17 | """Should raise an exception if done is not a bool.""" 18 | with pytest.raises(ValueError): 19 | tasks.add(Task(summary="summary", done="True")) 20 | 21 | 22 | def test_add_raises(): 23 | """add() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.add(task="not a Task object") 26 | 27 | 28 | @pytest.mark.smoke 29 | def test_list_raises(): 30 | """list() should raise an exception with wrong type param.""" 31 | with pytest.raises(TypeError): 32 | tasks.list_tasks(owner=123) 33 | 34 | 35 | @pytest.mark.get 36 | @pytest.mark.smoke 37 | def test_get_raises(): 38 | """get() should raise an exception with wrong type param.""" 39 | with pytest.raises(TypeError): 40 | tasks.get(task_id="123") 41 | 42 | 43 | class TestUpdate: 44 | """Test expected exceptions with tasks.update().""" 45 | 46 | def test_bad_id(self): 47 | """A non-int id should raise an exception.""" 48 | with pytest.raises(TypeError): 49 | tasks.update(task_id={"dict instead": 1}, task=tasks.Task()) 50 | 51 | def test_bad_task(self): 52 | """A non-Task task should raise an exception.""" 53 | with pytest.raises(TypeError): 54 | tasks.update(task_id=1, task="not a task") 55 | 56 | 57 | def test_delete_raises(): 58 | """delete() should raise an exception with wrong type param.""" 59 | with pytest.raises(TypeError): 60 | tasks.delete(task_id=(1, 2, 3)) 61 | 62 | 63 | def test_start_tasks_db_raises(): 64 | """Make sure unsupported db raises an exception.""" 65 | with pytest.raises(ValueError) as excinfo: 66 | tasks.start_tasks_db("some/great/path", "mysql") 67 | exception_msg = excinfo.value.args[0] 68 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 69 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch5/b/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def tasks_db_session(tmpdir_factory, request): 10 | """Connect to db before tests, disconnect after.""" 11 | temp_dir = tmpdir_factory.mktemp("temp") 12 | tasks.start_tasks_db(str(temp_dir), "tiny") 13 | yield # this is where the testing happens 14 | tasks.stop_tasks_db() 15 | 16 | 17 | @pytest.fixture() 18 | def tasks_db(tasks_db_session): 19 | """An empty tasks db.""" 20 | tasks.delete_all() 21 | 22 | 23 | # Reminder of Task constructor interface 24 | # Task(summary=None, owner=None, done=False, id=None) 25 | # Don't set id, it's set by database 26 | # owner and done are optional 27 | 28 | 29 | @pytest.fixture() 30 | def tasks_just_a_few(): 31 | """All summaries and owners are unique.""" 32 | return ( 33 | Task("Write some code", "Brian", True), 34 | Task("Code review Brian's code", "Katie", False), 35 | Task("Fix what Brian did", "Michelle", False), 36 | ) 37 | 38 | 39 | @pytest.fixture() 40 | def tasks_mult_per_owner(): 41 | """Several owners with several tasks each.""" 42 | return ( 43 | Task("Make a cookie", "Raphael"), 44 | Task("Use an emoji", "Raphael"), 45 | Task("Move to Berlin", "Raphael"), 46 | Task("Create", "Michelle"), 47 | Task("Inspire", "Michelle"), 48 | Task("Encourage", "Michelle"), 49 | Task("Do a handstand", "Daniel"), 50 | Task("Write some books", "Daniel"), 51 | Task("Eat ice cream", "Daniel"), 52 | ) 53 | 54 | 55 | @pytest.fixture() 56 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 57 | """Connected db with 3 tasks, all unique.""" 58 | for t in tasks_just_a_few: 59 | tasks.add(t) 60 | 61 | 62 | @pytest.fixture() 63 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 64 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 65 | for t in tasks_mult_per_owner: 66 | tasks.add(t) 67 | 68 | 69 | def pytest_addoption(parser): 70 | """Turn nice features on with --nice option.""" 71 | group = parser.getgroup("nice") 72 | group.addoption( 73 | "--nice", action="store_true", help="nice: turn failures into opportunities" 74 | ) 75 | 76 | 77 | def pytest_report_header(config): 78 | """Thank tester for running tests.""" 79 | if config.getoption("nice"): 80 | return "Thanks for running the tests." 81 | 82 | 83 | def pytest_report_teststatus(report, config): 84 | """Turn failures into opportunities.""" 85 | if report.when == "call": 86 | if report.failed and config.getoption("nice"): 87 | return (report.outcome, "O", "OPPORTUNITY for improvement") 88 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task('do something') 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task('sit in chair', owner='me', done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task('throw a party')) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/func/test_add_variety.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_1(tasks_db): 9 | """tasks.get() using id returned from add() works.""" 10 | task = Task('breathe', 'BRIAN', True) 11 | task_id = tasks.add(task) 12 | t_from_db = tasks.get(task_id) 13 | # everything but the id should be the same 14 | assert equivalent(t_from_db, task) 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | # Compare everything but the id field 20 | return ((t1.summary == t2.summary) and 21 | (t1.owner == t2.owner) and 22 | (t1.done == t2.done)) 23 | 24 | 25 | @pytest.mark.parametrize('task', 26 | [Task('sleep', done=True), 27 | Task('wake', 'brian'), 28 | Task('breathe', 'BRIAN', True), 29 | Task('exercise', 'BrIaN', False)]) 30 | def test_add_2(tasks_db, task): 31 | """Demonstrate parametrize with one parameter.""" 32 | task_id = tasks.add(task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, task) 35 | 36 | 37 | @pytest.mark.parametrize('summary, owner, done', 38 | [('sleep', None, False), 39 | ('wake', 'brian', False), 40 | ('breathe', 'BRIAN', True), 41 | ('eat eggs', 'BrIaN', False), 42 | ]) 43 | def test_add_3(tasks_db, summary, owner, done): 44 | """Demonstrate parametrize with multiple parameters.""" 45 | task = Task(summary, owner, done) 46 | task_id = tasks.add(task) 47 | t_from_db = tasks.get(task_id) 48 | assert equivalent(t_from_db, task) 49 | 50 | 51 | tasks_to_try = (Task('sleep', done=True), 52 | Task('wake', 'brian'), 53 | Task('breathe', 'BRIAN', True), 54 | Task('exercise', 'BrIaN', False)) 55 | 56 | 57 | @pytest.mark.parametrize('task', tasks_to_try) 58 | def test_add_4(tasks_db, task): 59 | """Slightly different take.""" 60 | task_id = tasks.add(task) 61 | t_from_db = tasks.get(task_id) 62 | assert equivalent(t_from_db, task) 63 | 64 | 65 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 66 | for t in tasks_to_try] 67 | 68 | 69 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 70 | def test_add_5(tasks_db, task): 71 | """Demonstrate ids.""" 72 | task_id = tasks.add(task) 73 | t_from_db = tasks.get(task_id) 74 | assert equivalent(t_from_db, task) 75 | 76 | 77 | @pytest.mark.parametrize('task', [ 78 | pytest.param(Task('create'), id='just summary'), 79 | pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 80 | pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 81 | def test_add_6(task): 82 | """Demonstrate pytest.param and id.""" 83 | task_id = tasks.add(task) 84 | t_from_db = tasks.get(task_id) 85 | assert equivalent(t_from_db, task) 86 | 87 | 88 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 89 | class TestAdd(): 90 | """Demonstrate paramterize and test classes.""" 91 | 92 | def test_equivalent(self, tasks_db, task): 93 | """Similar test, just within a class.""" 94 | task_id = tasks.add(task) 95 | t_from_db = tasks.get(task_id) 96 | assert equivalent(t_from_db, task) 97 | 98 | def test_valid_id(self, tasks_db, task): 99 | """We can use the same data for multiple tests.""" 100 | task_id = tasks.add(task) 101 | t_from_db = tasks.get(task_id) 102 | assert t_from_db.id == task_id 103 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/func/test_add_variety2.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | tasks_to_try = (Task('sleep', done=True), 8 | Task('wake', 'brian'), 9 | Task('breathe', 'BRIAN', True), 10 | Task('exercise', 'BrIaN', False)) 11 | 12 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 13 | for t in tasks_to_try] 14 | 15 | 16 | def equivalent(t1, t2): 17 | """Check two tasks for equivalence.""" 18 | return ((t1.summary == t2.summary) and 19 | (t1.owner == t2.owner) and 20 | (t1.done == t2.done)) 21 | 22 | 23 | @pytest.fixture(params=tasks_to_try) 24 | def a_task(request): 25 | """Using no ids.""" 26 | return request.param 27 | 28 | 29 | def test_add_a(tasks_db, a_task): 30 | """Using a_task fixture (no ids).""" 31 | task_id = tasks.add(a_task) 32 | t_from_db = tasks.get(task_id) 33 | assert equivalent(t_from_db, a_task) 34 | 35 | 36 | @pytest.fixture(params=tasks_to_try, ids=task_ids) 37 | def b_task(request): 38 | """Using a list of ids.""" 39 | return request.param 40 | 41 | 42 | def test_add_b(tasks_db, b_task): 43 | """Using b_task fixture, with ids.""" 44 | task_id = tasks.add(b_task) 45 | t_from_db = tasks.get(task_id) 46 | assert equivalent(t_from_db, b_task) 47 | 48 | 49 | def id_func(fixture_value): 50 | """A function for generating ids.""" 51 | t = fixture_value 52 | return 'Task({},{},{})'.format(t.summary, t.owner, t.done) 53 | 54 | 55 | @pytest.fixture(params=tasks_to_try, ids=id_func) 56 | def c_task(request): 57 | """Using a function (id_func) to generate ids.""" 58 | return request.param 59 | 60 | 61 | def test_add_c(tasks_db, c_task): 62 | """Use fixture with generated ids.""" 63 | task_id = tasks.add(c_task) 64 | t_from_db = tasks.get(task_id) 65 | assert equivalent(t_from_db, c_task) 66 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | import pytest 3 | import tasks 4 | from tasks import Task 5 | 6 | 7 | @pytest.mark.usefixtures('tasks_db') 8 | class TestAdd(): 9 | """Tests related to tasks.add().""" 10 | 11 | def test_missing_summary(self): 12 | """Should raise an exception if summary missing.""" 13 | with pytest.raises(ValueError): 14 | tasks.add(Task(owner='bob')) 15 | 16 | def test_done_not_bool(self): 17 | """Should raise an exception if done is not a bool.""" 18 | with pytest.raises(ValueError): 19 | tasks.add(Task(summary='summary', done='True')) 20 | 21 | 22 | def test_add_raises(): 23 | """add() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.add(task='not a Task object') 26 | 27 | 28 | @pytest.mark.smoke 29 | def test_list_raises(): 30 | """list() should raise an exception with wrong type param.""" 31 | with pytest.raises(TypeError): 32 | tasks.list_tasks(owner=123) 33 | 34 | 35 | @pytest.mark.get 36 | @pytest.mark.smoke 37 | def test_get_raises(): 38 | """get() should raise an exception with wrong type param.""" 39 | with pytest.raises(TypeError): 40 | tasks.get(task_id='123') 41 | 42 | 43 | class TestUpdate(): 44 | """Test expected exceptions with tasks.update().""" 45 | 46 | def test_bad_id(self): 47 | """A non-int id should raise an excption.""" 48 | with pytest.raises(TypeError): 49 | tasks.update(task_id={'dict instead': 1}, 50 | task=tasks.Task()) 51 | 52 | def test_bad_task(self): 53 | """A non-Task task should raise an excption.""" 54 | with pytest.raises(TypeError): 55 | tasks.update(task_id=1, task='not a task') 56 | 57 | 58 | def test_delete_raises(): 59 | """delete() should raise an exception with wrong type param.""" 60 | with pytest.raises(TypeError): 61 | tasks.delete(task_id=(1, 2, 3)) 62 | 63 | 64 | def test_start_tasks_db_raises(): 65 | """Make sure unsupported db raises an exception.""" 66 | with pytest.raises(ValueError) as excinfo: 67 | tasks.start_tasks_db('some/great/path', 'mysql') 68 | exception_msg = excinfo.value.args[0] 69 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 70 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | [pytest] 10 | nice=True 11 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch5/c/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch5/pytest-nice/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The Pragmatic Programmers, LLC 2 | 3 | All rights reserved. 4 | 5 | Copyrights apply to this source code. 6 | 7 | You may use the source code in your own projects, however the source code may 8 | not be used to create commercial training material, courses, books, articles, and the 9 | like. We make no guarantees that this source code is fit for any purpose. 10 | -------------------------------------------------------------------------------- /ch5/pytest-nice/README.rst: -------------------------------------------------------------------------------- 1 | pytest-nice : A pytest plugin 2 | ============================= 3 | 4 | Makes pytest output just a bit nicer during failures. 5 | 6 | Features 7 | -------- 8 | 9 | - Includes user name of person running tests in pytest output. 10 | - Adds ``--nice`` option that: 11 | 12 | - turns ``F`` to ``O`` 13 | - with ``-v``, turns ``FAILURE`` to ``OPPORTUNITY for improvement`` 14 | 15 | Installation 16 | ------------ 17 | 18 | Given that our pytest plugins are being saved in .tar.gz form in the shared directory PATH, then install like this: 19 | 20 | :: 21 | 22 | $ pip install PATH/pytest-nice-0.1.0.tar.gz 23 | $ pip install --no-index --find-links PATH pytest-nice 24 | 25 | Usage 26 | ----- 27 | 28 | :: 29 | 30 | $ pytest --nice -------------------------------------------------------------------------------- /ch5/pytest-nice/pytest_nice.py: -------------------------------------------------------------------------------- 1 | """Code for pytest-nice plugin.""" 2 | 3 | import pytest 4 | 5 | 6 | def pytest_addoption(parser): 7 | """Turn nice features on with --nice option.""" 8 | group = parser.getgroup("nice") 9 | group.addoption( 10 | "--nice", 11 | action="store_true", 12 | help="nice: turn FAILED into OPPORTUNITY for improvement", 13 | ) 14 | 15 | 16 | def pytest_report_header(config): 17 | """Thank tester for running tests.""" 18 | if config.getoption("nice"): 19 | return "Thanks for running the tests." 20 | 21 | 22 | def pytest_report_teststatus(report, config): 23 | """Turn failures into opportunities.""" 24 | if report.when == "call": 25 | if report.failed and config.getoption("nice"): 26 | return (report.outcome, "O", "OPPORTUNITY for improvement") 27 | -------------------------------------------------------------------------------- /ch5/pytest-nice/setup.py: -------------------------------------------------------------------------------- 1 | """Setup for pytest-nice plugin.""" 2 | 3 | from setuptools import setup 4 | 5 | # Minimal setup() - required fields 6 | setup( 7 | name="pytest-nice", 8 | version="0.1.0", 9 | description="A pytest plugin to turn FAILURE into OPPORTUNITY", 10 | url="https://wherever/you/have/info/on/this/package", 11 | author="Your Name", # or maintainer 12 | author_email="your_email@somewhere.com", # or maintainer_email 13 | license="proprietary", 14 | py_modules=["pytest_nice"], # Module(s) for this plugin 15 | install_requires=["pytest"], 16 | # pytest11 is a special identifier that pytest looks for 17 | # nice is the name of our plugin 18 | # pytest_nice is the name of the module where our plugin lives 19 | entry_points={"pytest11": ["nice = pytest_nice",],}, 20 | ) 21 | -------------------------------------------------------------------------------- /ch5/pytest-nice/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """pytester is needed for testing plugins.""" 2 | pytest_plugins = "pytester" 3 | -------------------------------------------------------------------------------- /ch5/pytest-nice/tests/test_nice.py: -------------------------------------------------------------------------------- 1 | """Testing the pytest-nice plugin.""" 2 | 3 | import pytest 4 | 5 | 6 | def test_pass_fail(testdir): 7 | 8 | # Create a temporary pytest test module. 9 | testdir.makepyfile( 10 | """ 11 | def test_pass(): 12 | assert 1 == 1 13 | 14 | def test_fail(): 15 | assert 1 == 2 16 | """ 17 | ) 18 | 19 | # Run pytest. Return value is of type RunResult. 20 | result = testdir.runpytest() 21 | 22 | # fnmatch_lines does an assertion internally. Strings can include glob wildcards. 23 | result.stdout.fnmatch_lines( 24 | ["*.F*",] # . for Pass, F for Fail 25 | ) 26 | 27 | # Make sure that that we get a '1' exit code for the testsuite. 28 | assert result.ret == 1 29 | 30 | 31 | @pytest.fixture() 32 | def sample_test(testdir): 33 | testdir.makepyfile( 34 | """ 35 | def test_pass(): 36 | assert 1 == 1 37 | 38 | def test_fail(): 39 | assert 1 == 2 40 | """ 41 | ) 42 | return testdir 43 | 44 | 45 | def test_with_nice(sample_test): 46 | result = sample_test.runpytest("--nice") 47 | result.stdout.fnmatch_lines( 48 | ["*.O*",] 49 | ) # . for Pass, O for Fail 50 | assert result.ret == 1 51 | 52 | 53 | def test_with_nice_verbose(sample_test): 54 | result = sample_test.runpytest("-v", "--nice") 55 | result.stdout.fnmatch_lines( 56 | ["*::test_fail OPPORTUNITY for improvement*",] 57 | ) 58 | assert result.ret == 1 59 | 60 | 61 | def test_not_nice_verbose(sample_test): 62 | result = sample_test.runpytest("-v") 63 | result.stdout.fnmatch_lines(["*::test_fail FAILED*"]) 64 | assert result.ret == 1 65 | 66 | 67 | def test_header(sample_test): 68 | result = sample_test.runpytest("--nice") 69 | result.stdout.fnmatch_lines(["Thanks for running the tests."]) 70 | 71 | 72 | def test_header_not_nice(sample_test): 73 | result = sample_test.runpytest() 74 | thanks_message = "Thanks for running the tests." 75 | assert thanks_message not in result.stdout.str() 76 | 77 | 78 | def test_help_message(testdir): 79 | result = testdir.runpytest("--help") 80 | 81 | # fnmatch_lines does an assertion internally 82 | result.stdout.fnmatch_lines( 83 | ["nice:", "*--nice*nice: turn FAILED into OPPORTUNITY for improvement",] 84 | ) 85 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.fixture(scope='session') 9 | def tasks_db_session(tmpdir_factory, request): 10 | """Connect to db before tests, disconnect after.""" 11 | temp_dir = tmpdir_factory.mktemp('temp') 12 | tasks.start_tasks_db(str(temp_dir), 'tiny') 13 | yield # this is where the testing happens 14 | tasks.stop_tasks_db() 15 | 16 | 17 | @pytest.fixture() 18 | def tasks_db(tasks_db_session): 19 | """An empty tasks db.""" 20 | tasks.delete_all() 21 | 22 | 23 | # Reminder of Task constructor interface 24 | # Task(summary=None, owner=None, done=False, id=None) 25 | # Don't set id, it's set by database 26 | # owner and done are optional 27 | 28 | 29 | @pytest.fixture() 30 | def tasks_just_a_few(): 31 | """All summaries and owners are unique.""" 32 | return ( 33 | Task('Write some code', 'Brian', True), 34 | Task("Code review Brian's code", 'Katie', False), 35 | Task('Fix what Brian did', 'Michelle', False)) 36 | 37 | 38 | @pytest.fixture() 39 | def tasks_mult_per_owner(): 40 | """Several owners with several tasks each.""" 41 | return ( 42 | Task('Make a cookie', 'Raphael'), 43 | Task('Use an emoji', 'Raphael'), 44 | Task('Move to Berlin', 'Raphael'), 45 | 46 | Task('Create', 'Michelle'), 47 | Task('Inspire', 'Michelle'), 48 | Task('Encourage', 'Michelle'), 49 | 50 | Task('Do a handstand', 'Daniel'), 51 | Task('Write some books', 'Daniel'), 52 | Task('Eat ice cream', 'Daniel')) 53 | 54 | 55 | @pytest.fixture() 56 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 57 | """Connected db with 3 tasks, all unique.""" 58 | for t in tasks_just_a_few: 59 | tasks.add(t) 60 | 61 | 62 | @pytest.fixture() 63 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 64 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 65 | for t in tasks_mult_per_owner: 66 | tasks.add(t) 67 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task('do something') 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task('sit in chair', owner='me', done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task('throw a party')) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/func/test_add_variety.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_1(tasks_db): 9 | """tasks.get() using id returned from add() works.""" 10 | task = Task('breathe', 'BRIAN', True) 11 | task_id = tasks.add(task) 12 | t_from_db = tasks.get(task_id) 13 | # everything but the id should be the same 14 | assert equivalent(t_from_db, task) 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | # Compare everything but the id field 20 | return ((t1.summary == t2.summary) and 21 | (t1.owner == t2.owner) and 22 | (t1.done == t2.done)) 23 | 24 | 25 | @pytest.mark.parametrize('task', 26 | [Task('sleep', done=True), 27 | Task('wake', 'brian'), 28 | Task('breathe', 'BRIAN', True), 29 | Task('exercise', 'BrIaN', False)]) 30 | def test_add_2(tasks_db, task): 31 | """Demonstrate parametrize with one parameter.""" 32 | task_id = tasks.add(task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, task) 35 | 36 | 37 | @pytest.mark.parametrize('summary, owner, done', 38 | [('sleep', None, False), 39 | ('wake', 'brian', False), 40 | ('breathe', 'BRIAN', True), 41 | ('eat eggs', 'BrIaN', False), 42 | ]) 43 | def test_add_3(tasks_db, summary, owner, done): 44 | """Demonstrate parametrize with multiple parameters.""" 45 | task = Task(summary, owner, done) 46 | task_id = tasks.add(task) 47 | t_from_db = tasks.get(task_id) 48 | assert equivalent(t_from_db, task) 49 | 50 | 51 | tasks_to_try = (Task('sleep', done=True), 52 | Task('wake', 'brian'), 53 | Task('breathe', 'BRIAN', True), 54 | Task('exercise', 'BrIaN', False)) 55 | 56 | 57 | @pytest.mark.parametrize('task', tasks_to_try) 58 | def test_add_4(tasks_db, task): 59 | """Slightly different take.""" 60 | task_id = tasks.add(task) 61 | t_from_db = tasks.get(task_id) 62 | assert equivalent(t_from_db, task) 63 | 64 | 65 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 66 | for t in tasks_to_try] 67 | 68 | 69 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 70 | def test_add_5(tasks_db, task): 71 | """Demonstrate ids.""" 72 | task_id = tasks.add(task) 73 | t_from_db = tasks.get(task_id) 74 | assert equivalent(t_from_db, task) 75 | 76 | 77 | @pytest.mark.parametrize('task', [ 78 | pytest.param(Task('create'), id='just summary'), 79 | pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 80 | pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 81 | def test_add_6(task): 82 | """Demonstrate pytest.param and id.""" 83 | task_id = tasks.add(task) 84 | t_from_db = tasks.get(task_id) 85 | assert equivalent(t_from_db, task) 86 | 87 | 88 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 89 | class TestAdd(): 90 | """Demonstrate paramterize and test classes.""" 91 | 92 | def test_equivalent(self, tasks_db, task): 93 | """Similar test, just within a class.""" 94 | task_id = tasks.add(task) 95 | t_from_db = tasks.get(task_id) 96 | assert equivalent(t_from_db, task) 97 | 98 | def test_valid_id(self, tasks_db, task): 99 | """We can use the same data for multiple tests.""" 100 | task_id = tasks.add(task) 101 | t_from_db = tasks.get(task_id) 102 | assert t_from_db.id == task_id 103 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/func/test_add_variety2.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | tasks_to_try = (Task('sleep', done=True), 8 | Task('wake', 'brian'), 9 | Task('breathe', 'BRIAN', True), 10 | Task('exercise', 'BrIaN', False)) 11 | 12 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 13 | for t in tasks_to_try] 14 | 15 | 16 | def equivalent(t1, t2): 17 | """Check two tasks for equivalence.""" 18 | return ((t1.summary == t2.summary) and 19 | (t1.owner == t2.owner) and 20 | (t1.done == t2.done)) 21 | 22 | 23 | @pytest.fixture(params=tasks_to_try) 24 | def a_task(request): 25 | """Using no ids.""" 26 | return request.param 27 | 28 | 29 | def test_add_a(tasks_db, a_task): 30 | """Using a_task fixture (no ids).""" 31 | task_id = tasks.add(a_task) 32 | t_from_db = tasks.get(task_id) 33 | assert equivalent(t_from_db, a_task) 34 | 35 | 36 | @pytest.fixture(params=tasks_to_try, ids=task_ids) 37 | def b_task(request): 38 | """Using a list of ids.""" 39 | return request.param 40 | 41 | 42 | def test_add_b(tasks_db, b_task): 43 | """Using b_task fixture, with ids.""" 44 | task_id = tasks.add(b_task) 45 | t_from_db = tasks.get(task_id) 46 | assert equivalent(t_from_db, b_task) 47 | 48 | 49 | def id_func(fixture_value): 50 | """A function for generating ids.""" 51 | t = fixture_value 52 | return 'Task({},{},{})'.format(t.summary, t.owner, t.done) 53 | 54 | 55 | @pytest.fixture(params=tasks_to_try, ids=id_func) 56 | def c_task(request): 57 | """Using a function (id_func) to generate ids.""" 58 | return request.param 59 | 60 | 61 | def test_add_c(tasks_db, c_task): 62 | """Use fixture with generated ids.""" 63 | task_id = tasks.add(c_task) 64 | t_from_db = tasks.get(task_id) 65 | assert equivalent(t_from_db, c_task) 66 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | import pytest 3 | import tasks 4 | from tasks import Task 5 | 6 | 7 | @pytest.mark.usefixtures('tasks_db') 8 | class TestAdd(): 9 | """Tests related to tasks.add().""" 10 | 11 | def test_missing_summary(self): 12 | """Should raise an exception if summary missing.""" 13 | with pytest.raises(ValueError): 14 | tasks.add(Task(owner='bob')) 15 | 16 | def test_done_not_bool(self): 17 | """Should raise an exception if done is not a bool.""" 18 | with pytest.raises(ValueError): 19 | tasks.add(Task(summary='summary', done='True')) 20 | 21 | 22 | def test_add_raises(): 23 | """add() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.add(task='not a Task object') 26 | 27 | 28 | @pytest.mark.smoke 29 | def test_list_raises(): 30 | """list() should raise an exception with wrong type param.""" 31 | with pytest.raises(TypeError): 32 | tasks.list_tasks(owner=123) 33 | 34 | 35 | @pytest.mark.get 36 | @pytest.mark.smoke 37 | def test_get_raises(): 38 | """get() should raise an exception with wrong type param.""" 39 | with pytest.raises(TypeError): 40 | tasks.get(task_id='123') 41 | 42 | 43 | class TestUpdate(): 44 | """Test expected exceptions with tasks.update().""" 45 | 46 | def test_bad_id(self): 47 | """A non-int id should raise an excption.""" 48 | with pytest.raises(TypeError): 49 | tasks.update(task_id={'dict instead': 1}, 50 | task=tasks.Task()) 51 | 52 | def test_bad_task(self): 53 | """A non-Task task should raise an excption.""" 54 | with pytest.raises(TypeError): 55 | tasks.update(task_id=1, task='not a task') 56 | 57 | 58 | def test_delete_raises(): 59 | """delete() should raise an exception with wrong type param.""" 60 | with pytest.raises(TypeError): 61 | tasks.delete(task_id=(1, 2, 3)) 62 | 63 | 64 | def test_start_tasks_db_raises(): 65 | """Make sure unsupported db raises an exception.""" 66 | with pytest.raises(ValueError) as excinfo: 67 | tasks.start_tasks_db('some/great/path', 'mysql') 68 | exception_msg = excinfo.value.args[0] 69 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 70 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch6/a/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.fixture(scope='session') 9 | def tasks_db_session(tmpdir_factory, request): 10 | """Connect to db before tests, disconnect after.""" 11 | temp_dir = tmpdir_factory.mktemp('temp') 12 | tasks.start_tasks_db(str(temp_dir), 'tiny') 13 | yield # this is where the testing happens 14 | tasks.stop_tasks_db() 15 | 16 | 17 | @pytest.fixture() 18 | def tasks_db(tasks_db_session): 19 | """An empty tasks db.""" 20 | tasks.delete_all() 21 | 22 | 23 | # Reminder of Task constructor interface 24 | # Task(summary=None, owner=None, done=False, id=None) 25 | # Don't set id, it's set by database 26 | # owner and done are optional 27 | 28 | 29 | @pytest.fixture() 30 | def tasks_just_a_few(): 31 | """All summaries and owners are unique.""" 32 | return ( 33 | Task('Write some code', 'Brian', True), 34 | Task("Code review Brian's code", 'Katie', False), 35 | Task('Fix what Brian did', 'Michelle', False)) 36 | 37 | 38 | @pytest.fixture() 39 | def tasks_mult_per_owner(): 40 | """Several owners with several tasks each.""" 41 | return ( 42 | Task('Make a cookie', 'Raphael'), 43 | Task('Use an emoji', 'Raphael'), 44 | Task('Move to Berlin', 'Raphael'), 45 | 46 | Task('Create', 'Michelle'), 47 | Task('Inspire', 'Michelle'), 48 | Task('Encourage', 'Michelle'), 49 | 50 | Task('Do a handstand', 'Daniel'), 51 | Task('Write some books', 'Daniel'), 52 | Task('Eat ice cream', 'Daniel')) 53 | 54 | 55 | @pytest.fixture() 56 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 57 | """Connected db with 3 tasks, all unique.""" 58 | for t in tasks_just_a_few: 59 | tasks.add(t) 60 | 61 | 62 | @pytest.fixture() 63 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 64 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 65 | for t in tasks_mult_per_owner: 66 | tasks.add(t) 67 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task('do something') 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task('sit in chair', owner='me', done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task('throw a party')) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/func/test_add_variety.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_1(tasks_db): 9 | """tasks.get() using id returned from add() works.""" 10 | task = Task('breathe', 'BRIAN', True) 11 | task_id = tasks.add(task) 12 | t_from_db = tasks.get(task_id) 13 | # everything but the id should be the same 14 | assert equivalent(t_from_db, task) 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | # Compare everything but the id field 20 | return ((t1.summary == t2.summary) and 21 | (t1.owner == t2.owner) and 22 | (t1.done == t2.done)) 23 | 24 | 25 | @pytest.mark.parametrize('task', 26 | [Task('sleep', done=True), 27 | Task('wake', 'brian'), 28 | Task('breathe', 'BRIAN', True), 29 | Task('exercise', 'BrIaN', False)]) 30 | def test_add_2(tasks_db, task): 31 | """Demonstrate parametrize with one parameter.""" 32 | task_id = tasks.add(task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, task) 35 | 36 | 37 | @pytest.mark.parametrize('summary, owner, done', 38 | [('sleep', None, False), 39 | ('wake', 'brian', False), 40 | ('breathe', 'BRIAN', True), 41 | ('eat eggs', 'BrIaN', False), 42 | ]) 43 | def test_add_3(tasks_db, summary, owner, done): 44 | """Demonstrate parametrize with multiple parameters.""" 45 | task = Task(summary, owner, done) 46 | task_id = tasks.add(task) 47 | t_from_db = tasks.get(task_id) 48 | assert equivalent(t_from_db, task) 49 | 50 | 51 | tasks_to_try = (Task('sleep', done=True), 52 | Task('wake', 'brian'), 53 | Task('breathe', 'BRIAN', True), 54 | Task('exercise', 'BrIaN', False)) 55 | 56 | 57 | @pytest.mark.parametrize('task', tasks_to_try) 58 | def test_add_4(tasks_db, task): 59 | """Slightly different take.""" 60 | task_id = tasks.add(task) 61 | t_from_db = tasks.get(task_id) 62 | assert equivalent(t_from_db, task) 63 | 64 | 65 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 66 | for t in tasks_to_try] 67 | 68 | 69 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 70 | def test_add_5(tasks_db, task): 71 | """Demonstrate ids.""" 72 | task_id = tasks.add(task) 73 | t_from_db = tasks.get(task_id) 74 | assert equivalent(t_from_db, task) 75 | 76 | 77 | @pytest.mark.parametrize('task', [ 78 | pytest.param(Task('create'), id='just summary'), 79 | pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 80 | pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 81 | def test_add_6(task): 82 | """Demonstrate pytest.param and id.""" 83 | task_id = tasks.add(task) 84 | t_from_db = tasks.get(task_id) 85 | assert equivalent(t_from_db, task) 86 | 87 | 88 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 89 | class TestAdd(): 90 | """Demonstrate paramterize and test classes.""" 91 | 92 | def test_equivalent(self, tasks_db, task): 93 | """Similar test, just within a class.""" 94 | task_id = tasks.add(task) 95 | t_from_db = tasks.get(task_id) 96 | assert equivalent(t_from_db, task) 97 | 98 | def test_valid_id(self, tasks_db, task): 99 | """We can use the same data for multiple tests.""" 100 | task_id = tasks.add(task) 101 | t_from_db = tasks.get(task_id) 102 | assert t_from_db.id == task_id 103 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/func/test_add_variety2.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | tasks_to_try = (Task('sleep', done=True), 8 | Task('wake', 'brian'), 9 | Task('breathe', 'BRIAN', True), 10 | Task('exercise', 'BrIaN', False)) 11 | 12 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 13 | for t in tasks_to_try] 14 | 15 | 16 | def equivalent(t1, t2): 17 | """Check two tasks for equivalence.""" 18 | return ((t1.summary == t2.summary) and 19 | (t1.owner == t2.owner) and 20 | (t1.done == t2.done)) 21 | 22 | 23 | @pytest.fixture(params=tasks_to_try) 24 | def a_task(request): 25 | """Using no ids.""" 26 | return request.param 27 | 28 | 29 | def test_add_a(tasks_db, a_task): 30 | """Using a_task fixture (no ids).""" 31 | task_id = tasks.add(a_task) 32 | t_from_db = tasks.get(task_id) 33 | assert equivalent(t_from_db, a_task) 34 | 35 | 36 | @pytest.fixture(params=tasks_to_try, ids=task_ids) 37 | def b_task(request): 38 | """Using a list of ids.""" 39 | return request.param 40 | 41 | 42 | def test_add_b(tasks_db, b_task): 43 | """Using b_task fixture, with ids.""" 44 | task_id = tasks.add(b_task) 45 | t_from_db = tasks.get(task_id) 46 | assert equivalent(t_from_db, b_task) 47 | 48 | 49 | def id_func(fixture_value): 50 | """A function for generating ids.""" 51 | t = fixture_value 52 | return 'Task({},{},{})'.format(t.summary, t.owner, t.done) 53 | 54 | 55 | @pytest.fixture(params=tasks_to_try, ids=id_func) 56 | def c_task(request): 57 | """Using a function (id_func) to generate ids.""" 58 | return request.param 59 | 60 | 61 | def test_add_c(tasks_db, c_task): 62 | """Use fixture with generated ids.""" 63 | task_id = tasks.add(c_task) 64 | t_from_db = tasks.get(task_id) 65 | assert equivalent(t_from_db, c_task) 66 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | import pytest 3 | import tasks 4 | from tasks import Task 5 | 6 | 7 | @pytest.mark.usefixtures('tasks_db') 8 | class TestAdd(): 9 | """Tests related to tasks.add().""" 10 | 11 | def test_missing_summary(self): 12 | """Should raise an exception if summary missing.""" 13 | with pytest.raises(ValueError): 14 | tasks.add(Task(owner='bob')) 15 | 16 | def test_done_not_bool(self): 17 | """Should raise an exception if done is not a bool.""" 18 | with pytest.raises(ValueError): 19 | tasks.add(Task(summary='summary', done='True')) 20 | 21 | 22 | def test_add_raises(): 23 | """add() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.add(task='not a Task object') 26 | 27 | 28 | @pytest.mark.smoke 29 | def test_list_raises(): 30 | """list() should raise an exception with wrong type param.""" 31 | with pytest.raises(TypeError): 32 | tasks.list_tasks(owner=123) 33 | 34 | 35 | @pytest.mark.get 36 | @pytest.mark.smoke 37 | def test_get_raises(): 38 | """get() should raise an exception with wrong type param.""" 39 | with pytest.raises(TypeError): 40 | tasks.get(task_id='123') 41 | 42 | 43 | class TestUpdate(): 44 | """Test expected exceptions with tasks.update().""" 45 | 46 | def test_bad_id(self): 47 | """A non-int id should raise an excption.""" 48 | with pytest.raises(TypeError): 49 | tasks.update(task_id={'dict instead': 1}, 50 | task=tasks.Task()) 51 | 52 | def test_bad_task(self): 53 | """A non-Task task should raise an excption.""" 54 | with pytest.raises(TypeError): 55 | tasks.update(task_id=1, task='not a task') 56 | 57 | 58 | def test_delete_raises(): 59 | """delete() should raise an exception with wrong type param.""" 60 | with pytest.raises(TypeError): 61 | tasks.delete(task_id=(1, 2, 3)) 62 | 63 | 64 | def test_start_tasks_db_raises(): 65 | """Make sure unsupported db raises an exception.""" 66 | with pytest.raises(ValueError) as excinfo: 67 | tasks.start_tasks_db('some/great/path', 'mysql') 68 | exception_msg = excinfo.value.args[0] 69 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 70 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | [pytest] 10 | addopts = -rsxX -l --tb=short --strict 11 | markers = 12 | smoke: Run the smoke test test functions 13 | get: Run the test functions that test tasks.get() 14 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch6/b/tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch6/dups/a/test_foo.py: -------------------------------------------------------------------------------- 1 | def test_a(): 2 | pass 3 | -------------------------------------------------------------------------------- /ch6/dups/b/test_foo.py: -------------------------------------------------------------------------------- 1 | def test_b(): 2 | pass 3 | -------------------------------------------------------------------------------- /ch6/dups_fixed/a/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jashburn8020/python-testing-with-pytest/eca162766f3eb25c79778d64f993e3162c359674/ch6/dups_fixed/a/__init__.py -------------------------------------------------------------------------------- /ch6/dups_fixed/a/test_foo.py: -------------------------------------------------------------------------------- 1 | def test_a(): 2 | pass 3 | -------------------------------------------------------------------------------- /ch6/dups_fixed/b/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jashburn8020/python-testing-with-pytest/eca162766f3eb25c79778d64f993e3162c359674/ch6/dups_fixed/b/__init__.py -------------------------------------------------------------------------------- /ch6/dups_fixed/b/test_foo.py: -------------------------------------------------------------------------------- 1 | def test_b(): 2 | pass 3 | -------------------------------------------------------------------------------- /ch6/format/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | [pytest] 10 | addopts = -rsxX -l --tb=short --strict 11 | xfail_strict = true 12 | ;... more options ... 13 | -------------------------------------------------------------------------------- /ch6/format/setup.cfg: -------------------------------------------------------------------------------- 1 | ;... packaging specific stuff ... 2 | 3 | [tool:pytest] 4 | addopts = -rsxX -l --tb=short --strict 5 | xfail_strict = true 6 | ;... more options ... 7 | -------------------------------------------------------------------------------- /ch6/format/tox.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | ;... tox specific stuff ... 10 | 11 | [pytest] 12 | addopts = -rsxX -l --tb=short --strict 13 | xfail_strict = true 14 | ;... more options ... 15 | -------------------------------------------------------------------------------- /ch7/jenkins/run_tests.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # your paths will be different 4 | top_path=/Users/okken/projects/book/bopytest/Book 5 | code_path=${top_path}/code 6 | venv_path=${top_path}/venv 7 | 8 | tasks_proj_dir=${code_path}/$1 9 | start_tests_dir=${code_path}/$2 10 | results_dir=$3 11 | 12 | # click and Python 3, 13 | # from http://click.pocoo.org/5/python3/ 14 | export LC_ALL=en_US.utf-8 15 | export LANG=en_US.utf-8 16 | 17 | # virtual environment 18 | source ${venv_path}/bin/activate 19 | 20 | # install project 21 | pip install -e ${tasks_proj_dir} 22 | 23 | # run tests 24 | cd ${start_tests_dir} 25 | pytest --junit-xml=${results_dir}/results.xml 26 | -------------------------------------------------------------------------------- /ch7/jenkins/run_tests_cov.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # your paths will be different 4 | top_path=/Users/okken/projects/book/bopytest/Book 5 | code_path=${top_path}/code 6 | venv_path=${top_path}/venv 7 | 8 | tasks_proj_dir=${code_path}/$1 9 | start_tests_dir=${code_path}/$2 10 | results_dir=$3 11 | 12 | # click and Python 3, 13 | # from http://click.pocoo.org/5/python3/ 14 | export LC_ALL=en_US.utf-8 15 | export LANG=en_US.utf-8 16 | 17 | # virtual environment 18 | source ${venv_path}/bin/activate 19 | 20 | # install project 21 | pip install -e ${tasks_proj_dir} 22 | 23 | # run tests 24 | cd ${start_tests_dir} 25 | pytest --junit-xml=${results_dir}/results.xml \ 26 | --cov-report term-missing \ 27 | --cov-report xml --cov=${tasks_proj_dir}/src \ 28 | 29 | # copy coverage reports 30 | cp *.xml ${results_dir} 31 | 32 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The Pragmatic Programmers, LLC 2 | 3 | All rights reserved. 4 | 5 | Copyrights apply to this source code. 6 | 7 | You may use the source code in your own projects, however the source code may 8 | not be used to create commercial training material, courses, books, articles, and the 9 | like. We make no guarantees that this source code is fit for any purpose. 10 | 11 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | 3 | # Tests 4 | recursive-include tests *.py 5 | 6 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/setup.py: -------------------------------------------------------------------------------- 1 | """Minimal setup file for tasks project.""" 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name="tasks", 7 | version="0.1.1", 8 | license="proprietary", 9 | description="Minimal Project Task Management", 10 | author="Brian Okken", 11 | author_email="Please use pythontesting.net contact form.", 12 | url="https://pragprog.com/book/bopytest", 13 | packages=find_packages(where="src"), 14 | package_dir={"": "src"}, 15 | install_requires=["click", "tinydb", "six", "pytest", "pytest-mock"], 16 | tests_require=["pytest", "pytest-mock"], 17 | extras_require={"mongo": "pymongo"}, 18 | entry_points={"console_scripts": ["tasks = tasks.cli:tasks_cli",]}, 19 | ) 20 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/src/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | """Minimal Project Task Management.""" 2 | 3 | from .api import ( # noqa: F401 4 | Task, 5 | TasksException, 6 | add, 7 | get, 8 | list_tasks, 9 | count, 10 | update, 11 | delete, 12 | delete_all, 13 | unique_id, 14 | start_tasks_db, 15 | stop_tasks_db 16 | ) 17 | 18 | __version__ = '0.1.1' 19 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/src/tasks/cli.py: -------------------------------------------------------------------------------- 1 | """Command Line Interface (CLI) for tasks project.""" 2 | 3 | from __future__ import print_function 4 | from contextlib import contextmanager 5 | import click 6 | import tasks.config 7 | from tasks.api import Task 8 | 9 | 10 | # The main entry point for tasks. 11 | @click.group(context_settings={"help_option_names": ["-h", "--help"]}) 12 | @click.version_option(version="0.1.1") 13 | def tasks_cli(): 14 | """Run the tasks application.""" 15 | pass 16 | 17 | 18 | @tasks_cli.command(help="add a task") 19 | @click.argument("summary") 20 | @click.option("-o", "--owner", default=None, help="set the task owner") 21 | def add(summary, owner): 22 | """Add a task to db.""" 23 | with _tasks_db(): 24 | tasks.add(Task(summary, owner)) 25 | 26 | 27 | @tasks_cli.command(help="delete a task") 28 | @click.argument("task_id", type=int) 29 | def delete(task_id): 30 | """Remove task in db with given id.""" 31 | with _tasks_db(): 32 | tasks.delete(task_id) 33 | 34 | 35 | @tasks_cli.command(name="list", help="list tasks") 36 | @click.option("-o", "--owner", default=None, help="list tasks with this owner") 37 | def list_tasks(owner): 38 | """ 39 | List tasks in db. 40 | 41 | If owner given, only list tasks with that owner. 42 | """ 43 | formatstr = "{: >4} {: >10} {: >5} {}" 44 | print(formatstr.format("ID", "owner", "done", "summary")) 45 | print(formatstr.format("--", "-----", "----", "-------")) 46 | with _tasks_db(): 47 | for t in tasks.list_tasks(owner): 48 | done = "True" if t.done else "False" 49 | owner = "" if t.owner is None else t.owner 50 | print(formatstr.format(t.id, owner, done, t.summary)) 51 | 52 | 53 | @tasks_cli.command(help="update task") 54 | @click.argument("task_id", type=int) 55 | @click.option("-o", "--owner", default=None, help="change the task owner") 56 | @click.option("-s", "--summary", default=None, help="change the task summary") 57 | @click.option( 58 | "-d", 59 | "--done", 60 | default=None, 61 | type=bool, 62 | help="change the task done state (True or False)", 63 | ) 64 | def update(task_id, owner, summary, done): 65 | """Modify a task in db with given id with new info.""" 66 | with _tasks_db(): 67 | tasks.update(task_id, Task(summary, owner, done)) 68 | 69 | 70 | @tasks_cli.command(help="list count") 71 | def count(): 72 | """Return number of tasks in db.""" 73 | with _tasks_db(): 74 | c = tasks.count() 75 | print(c) 76 | 77 | 78 | @contextmanager 79 | def _tasks_db(): 80 | config = tasks.config.get_config() 81 | tasks.start_tasks_db(config.db_path, config.db_type) 82 | yield 83 | tasks.stop_tasks_db() 84 | 85 | 86 | if __name__ == "__main__": 87 | tasks_cli() 88 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/src/tasks/config.py: -------------------------------------------------------------------------------- 1 | """Handle configuration files for tasks CLI.""" 2 | 3 | from collections import namedtuple 4 | try: 5 | from configparser import ConfigParser 6 | except ImportError: 7 | from ConfigParser import ConfigParser 8 | 9 | import os 10 | 11 | TasksConfig = namedtuple('TasksConfig', ['db_path', 'db_type']) 12 | 13 | 14 | def get_config(): 15 | """Return TasksConfig object after reading config file.""" 16 | parser = ConfigParser() 17 | config_file = os.path.expanduser('~/.tasks.config') 18 | if not os.path.exists(config_file): 19 | tasks_db_path = '~/tasks_db/' 20 | tasks_db_type = 'tiny' 21 | else: 22 | parser.read(config_file) 23 | tasks_db_path = parser.get('TASKS', 'tasks_db_path') 24 | tasks_db_type = parser.get('TASKS', 'tasks_db_type') 25 | tasks_db_path = os.path.expanduser(tasks_db_path) 26 | return TasksConfig(tasks_db_path, tasks_db_type) 27 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/src/tasks/tasksdb_tinydb.py: -------------------------------------------------------------------------------- 1 | """Database wrapper for TinyDB for tasks project.""" 2 | import tinydb 3 | 4 | 5 | class TasksDB_TinyDB(): # noqa : E801 6 | """Wrapper class for TinyDB. 7 | 8 | The methods in this class need to match 9 | all database interaction classes. 10 | 11 | So far, this is: 12 | TasksDB_MongoDB found in tasksdb_pymongo.py. 13 | TasksDB_TinyDB found in tasksdb_tinydb.py. 14 | """ 15 | 16 | def __init__(self, db_path): # type (str) -> () 17 | """Connect to db.""" 18 | self._db = tinydb.TinyDB(db_path + '/tasks_db.json') 19 | 20 | def add(self, task): # type (dict) -> int 21 | """Add a task dict to db.""" 22 | task_id = self._db.insert(task) 23 | task['id'] = task_id 24 | self._db.update(task, doc_ids=[task_id]) 25 | return task_id 26 | 27 | def get(self, task_id): # type (int) -> dict 28 | """Return a task dict with matching id.""" 29 | return self._db.get(doc_id=task_id) 30 | 31 | def list_tasks(self, owner=None): # type (str) -> list[dict] 32 | """Return list of tasks.""" 33 | if owner is None: 34 | return self._db.all() 35 | else: 36 | return self._db.search(tinydb.Query().owner == owner) 37 | 38 | def count(self): # type () -> int 39 | """Return number of tasks in db.""" 40 | return len(self._db) 41 | 42 | def update(self, task_id, task): # type (int, dict) -> () 43 | """Modify task in db with given task_id.""" 44 | self._db.update(task, doc_ids=[task_id]) 45 | 46 | def delete(self, task_id): # type (int) -> () 47 | """Remove a task from db with given task_id.""" 48 | self._db.remove(doc_ids=[task_id]) 49 | 50 | def delete_all(self): 51 | """Remove all tasks from db.""" 52 | self._db.purge() 53 | 54 | def unique_id(self): # type () -> int 55 | """Return an integer that does not exist in the db.""" 56 | i = 1 57 | while self._db.contains(doc_ids=[i]): 58 | i += 1 59 | return i 60 | 61 | def stop_tasks_db(self): 62 | """Disconnect from DB.""" 63 | self._db.close() 64 | 65 | 66 | def start_tasks_db(db_path): # type (str) -> TasksDB_MongoDB object 67 | """Connect to db.""" 68 | return TasksDB_TinyDB(db_path) 69 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Define some fixtures to use in the project.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def tasks_db_session(tmpdir_factory, request): 10 | """Connect to db before tests, disconnect after.""" 11 | temp_dir = tmpdir_factory.mktemp("temp") 12 | tasks.start_tasks_db(str(temp_dir), "tiny") 13 | yield # this is where the testing happens 14 | tasks.stop_tasks_db() 15 | 16 | 17 | @pytest.fixture() 18 | def tasks_db(tasks_db_session): 19 | """An empty tasks db.""" 20 | tasks.delete_all() 21 | 22 | 23 | # Reminder of Task constructor interface 24 | # Task(summary=None, owner=None, done=False, id=None) 25 | # Don't set id, it's set by database 26 | # owner and done are optional 27 | 28 | 29 | @pytest.fixture() 30 | def tasks_just_a_few(): 31 | """All summaries and owners are unique.""" 32 | return ( 33 | Task("Write some code", "Brian", True), 34 | Task("Code review Brian's code", "Katie", False), 35 | Task("Fix what Brian did", "Michelle", False), 36 | ) 37 | 38 | 39 | @pytest.fixture() 40 | def tasks_mult_per_owner(): 41 | """Several owners with several tasks each.""" 42 | return ( 43 | Task("Make a cookie", "Raphael"), 44 | Task("Use an emoji", "Raphael"), 45 | Task("Move to Berlin", "Raphael"), 46 | Task("Create", "Michelle"), 47 | Task("Inspire", "Michelle"), 48 | Task("Encourage", "Michelle"), 49 | Task("Do a handstand", "Daniel"), 50 | Task("Write some books", "Daniel"), 51 | Task("Eat ice cream", "Daniel"), 52 | ) 53 | 54 | 55 | @pytest.fixture() 56 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 57 | """Connected db with 3 tasks, all unique.""" 58 | for t in tasks_just_a_few: 59 | tasks.add(t) 60 | 61 | 62 | @pytest.fixture() 63 | def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): 64 | """Connected db with 9 tasks, 3 owners, all with 3 tasks.""" 65 | for t in tasks_mult_per_owner: 66 | tasks.add(t) 67 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_returns_valid_id(tasks_db): 9 | """tasks.add() should return an integer.""" 10 | # GIVEN an initialized tasks db 11 | # WHEN a new task is added 12 | # THEN returned task_id is of type int 13 | new_task = Task('do something') 14 | task_id = tasks.add(new_task) 15 | assert isinstance(task_id, int) 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_added_task_has_id_set(tasks_db): 20 | """Make sure the task_id field is set by tasks.add().""" 21 | # GIVEN an initialized tasks db 22 | # AND a new task is added 23 | new_task = Task('sit in chair', owner='me', done=True) 24 | task_id = tasks.add(new_task) 25 | 26 | # WHEN task is retrieved 27 | task_from_db = tasks.get(task_id) 28 | 29 | # THEN task_id matches id field 30 | assert task_from_db.id == task_id 31 | 32 | # AND contents are equivalent (except for id) 33 | # the [:-1] syntax returns a list with all but the last element 34 | assert task_from_db[:-1] == new_task[:-1] 35 | 36 | 37 | def test_add_increases_count(db_with_3_tasks): 38 | """Test tasks.add() affect on tasks.count().""" 39 | # GIVEN a db with 3 tasks 40 | # WHEN another task is added 41 | tasks.add(Task('throw a party')) 42 | 43 | # THEN the count increases by 1 44 | assert tasks.count() == 4 45 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/func/test_add_variety.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def test_add_1(tasks_db): 9 | """tasks.get() using id returned from add() works.""" 10 | task = Task('breathe', 'BRIAN', True) 11 | task_id = tasks.add(task) 12 | t_from_db = tasks.get(task_id) 13 | # everything but the id should be the same 14 | assert equivalent(t_from_db, task) 15 | 16 | 17 | def equivalent(t1, t2): 18 | """Check two tasks for equivalence.""" 19 | # Compare everything but the id field 20 | return ((t1.summary == t2.summary) and 21 | (t1.owner == t2.owner) and 22 | (t1.done == t2.done)) 23 | 24 | 25 | @pytest.mark.parametrize('task', 26 | [Task('sleep', done=True), 27 | Task('wake', 'brian'), 28 | Task('breathe', 'BRIAN', True), 29 | Task('exercise', 'BrIaN', False)]) 30 | def test_add_2(tasks_db, task): 31 | """Demonstrate parametrize with one parameter.""" 32 | task_id = tasks.add(task) 33 | t_from_db = tasks.get(task_id) 34 | assert equivalent(t_from_db, task) 35 | 36 | 37 | @pytest.mark.parametrize('summary, owner, done', 38 | [('sleep', None, False), 39 | ('wake', 'brian', False), 40 | ('breathe', 'BRIAN', True), 41 | ('eat eggs', 'BrIaN', False), 42 | ]) 43 | def test_add_3(tasks_db, summary, owner, done): 44 | """Demonstrate parametrize with multiple parameters.""" 45 | task = Task(summary, owner, done) 46 | task_id = tasks.add(task) 47 | t_from_db = tasks.get(task_id) 48 | assert equivalent(t_from_db, task) 49 | 50 | 51 | tasks_to_try = (Task('sleep', done=True), 52 | Task('wake', 'brian'), 53 | Task('breathe', 'BRIAN', True), 54 | Task('exercise', 'BrIaN', False)) 55 | 56 | 57 | @pytest.mark.parametrize('task', tasks_to_try) 58 | def test_add_4(tasks_db, task): 59 | """Slightly different take.""" 60 | task_id = tasks.add(task) 61 | t_from_db = tasks.get(task_id) 62 | assert equivalent(t_from_db, task) 63 | 64 | 65 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 66 | for t in tasks_to_try] 67 | 68 | 69 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 70 | def test_add_5(tasks_db, task): 71 | """Demonstrate ids.""" 72 | task_id = tasks.add(task) 73 | t_from_db = tasks.get(task_id) 74 | assert equivalent(t_from_db, task) 75 | 76 | 77 | @pytest.mark.parametrize('task', [ 78 | pytest.param(Task('create'), id='just summary'), 79 | pytest.param(Task('inspire', 'Michelle'), id='summary/owner'), 80 | pytest.param(Task('encourage', 'Michelle', True), id='summary/owner/done')]) 81 | def test_add_6(task): 82 | """Demonstrate pytest.param and id.""" 83 | task_id = tasks.add(task) 84 | t_from_db = tasks.get(task_id) 85 | assert equivalent(t_from_db, task) 86 | 87 | 88 | @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) 89 | class TestAdd(): 90 | """Demonstrate paramterize and test classes.""" 91 | 92 | def test_equivalent(self, tasks_db, task): 93 | """Similar test, just within a class.""" 94 | task_id = tasks.add(task) 95 | t_from_db = tasks.get(task_id) 96 | assert equivalent(t_from_db, task) 97 | 98 | def test_valid_id(self, tasks_db, task): 99 | """We can use the same data for multiple tests.""" 100 | task_id = tasks.add(task) 101 | t_from_db = tasks.get(task_id) 102 | assert t_from_db.id == task_id 103 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/func/test_add_variety2.py: -------------------------------------------------------------------------------- 1 | """Test the tasks.add() API function.""" 2 | 3 | import pytest 4 | import tasks 5 | from tasks import Task 6 | 7 | tasks_to_try = (Task('sleep', done=True), 8 | Task('wake', 'brian'), 9 | Task('breathe', 'BRIAN', True), 10 | Task('exercise', 'BrIaN', False)) 11 | 12 | task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) 13 | for t in tasks_to_try] 14 | 15 | 16 | def equivalent(t1, t2): 17 | """Check two tasks for equivalence.""" 18 | return ((t1.summary == t2.summary) and 19 | (t1.owner == t2.owner) and 20 | (t1.done == t2.done)) 21 | 22 | 23 | @pytest.fixture(params=tasks_to_try) 24 | def a_task(request): 25 | """Using no ids.""" 26 | return request.param 27 | 28 | 29 | def test_add_a(tasks_db, a_task): 30 | """Using a_task fixture (no ids).""" 31 | task_id = tasks.add(a_task) 32 | t_from_db = tasks.get(task_id) 33 | assert equivalent(t_from_db, a_task) 34 | 35 | 36 | @pytest.fixture(params=tasks_to_try, ids=task_ids) 37 | def b_task(request): 38 | """Using a list of ids.""" 39 | return request.param 40 | 41 | 42 | def test_add_b(tasks_db, b_task): 43 | """Using b_task fixture, with ids.""" 44 | task_id = tasks.add(b_task) 45 | t_from_db = tasks.get(task_id) 46 | assert equivalent(t_from_db, b_task) 47 | 48 | 49 | def id_func(fixture_value): 50 | """A function for generating ids.""" 51 | t = fixture_value 52 | return 'Task({},{},{})'.format(t.summary, t.owner, t.done) 53 | 54 | 55 | @pytest.fixture(params=tasks_to_try, ids=id_func) 56 | def c_task(request): 57 | """Using a function (id_func) to generate ids.""" 58 | return request.param 59 | 60 | 61 | def test_add_c(tasks_db, c_task): 62 | """Use fixture with generated ids.""" 63 | task_id = tasks.add(c_task) 64 | t_from_db = tasks.get(task_id) 65 | assert equivalent(t_from_db, c_task) 66 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/func/test_api_exceptions.py: -------------------------------------------------------------------------------- 1 | """Test for expected exceptions from using the API wrong.""" 2 | import pytest 3 | import tasks 4 | from tasks import Task 5 | 6 | 7 | @pytest.mark.usefixtures('tasks_db') 8 | class TestAdd(): 9 | """Tests related to tasks.add().""" 10 | 11 | def test_missing_summary(self): 12 | """Should raise an exception if summary missing.""" 13 | with pytest.raises(ValueError): 14 | tasks.add(Task(owner='bob')) 15 | 16 | def test_done_not_bool(self): 17 | """Should raise an exception if done is not a bool.""" 18 | with pytest.raises(ValueError): 19 | tasks.add(Task(summary='summary', done='True')) 20 | 21 | 22 | def test_add_raises(): 23 | """add() should raise an exception with wrong type param.""" 24 | with pytest.raises(TypeError): 25 | tasks.add(task='not a Task object') 26 | 27 | 28 | @pytest.mark.smoke 29 | def test_list_raises(): 30 | """list() should raise an exception with wrong type param.""" 31 | with pytest.raises(TypeError): 32 | tasks.list_tasks(owner=123) 33 | 34 | 35 | @pytest.mark.get 36 | @pytest.mark.smoke 37 | def test_get_raises(): 38 | """get() should raise an exception with wrong type param.""" 39 | with pytest.raises(TypeError): 40 | tasks.get(task_id='123') 41 | 42 | 43 | class TestUpdate(): 44 | """Test expected exceptions with tasks.update().""" 45 | 46 | def test_bad_id(self): 47 | """A non-int id should raise an excption.""" 48 | with pytest.raises(TypeError): 49 | tasks.update(task_id={'dict instead': 1}, 50 | task=tasks.Task()) 51 | 52 | def test_bad_task(self): 53 | """A non-Task task should raise an excption.""" 54 | with pytest.raises(TypeError): 55 | tasks.update(task_id=1, task='not a task') 56 | 57 | 58 | def test_delete_raises(): 59 | """delete() should raise an exception with wrong type param.""" 60 | with pytest.raises(TypeError): 61 | tasks.delete(task_id=(1, 2, 3)) 62 | 63 | 64 | def test_start_tasks_db_raises(): 65 | """Make sure unsupported db raises an exception.""" 66 | with pytest.raises(ValueError) as excinfo: 67 | tasks.start_tasks_db('some/great/path', 'mysql') 68 | exception_msg = excinfo.value.args[0] 69 | assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 70 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/func/test_unique_id.py: -------------------------------------------------------------------------------- 1 | """Test tasks.unique_id().""" 2 | 3 | import tasks 4 | 5 | 6 | def test_unique_id(tasks_db, tasks_mult_per_owner): 7 | """unique_id() should return an unused id.""" 8 | existing_tasks = tasks.list_tasks() 9 | uid = tasks.unique_id() 10 | for t in existing_tasks: 11 | assert uid != t.id 12 | 13 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/unit/test_cli.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import pytest 3 | from click.testing import CliRunner 4 | import tasks.cli 5 | import tasks.config 6 | from tasks.api import Task 7 | 8 | 9 | @contextmanager 10 | def stub_tasks_db(): 11 | yield 12 | 13 | 14 | def test_list_no_args(mocker): 15 | # Replace the _tasks_db() context manager with our stub that does nothing. 16 | mocker.patch.object(tasks.cli, "_tasks_db", new=stub_tasks_db) 17 | 18 | # Replace any calls to tasks.list_tasks() from within tasks.cli to a default 19 | # MagicMock object with a return value of an empty list. 20 | mocker.patch.object(tasks.cli.tasks, "list_tasks", return_value=[]) 21 | 22 | # Use the Click CliRunner to do the same thing as calling tasks list on the command 23 | # line. 24 | runner = CliRunner() 25 | runner.invoke(tasks.cli.tasks_cli, ["list"]) 26 | 27 | # Use the mock object to make sure the API call was called correctly. 28 | # assert_called_once_with() is part of unittest.mock.Mock objects. 29 | tasks.cli.tasks.list_tasks.assert_called_once_with(None) 30 | 31 | 32 | @pytest.fixture() 33 | def no_db(mocker): 34 | """Put the mock stubbing of _tasks_db into a fixture so we can reuse it more easily 35 | in future tests.""" 36 | mocker.patch.object(tasks.cli, "_tasks_db", new=stub_tasks_db) 37 | 38 | 39 | def test_list_print_empty(no_db, mocker): 40 | mocker.patch.object(tasks.cli.tasks, "list_tasks", return_value=[]) 41 | runner = CliRunner() 42 | result = runner.invoke(tasks.cli.tasks_cli, ["list"]) 43 | expected_output = ( 44 | " ID owner done summary\n" " -- ----- ---- -------\n" 45 | ) 46 | 47 | # Check the output of the command-line action 48 | assert result.output == expected_output 49 | 50 | 51 | def test_list_print_many_items(no_db, mocker): 52 | many_tasks = ( 53 | Task("write chapter", "Brian", True, 1), 54 | Task("edit chapter", "Katie", False, 2), 55 | Task("modify chapter", "Brian", False, 3), 56 | Task("finalize chapter", "Katie", False, 4), 57 | ) 58 | mocker.patch.object(tasks.cli.tasks, "list_tasks", return_value=many_tasks) 59 | runner = CliRunner() 60 | result = runner.invoke(tasks.cli.tasks_cli, ["list"]) 61 | expected_output = ( 62 | " ID owner done summary\n" 63 | " -- ----- ---- -------\n" 64 | " 1 Brian True write chapter\n" 65 | " 2 Katie False edit chapter\n" 66 | " 3 Brian False modify chapter\n" 67 | " 4 Katie False finalize chapter\n" 68 | ) 69 | assert result.output == expected_output 70 | 71 | 72 | def test_list_dash_o(no_db, mocker): 73 | mocker.patch.object(tasks.cli.tasks, "list_tasks") 74 | runner = CliRunner() 75 | runner.invoke(tasks.cli.tasks_cli, ["list", "-o", "brian"]) 76 | tasks.cli.tasks.list_tasks.assert_called_once_with("brian") 77 | 78 | 79 | def test_list_dash_dash_owner(no_db, mocker): 80 | mocker.patch.object(tasks.cli.tasks, "list_tasks") 81 | runner = CliRunner() 82 | runner.invoke(tasks.cli.tasks_cli, ["list", "--owner", "okken"]) 83 | tasks.cli.tasks.list_tasks.assert_called_once_with("okken") 84 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Test the Task data type.""" 2 | from tasks import Task 3 | 4 | 5 | def test_asdict(): 6 | """_asdict() should return a dictionary.""" 7 | t_task = Task('do something', 'okken', True, 21) 8 | t_dict = t_task._asdict() 9 | expected = {'summary': 'do something', 10 | 'owner': 'okken', 11 | 'done': True, 12 | 'id': 21} 13 | assert t_dict == expected 14 | 15 | 16 | def test_replace(): 17 | """replace() should change passed in fields.""" 18 | t_before = Task('finish book', 'brian', False) 19 | t_after = t_before._replace(id=10, done=True) 20 | t_expected = Task('finish book', 'brian', True, 10) 21 | assert t_after == t_expected 22 | 23 | 24 | def test_defaults(): 25 | """Using no parameters should invoke defaults.""" 26 | t1 = Task() 27 | t2 = Task(None, None, False, None) 28 | assert t1 == t2 29 | 30 | 31 | def test_member_access(): 32 | """Check .field functionality of namedtuple.""" 33 | t = Task('buy milk', 'brian') 34 | assert t.summary == 'buy milk' 35 | assert t.owner == 'brian' 36 | assert (t.done, t.id) == (False, None) 37 | -------------------------------------------------------------------------------- /ch7/tasks_proj_v2/tox.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | # tox.ini , put in same dir as setup.py 10 | 11 | [tox] 12 | envlist = py27,py36 13 | 14 | [testenv] 15 | # If you have multiple test dependencies, you can put them on separate lines 16 | deps=pytest 17 | # Tells tox to run pytest in each environment 18 | commands=pytest 19 | 20 | [pytest] 21 | # We can put whatever we normally would want to put into pytest.ini to configure pytest 22 | addopts = -rsxX -l --tb=short --strict 23 | markers = 24 | smoke: Run the smoke test test functions 25 | get: Run the test functions that test tasks.get() 26 | -------------------------------------------------------------------------------- /ch7/unittest/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tasks 3 | from tasks import Task 4 | 5 | 6 | @pytest.fixture(scope='session') 7 | def tasks_db_session(tmpdir_factory, request): 8 | """Connect to db before tests, disconnect after.""" 9 | temp_dir = tmpdir_factory.mktemp('temp') 10 | tasks.start_tasks_db(str(temp_dir), 'tiny') 11 | yield # this is where the testing happens 12 | tasks.stop_tasks_db() 13 | 14 | 15 | @pytest.fixture() 16 | def tasks_db(tasks_db_session): 17 | """An empty tasks db.""" 18 | tasks.delete_all() 19 | 20 | 21 | @pytest.fixture() 22 | def tasks_just_a_few(): 23 | """All summaries and owners are unique.""" 24 | return ( 25 | Task('Write some code', 'Brian', True), 26 | Task("Code review Brian's code", 'Katie', False), 27 | Task('Fix what Brian did', 'Michelle', False)) 28 | 29 | 30 | @pytest.fixture() 31 | def db_with_3_tasks(tasks_db, tasks_just_a_few): 32 | """Connected db with 3 tasks, all unique.""" 33 | for t in tasks_just_a_few: 34 | tasks.add(t) 35 | -------------------------------------------------------------------------------- /ch7/unittest/test_delete_pytest.py: -------------------------------------------------------------------------------- 1 | import tasks 2 | 3 | 4 | def test_delete_decreases_count(db_with_3_tasks): 5 | ids = [t.id for t in tasks.list_tasks()] 6 | # GIVEN 3 items 7 | assert tasks.count() == 3 8 | # WHEN we delete one 9 | tasks.delete(ids[0]) 10 | # THEN count decreases by 1 11 | assert tasks.count() == 2 12 | -------------------------------------------------------------------------------- /ch7/unittest/test_delete_unittest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import shutil 3 | import tempfile 4 | import tasks 5 | from tasks import Task 6 | 7 | 8 | def setUpModule(): 9 | """Make temp dir, initialize DB.""" 10 | global temp_dir 11 | temp_dir = tempfile.mkdtemp() 12 | tasks.start_tasks_db(str(temp_dir), 'tiny') 13 | 14 | 15 | def tearDownModule(): 16 | """Clean up DB, remove temp dir.""" 17 | tasks.stop_tasks_db() 18 | shutil.rmtree(temp_dir) 19 | 20 | 21 | class TestNonEmpty(unittest.TestCase): 22 | 23 | def setUp(self): 24 | tasks.delete_all() # start empty 25 | # add a few items, saving ids 26 | self.ids = [] 27 | self.ids.append(tasks.add(Task('One', 'Brian', True))) 28 | self.ids.append(tasks.add(Task('Two', 'Still Brian', False))) 29 | self.ids.append(tasks.add(Task('Three', 'Not Brian', False))) 30 | 31 | def test_delete_decreases_count(self): 32 | # GIVEN 3 items 33 | self.assertEqual(tasks.count(), 3) 34 | # WHEN we delete one 35 | tasks.delete(self.ids[0]) 36 | # THEN count decreases by 1 37 | self.assertEqual(tasks.count(), 2) 38 | -------------------------------------------------------------------------------- /ch7/unittest/test_delete_unittest_fix.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest 3 | import tasks 4 | from tasks import Task 5 | 6 | 7 | @pytest.mark.usefixtures('tasks_db_session') 8 | class TestNonEmpty(unittest.TestCase): 9 | 10 | def setUp(self): 11 | tasks.delete_all() # start empty 12 | # add a few items, saving ids 13 | self.ids = [] 14 | self.ids.append(tasks.add(Task('One', 'Brian', True))) 15 | self.ids.append(tasks.add(Task('Two', 'Still Brian', False))) 16 | self.ids.append(tasks.add(Task('Three', 'Not Brian', False))) 17 | 18 | def test_delete_decreases_count(self): 19 | # GIVEN 3 items 20 | self.assertEqual(tasks.count(), 3) 21 | # WHEN we delete one 22 | tasks.delete(self.ids[0]) 23 | # THEN count decreases by 1 24 | self.assertEqual(tasks.count(), 2) 25 | -------------------------------------------------------------------------------- /ch7/unittest/test_delete_unittest_fix2.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest 3 | import tasks 4 | from tasks import Task 5 | 6 | 7 | @pytest.fixture() 8 | def tasks_db_non_empty(tasks_db_session, request): 9 | tasks.delete_all() # start empty 10 | # add a few items, saving ids 11 | ids = [] 12 | ids.append(tasks.add(Task('One', 'Brian', True))) 13 | ids.append(tasks.add(Task('Two', 'Still Brian', False))) 14 | ids.append(tasks.add(Task('Three', 'Not Brian', False))) 15 | request.cls.ids = ids 16 | 17 | 18 | @pytest.mark.usefixtures('tasks_db_non_empty') 19 | class TestNonEmpty(unittest.TestCase): 20 | 21 | def test_delete_decreases_count(self): 22 | # GIVEN 3 items 23 | self.assertEqual(tasks.count(), 3) 24 | # WHEN we delete one 25 | tasks.delete(self.ids[0]) 26 | # THEN count decreases by 1 27 | self.assertEqual(tasks.count(), 2) 28 | -------------------------------------------------------------------------------- /tasks_proj/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The Pragmatic Programmers, LLC 2 | 3 | All rights reserved. 4 | 5 | Copyrights apply to this source code. 6 | 7 | You may use the source code in your own projects, however the source code may 8 | not be used to create commercial training material, courses, books, articles, and the 9 | like. We make no guarantees that this source code is fit for any purpose. 10 | -------------------------------------------------------------------------------- /tasks_proj/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | 3 | # Tests 4 | recursive-include tests *.py 5 | 6 | -------------------------------------------------------------------------------- /tasks_proj/setup.py: -------------------------------------------------------------------------------- 1 | """Minimal setup file for tasks project.""" 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name="tasks", 7 | version="0.1.0", 8 | license="proprietary", 9 | description="Minimal Project Task Management", 10 | author="Brian Okken", 11 | author_email="Please use pythontesting.net contact form.", 12 | url="https://pragprog.com/book/bopytest", 13 | packages=find_packages(where="src"), 14 | package_dir={"": "src"}, 15 | install_requires=["click", "tinydb", "six"], 16 | extras_require={"mongo": "pymongo"}, 17 | entry_points={"console_scripts": ["tasks = tasks.cli:tasks_cli",]}, 18 | ) 19 | -------------------------------------------------------------------------------- /tasks_proj/src/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | """Minimal Project Task Management.""" 2 | 3 | from .api import ( # noqa: F401 4 | Task, 5 | TasksException, 6 | add, 7 | get, 8 | list_tasks, 9 | count, 10 | update, 11 | delete, 12 | delete_all, 13 | unique_id, 14 | start_tasks_db, 15 | stop_tasks_db 16 | ) 17 | 18 | __version__ = '0.1.0' 19 | -------------------------------------------------------------------------------- /tasks_proj/src/tasks/cli.py: -------------------------------------------------------------------------------- 1 | """Command Line Interface (CLI) for tasks project.""" 2 | 3 | from __future__ import print_function 4 | import click 5 | import tasks.config 6 | from contextlib import contextmanager 7 | from tasks.api import Task 8 | 9 | 10 | # The main entry point for tasks. 11 | @click.group(context_settings={'help_option_names': ['-h', '--help']}) 12 | @click.version_option(version='0.1.0') 13 | def tasks_cli(): 14 | """Run the tasks application.""" 15 | 16 | 17 | @tasks_cli.command(help="add a task") 18 | @click.argument('summary') 19 | @click.option('-o', '--owner', default=None, 20 | help='set the task owner') 21 | def add(summary, owner): 22 | """Add a task to db.""" 23 | with _tasks_db(): 24 | tasks.add(Task(summary, owner)) 25 | 26 | 27 | @tasks_cli.command(help="delete a task") 28 | @click.argument('task_id', type=int) 29 | def delete(task_id): 30 | """Remove task in db with given id.""" 31 | with _tasks_db(): 32 | tasks.delete(task_id) 33 | 34 | 35 | @tasks_cli.command(name="list", help="list tasks") 36 | @click.option('-o', '--owner', default=None, 37 | help='list tasks with this owner') 38 | def list_tasks(owner): 39 | """ 40 | List tasks in db. 41 | 42 | If owner given, only list tasks with that owner. 43 | """ 44 | formatstr = "{: >4} {: >10} {: >5} {}" 45 | print(formatstr.format('ID', 'owner', 'done', 'summary')) 46 | print(formatstr.format('--', '-----', '----', '-------')) 47 | with _tasks_db(): 48 | for t in tasks.list_tasks(owner): 49 | done = 'True' if t.done else 'False' 50 | owner = '' if t.owner is None else t.owner 51 | print(formatstr.format( 52 | t.id, owner, done, t.summary)) 53 | 54 | 55 | @tasks_cli.command(help="update task") 56 | @click.argument('task_id', type=int) 57 | @click.option('-o', '--owner', default=None, 58 | help='change the task owner') 59 | @click.option('-s', '--summary', default=None, 60 | help='change the task summary') 61 | @click.option('-d', '--done', default=None, 62 | type=bool, 63 | help='change the task done state (True or False)') 64 | def update(task_id, owner, summary, done): 65 | """Modify a task in db with given id with new info.""" 66 | with _tasks_db(): 67 | tasks.update(task_id, Task(summary, owner, done)) 68 | 69 | 70 | @tasks_cli.command(help="list count") 71 | def count(): 72 | """Return number of tasks in db.""" 73 | with _tasks_db(): 74 | c = tasks.count() 75 | print(c) 76 | 77 | 78 | @contextmanager 79 | def _tasks_db(): 80 | config = tasks.config.get_config() 81 | tasks.start_tasks_db(config.db_path, config.db_type) 82 | yield 83 | tasks.stop_tasks_db() 84 | 85 | 86 | if __name__ == '__main__': 87 | tasks_cli() 88 | -------------------------------------------------------------------------------- /tasks_proj/src/tasks/config.py: -------------------------------------------------------------------------------- 1 | """Handle configuration files for tasks CLI.""" 2 | 3 | from collections import namedtuple 4 | try: 5 | from configparser import ConfigParser 6 | except ImportError: 7 | from ConfigParser import ConfigParser 8 | 9 | import os 10 | 11 | TasksConfig = namedtuple('TasksConfig', ['db_path', 'db_type']) 12 | 13 | 14 | def get_config(): 15 | """Return TasksConfig object after reading config file.""" 16 | parser = ConfigParser() 17 | config_file = os.path.expanduser('~/.tasks.config') 18 | if not os.path.exists(config_file): 19 | tasks_db_path = '~/tasks_db/' 20 | tasks_db_type = 'tiny' 21 | else: 22 | parser.read(config_file) 23 | tasks_db_path = parser.get('TASKS', 'tasks_db_path') 24 | tasks_db_type = parser.get('TASKS', 'tasks_db_type') 25 | tasks_db_path = os.path.expanduser(tasks_db_path) 26 | return TasksConfig(tasks_db_path, tasks_db_type) 27 | -------------------------------------------------------------------------------- /tasks_proj/src/tasks/tasksdb_pymongo.py: -------------------------------------------------------------------------------- 1 | """Database wrapper for MongoDB for tasks project.""" 2 | 3 | import os 4 | import pymongo 5 | import subprocess 6 | import time 7 | from bson.objectid import ObjectId 8 | 9 | 10 | class TasksDB_MongoDB(): # noqa: E801 11 | """Wrapper class for MongoDB. 12 | 13 | The methods in this class need to match 14 | all database interaction classes. 15 | 16 | So far, this is: 17 | TasksDB_TinyDB found in tasksdb_tinydb.py. 18 | """ 19 | 20 | def __init__(self, db_path): # type (str) -> () 21 | """Start MongoDB client and connect to db.""" 22 | self._process = None 23 | self._client = None 24 | self._start_mongod(db_path) 25 | self._connect() 26 | 27 | def add(self, task): # type (dict) -> int 28 | """Add a task dict to db.""" 29 | return self._db.task_list.insert_one(task).inserted_id 30 | 31 | def get(self, task_id): # type (int) -> dict 32 | """Return a task dict with matching id.""" 33 | return self._db.task_list.find_one({'_id': ObjectId(task_id)}) 34 | 35 | def list_tasks(self, owner=None): # type (str) -> list[dict] 36 | """Return list of tasks.""" 37 | return list(self._db.task_list.find()) 38 | 39 | def count(self): # type () -> int 40 | """Return number of tasks in db.""" 41 | return self._db.task_list.count() 42 | 43 | def update(self, task_id, task): # type (int, dict) -> () 44 | """Modify task in db with given task_id.""" 45 | self._db.tasks_list.update_one({'_id': ObjectId(task_id)}, task) 46 | 47 | def delete(self, task_id): # type (int) -> () 48 | """Remove a task from db with given task_id.""" 49 | reply = self._db.task_list.delete_one({'_id': ObjectId(task_id)}) 50 | if reply.deleted_count == 0: 51 | raise ValueError('id {} not in task database'.format(str(task_id))) 52 | 53 | def unique_id(self): # type () -> int 54 | """Return an integer that does not exist in the db.""" 55 | return ObjectId() 56 | 57 | def delete_all(self): 58 | """Remove all tasks from db.""" 59 | self._db.task_list.drop() 60 | 61 | def stop_tasks_db(self): 62 | """Disconnect from db.""" 63 | self._disconnect() 64 | self._stop_mongod() 65 | 66 | def _start_mongod(self, db_path): 67 | self._process = subprocess.Popen(['mongod', '--dbpath', db_path], 68 | stdout=open(os.devnull, 'wb'), 69 | stderr=subprocess.STDOUT) 70 | assert self._process, "mongod process failed to start" 71 | 72 | def _stop_mongod(self): 73 | if self._process: 74 | self._process.terminate() 75 | self._process.wait() 76 | self._process = None 77 | 78 | def _connect(self): 79 | if self._process and (not self._client or not self._db): 80 | for i in range(3): 81 | try: 82 | self._client = pymongo.MongoClient() 83 | except pymongo.errors.ConnectionFailure: 84 | time.sleep(0.1) 85 | continue 86 | else: 87 | break 88 | if self._client: 89 | self._db = self._client.task_list 90 | 91 | def _disconnect(self): 92 | self._db = None 93 | self._client = None 94 | 95 | 96 | def start_tasks_db(db_path): # type (str) -> TasksDB_MongoDB object 97 | """Connect to db.""" 98 | return TasksDB_MongoDB(db_path) 99 | -------------------------------------------------------------------------------- /tasks_proj/src/tasks/tasksdb_tinydb.py: -------------------------------------------------------------------------------- 1 | """Database wrapper for TinyDB for tasks project.""" 2 | import tinydb 3 | 4 | 5 | class TasksDB_TinyDB(): # noqa : E801 6 | """Wrapper class for TinyDB. 7 | 8 | The methods in this class need to match 9 | all database interaction classes. 10 | 11 | So far, this is: 12 | TasksDB_MongoDB found in tasksdb_pymongo.py. 13 | TasksDB_TinyDB found in tasksdb_tinydb.py. 14 | """ 15 | 16 | def __init__(self, db_path): # type (str) -> () 17 | """Connect to db.""" 18 | self._db = tinydb.TinyDB(db_path + '/tasks_db.json') 19 | 20 | def add(self, task): # type (dict) -> int 21 | """Add a task dict to db.""" 22 | task_id = self._db.insert(task) 23 | task['id'] = task_id 24 | self._db.update(task, doc_ids=[task_id]) 25 | return task_id 26 | 27 | def get(self, task_id): # type (int) -> dict 28 | """Return a task dict with matching id.""" 29 | return self._db.get(doc_id=task_id) 30 | 31 | def list_tasks(self, owner=None): # type (str) -> list[dict] 32 | """Return list of tasks.""" 33 | if owner is None: 34 | return self._db.all() 35 | else: 36 | return self._db.search(tinydb.Query().owner == owner) 37 | 38 | def count(self): # type () -> int 39 | """Return number of tasks in db.""" 40 | return len(self._db) 41 | 42 | def update(self, task_id, task): # type (int, dict) -> () 43 | """Modify task in db with given task_id.""" 44 | self._db.update(task, doc_ids=[task_id]) 45 | 46 | def delete(self, task_id): # type (int) -> () 47 | """Remove a task from db with given task_id.""" 48 | self._db.remove(doc_ids=[task_id]) 49 | 50 | def delete_all(self): 51 | """Remove all tasks from db.""" 52 | self._db.purge() 53 | 54 | def unique_id(self): # type () -> int 55 | """Return an integer that does not exist in the db.""" 56 | i = 1 57 | while self._db.contains(doc_ids=[i]): 58 | i += 1 59 | return i 60 | 61 | def stop_tasks_db(self): 62 | """Disconnect from DB.""" 63 | self._db.close() 64 | 65 | 66 | def start_tasks_db(db_path): # type (str) -> TasksDB_MongoDB object 67 | """Connect to db.""" 68 | return TasksDB_TinyDB(db_path) 69 | -------------------------------------------------------------------------------- /tasks_proj/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Placeholder.""" 2 | 3 | # nothing here yet 4 | -------------------------------------------------------------------------------- /tasks_proj/tests/func/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /tasks_proj/tests/func/test_add.py: -------------------------------------------------------------------------------- 1 | """ 2 | Placeholder test file. 3 | 4 | We'll add a bunch of tests here in later versions. 5 | """ 6 | 7 | 8 | def test_add(): 9 | """Placeholder test.""" 10 | pass 11 | -------------------------------------------------------------------------------- /tasks_proj/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ;--- 2 | ; Excerpted from "Python Testing with pytest", 3 | ; published by The Pragmatic Bookshelf. 4 | ; Copyrights apply to this code. It may not be used to create training material, 5 | ; courses, books, articles, and the like. Contact us if you are in doubt. 6 | ; We make no guarantees that this code is fit for any purpose. 7 | ; Visit http://www.pragmaticprogrammer.com/titles/bopytest for more book information. 8 | ;--- 9 | -------------------------------------------------------------------------------- /tasks_proj/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Avoid test file name collision. 3 | 4 | __init__.py files in test directories allow 5 | test files in multiple directories to have the same 6 | name in the same session. 7 | 8 | See "Avoiding Filename Collisions" in Chapter 6 for 9 | more information. 10 | """ 11 | -------------------------------------------------------------------------------- /tasks_proj/tests/unit/test_task.py: -------------------------------------------------------------------------------- 1 | """Placeholder test file.""" 2 | --------------------------------------------------------------------------------