├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── django_mock_queries ├── __init__.py ├── asserts.py ├── comparisons.py ├── constants.py ├── exceptions.py ├── mocks.py ├── query.py └── utils.py ├── examples ├── __init__.py └── users │ ├── __init__.py │ ├── analytics │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── models.py │ ├── tests.py │ └── views.py │ ├── manage.py │ ├── pytest.ini │ └── users │ ├── __init__.py │ ├── settings.py │ ├── settings_mocked.py │ ├── urls.py │ └── wsgi.py ├── requirements ├── core.txt └── dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── mock_models.py ├── mock_settings.py ├── test_asserts.py ├── test_mocks.py ├── test_query.py └── test_utils.py └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | main: 9 | strategy: 10 | matrix: 11 | include: 12 | - python: '3.8' 13 | tox_env: 'py38-dj22-drf313' 14 | - python: '3.9' 15 | tox_env: 'py39-dj22-drf313' 16 | - python: '3.8' 17 | tox_env: 'py38-dj32-drf314' 18 | - python: '3.9' 19 | tox_env: 'py39-dj32-drf314' 20 | - python: '3.10' 21 | tox_env: 'py310-dj32-drf314' 22 | - python: '3.8' 23 | tox_env: 'py38-dj42-drf314' 24 | - python: '3.9' 25 | tox_env: 'py39-dj42-drf314' 26 | - python: '3.10' 27 | tox_env: 'py310-dj42-drf314' 28 | - python: '3.11' 29 | tox_env: 'py311-dj42-drf314' 30 | - python: '3.12' 31 | tox_env: 'py312-dj42-drf314' 32 | - python: '3.10' 33 | tox_env: 'py310-dj50-drf314' 34 | - python: '3.11' 35 | tox_env: 'py311-dj50-drf314' 36 | - python: '3.12' 37 | tox_env: 'py312-dj50-drf314' 38 | 39 | runs-on: ubuntu-22.04 40 | name: Python ${{ matrix.python }} with packages ${{ matrix.tox_env }} 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: set up Python 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: ${{ matrix.python }} 48 | 49 | - name: install dependencies 50 | run: python -m pip install tox coverage 51 | 52 | - name: run test suite 53 | env: 54 | TOX_ENV: ${{ matrix.tox_env }} 55 | run: tox -ve $TOX_ENV 56 | 57 | - name: run coverage 58 | env: 59 | TOX_ENV: ${{ matrix.tox_env }} 60 | run: | 61 | python -m pip install codecov 62 | codecov -e TOX_ENV 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | .python-version 4 | .coverage 5 | MANIFEST 6 | 7 | dist/ 8 | .tox/ 9 | .idea/ 10 | .vscode/ 11 | .cache/ 12 | *migrations/ 13 | *.egg-info/ 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | For more recent versions, please refer to the corresponding release on GitHub: https://github.com/stphivos/django-mock-queries/releases 4 | 5 | ## [v2.1.7](https://github.com/stphivos/django-mock-queries/tree/v2.1.7) (2021-09-12) 6 | 7 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.1.6...v2.1.7) 8 | 9 | **Closed issues:** 10 | 11 | - Release date 2.1.6 [\#131](https://github.com/stphivos/django-mock-queries/issues/131) 12 | - Loosen model-bakery dependency [\#130](https://github.com/stphivos/django-mock-queries/issues/130) 13 | - Support for QuerySet.iterator\(\) [\#94](https://github.com/stphivos/django-mock-queries/issues/94) 14 | 15 | **Merged pull requests:** 16 | 17 | - Improve flaky test for query order by random [\#145](https://github.com/stphivos/django-mock-queries/pull/145) ([stphivos](https://github.com/stphivos)) 18 | - Remove dollar sign from example commands [\#144](https://github.com/stphivos/django-mock-queries/pull/144) ([1oglop1](https://github.com/1oglop1)) 19 | - Loosen model-bakery dependency [\#140](https://github.com/stphivos/django-mock-queries/pull/140) ([allanlewis](https://github.com/allanlewis)) 20 | - Suppress UnorderedObjectListWarning when ordering MockSet [\#139](https://github.com/stphivos/django-mock-queries/pull/139) ([thatguysimon](https://github.com/thatguysimon)) 21 | 22 | ## [v2.1.6](https://github.com/stphivos/django-mock-queries/tree/v2.1.6) (2021-02-21) 23 | 24 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.1.5...v2.1.6) 25 | 26 | **Closed issues:** 27 | 28 | - MockSet constructor limited to 11 elements on Python 3.8 [\#125](https://github.com/stphivos/django-mock-queries/issues/125) 29 | - Error if use order\_by random [\#119](https://github.com/stphivos/django-mock-queries/issues/119) 30 | - MockModel never "in" MockSet in Python 2.7 [\#117](https://github.com/stphivos/django-mock-queries/issues/117) 31 | - Limit in MockModel number inside MockSet? [\#113](https://github.com/stphivos/django-mock-queries/issues/113) 32 | - README.md typo [\#106](https://github.com/stphivos/django-mock-queries/issues/106) 33 | - Filter with empty Q object doesn't work as expected [\#102](https://github.com/stphivos/django-mock-queries/issues/102) 34 | 35 | **Merged pull requests:** 36 | 37 | - Fix tox for ci [\#134](https://github.com/stphivos/django-mock-queries/pull/134) ([mdalp](https://github.com/mdalp)) 38 | - Support for QuerySet.iterator\(\) [\#132](https://github.com/stphivos/django-mock-queries/pull/132) ([platonfloria](https://github.com/platonfloria)) 39 | - Fix MockSet too many positional arguments error in python 3.8 [\#129](https://github.com/stphivos/django-mock-queries/pull/129) ([stphivos](https://github.com/stphivos)) 40 | - Fix bug on empty DjangoQ [\#128](https://github.com/stphivos/django-mock-queries/pull/128) ([stphivos](https://github.com/stphivos)) 41 | - Fix order\_by random [\#127](https://github.com/stphivos/django-mock-queries/pull/127) ([stphivos](https://github.com/stphivos)) 42 | - Update packages, drop unsupported Django versions with python 2.7, 3.5 [\#126](https://github.com/stphivos/django-mock-queries/pull/126) ([stphivos](https://github.com/stphivos)) 43 | - \#106 - add import to README [\#124](https://github.com/stphivos/django-mock-queries/pull/124) ([shinneider](https://github.com/shinneider)) 44 | - Build universal wheel [\#122](https://github.com/stphivos/django-mock-queries/pull/122) ([allanlewis](https://github.com/allanlewis)) 45 | - Use unittest.mock when available instead of mock. [\#121](https://github.com/stphivos/django-mock-queries/pull/121) ([Gabriel-Fontenelle](https://github.com/Gabriel-Fontenelle)) 46 | - Fix the problem that MockModel never "in" MockSet [\#118](https://github.com/stphivos/django-mock-queries/pull/118) ([mapx](https://github.com/mapx)) 47 | - Add query named values list [\#116](https://github.com/stphivos/django-mock-queries/pull/116) ([stphivos](https://github.com/stphivos)) 48 | 49 | ## [v2.1.5](https://github.com/stphivos/django-mock-queries/tree/v2.1.5) (2020-05-04) 50 | 51 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.1.4...v2.1.5) 52 | 53 | **Closed issues:** 54 | 55 | - Build is broken with pip 20.1 [\#111](https://github.com/stphivos/django-mock-queries/issues/111) 56 | - Add support for Django 3 [\#104](https://github.com/stphivos/django-mock-queries/issues/104) 57 | 58 | **Merged pull requests:** 59 | 60 | - Remove dependency on internal pip APIs [\#112](https://github.com/stphivos/django-mock-queries/pull/112) ([rbusquet](https://github.com/rbusquet)) 61 | 62 | ## [v2.1.4](https://github.com/stphivos/django-mock-queries/tree/v2.1.4) (2020-03-02) 63 | 64 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.1.3...v2.1.4) 65 | 66 | **Closed issues:** 67 | 68 | - Values list doesn't act as expected [\#103](https://github.com/stphivos/django-mock-queries/issues/103) 69 | 70 | **Merged pull requests:** 71 | 72 | - Add support for Django 3.0.x [\#109](https://github.com/stphivos/django-mock-queries/pull/109) ([stphivos](https://github.com/stphivos)) 73 | - Replace model-mommy with model-bakery [\#108](https://github.com/stphivos/django-mock-queries/pull/108) ([stphivos](https://github.com/stphivos)) 74 | - Extend get\_field\_values to work for date/datetime objects [\#101](https://github.com/stphivos/django-mock-queries/pull/101) ([brianzhou13](https://github.com/brianzhou13)) 75 | 76 | ## [v2.1.3](https://github.com/stphivos/django-mock-queries/tree/v2.1.3) (2019-05-04) 77 | 78 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.1.2...v2.1.3) 79 | 80 | **Closed issues:** 81 | 82 | - Breaking changes in 2.1.2: ignored filtered elements [\#96](https://github.com/stphivos/django-mock-queries/issues/96) 83 | 84 | **Merged pull requests:** 85 | 86 | - Use compatible release clause in tox deps versions [\#100](https://github.com/stphivos/django-mock-queries/pull/100) ([stphivos](https://github.com/stphivos)) 87 | - Add failing test with falsy comparable value [\#99](https://github.com/stphivos/django-mock-queries/pull/99) ([stphivos](https://github.com/stphivos)) 88 | - Django Dependency Issues [\#98](https://github.com/stphivos/django-mock-queries/pull/98) ([ish-vasa](https://github.com/ish-vasa)) 89 | - Fix 96: check if value is None, and do not rely on boolean conversion [\#97](https://github.com/stphivos/django-mock-queries/pull/97) ([dannywillems](https://github.com/dannywillems)) 90 | 91 | ## [v2.1.2](https://github.com/stphivos/django-mock-queries/tree/v2.1.2) (2019-04-27) 92 | 93 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.1.1...v2.1.2) 94 | 95 | **Closed issues:** 96 | 97 | - Mocking serializer: ListSerializer object is not callable [\#91](https://github.com/stphivos/django-mock-queries/issues/91) 98 | 99 | **Merged pull requests:** 100 | 101 | - Add support for Django 2.2 and Python 3.7 [\#95](https://github.com/stphivos/django-mock-queries/pull/95) ([m3brown](https://github.com/m3brown)) 102 | - Exclude none row from filter comparison [\#93](https://github.com/stphivos/django-mock-queries/pull/93) ([stphivos](https://github.com/stphivos)) 103 | - Exclude none row from filter comparison [\#92](https://github.com/stphivos/django-mock-queries/pull/92) ([tlfung0219](https://github.com/tlfung0219)) 104 | 105 | ## [v2.1.1](https://github.com/stphivos/django-mock-queries/tree/v2.1.1) (2018-08-09) 106 | 107 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.1.0...v2.1.1) 108 | 109 | ## [v2.1.0](https://github.com/stphivos/django-mock-queries/tree/v2.1.0) (2018-08-09) 110 | 111 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.0.1...v2.1.0) 112 | 113 | **Closed issues:** 114 | 115 | - MockSet doesn't seem to copy all of its related model's properties. [\#88](https://github.com/stphivos/django-mock-queries/issues/88) 116 | 117 | **Merged pull requests:** 118 | 119 | - Bump django version lock [\#89](https://github.com/stphivos/django-mock-queries/pull/89) ([dmastylo](https://github.com/dmastylo)) 120 | - Fix MockSet empty querysets evaluated to False when converted to bool in py27 [\#87](https://github.com/stphivos/django-mock-queries/pull/87) ([stphivos](https://github.com/stphivos)) 121 | - Fix: Empty querysets should be evaluated to False when converted to boolean [\#86](https://github.com/stphivos/django-mock-queries/pull/86) ([mannysz](https://github.com/mannysz)) 122 | - Missing range in comparisons list [\#85](https://github.com/stphivos/django-mock-queries/pull/85) ([rbusquet](https://github.com/rbusquet)) 123 | 124 | ## [v2.0.1](https://github.com/stphivos/django-mock-queries/tree/v2.0.1) (2018-05-18) 125 | 126 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v2.0.0...v2.0.1) 127 | 128 | **Closed issues:** 129 | 130 | - Filter by attribute with False value is not working [\#83](https://github.com/stphivos/django-mock-queries/issues/83) 131 | - Plans for releasing `MockSet` as a child of `MagicMock`? [\#81](https://github.com/stphivos/django-mock-queries/issues/81) 132 | 133 | **Merged pull requests:** 134 | 135 | - Fixing filtering with boolean fields. [\#84](https://github.com/stphivos/django-mock-queries/pull/84) ([zuzelvp](https://github.com/zuzelvp)) 136 | 137 | ## [v2.0.0](https://github.com/stphivos/django-mock-queries/tree/v2.0.0) (2018-04-21) 138 | 139 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v1.0.7...v2.0.0) 140 | 141 | **Closed issues:** 142 | 143 | - Incompatible with pip 10.0.0 [\#80](https://github.com/stphivos/django-mock-queries/issues/80) 144 | - `.distinct\(\)` when using a regular model: 'ModelName has no attribute items` [\#77](https://github.com/stphivos/django-mock-queries/issues/77) 145 | 146 | **Merged pull requests:** 147 | 148 | - ISSUE-80 Added pip==10.0 compatibility [\#82](https://github.com/stphivos/django-mock-queries/pull/82) ([khudyakovavi](https://github.com/khudyakovavi)) 149 | - Adds support for update\_or\_create [\#79](https://github.com/stphivos/django-mock-queries/pull/79) ([rbusquet](https://github.com/rbusquet)) 150 | - Fix hash\_dict to use concrete fields with django models [\#78](https://github.com/stphivos/django-mock-queries/pull/78) ([stphivos](https://github.com/stphivos)) 151 | - Refactor maintainability issues on complexity part 2 [\#76](https://github.com/stphivos/django-mock-queries/pull/76) ([stphivos](https://github.com/stphivos)) 152 | - Refactor maintainability issues on complexity part 1 [\#75](https://github.com/stphivos/django-mock-queries/pull/75) ([stphivos](https://github.com/stphivos)) 153 | - Refactor more maintainability issues on duplication [\#74](https://github.com/stphivos/django-mock-queries/pull/74) ([stphivos](https://github.com/stphivos)) 154 | - Refactor maintainability issues on duplication [\#73](https://github.com/stphivos/django-mock-queries/pull/73) ([stphivos](https://github.com/stphivos)) 155 | - Attempt to improve performance of MockSet by reducing the use of MagicMock. [\#71](https://github.com/stphivos/django-mock-queries/pull/71) ([zuzelvp](https://github.com/zuzelvp)) 156 | 157 | ## [v1.0.7](https://github.com/stphivos/django-mock-queries/tree/v1.0.7) (2018-03-03) 158 | 159 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v1.0.6...v1.0.7) 160 | 161 | **Closed issues:** 162 | 163 | - Support for Django 2.0 [\#69](https://github.com/stphivos/django-mock-queries/issues/69) 164 | - values\(\).distinct\(\)fails with TypeError: unhashable type: 'dict' [\#65](https://github.com/stphivos/django-mock-queries/issues/65) 165 | 166 | **Merged pull requests:** 167 | 168 | - Add support for Django 2 [\#72](https://github.com/stphivos/django-mock-queries/pull/72) ([stphivos](https://github.com/stphivos)) 169 | - Add MockSet write event triggers, improve model mocker orm simulation. [\#70](https://github.com/stphivos/django-mock-queries/pull/70) ([stphivos](https://github.com/stphivos)) 170 | - Feature/qs update delete [\#68](https://github.com/stphivos/django-mock-queries/pull/68) ([stphivos](https://github.com/stphivos)) 171 | - Omit call to save in MockSet.create [\#67](https://github.com/stphivos/django-mock-queries/pull/67) ([stphivos](https://github.com/stphivos)) 172 | - Improving implementation for distinct\(\) [\#66](https://github.com/stphivos/django-mock-queries/pull/66) ([zuzelvp](https://github.com/zuzelvp)) 173 | 174 | ## [v1.0.6](https://github.com/stphivos/django-mock-queries/tree/v1.0.6) (2018-02-13) 175 | 176 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v1.0.5...v1.0.6) 177 | 178 | **Closed issues:** 179 | 180 | - AND operator is not handled properly causing random tests [\#60](https://github.com/stphivos/django-mock-queries/issues/60) 181 | 182 | **Merged pull requests:** 183 | 184 | - Add python 3.5 to build matrix [\#64](https://github.com/stphivos/django-mock-queries/pull/64) ([stphivos](https://github.com/stphivos)) 185 | - Carta issues 60 and false positives [\#63](https://github.com/stphivos/django-mock-queries/pull/63) ([stphivos](https://github.com/stphivos)) 186 | - Fix failing test due to hardcoded year value 2017 [\#62](https://github.com/stphivos/django-mock-queries/pull/62) ([stphivos](https://github.com/stphivos)) 187 | - Issues 60 AND false positives [\#61](https://github.com/stphivos/django-mock-queries/pull/61) ([zuzelvp](https://github.com/zuzelvp)) 188 | - Keep the previous name for decorated methods, functions and classes [\#58](https://github.com/stphivos/django-mock-queries/pull/58) ([grabekm90](https://github.com/grabekm90)) 189 | 190 | ## [v1.0.5](https://github.com/stphivos/django-mock-queries/tree/v1.0.5) (2017-11-30) 191 | 192 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v1.0.4...v1.0.5) 193 | 194 | ## [v1.0.4](https://github.com/stphivos/django-mock-queries/tree/v1.0.4) (2017-11-30) 195 | 196 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v1.0.2...v1.0.4) 197 | 198 | **Fixed bugs:** 199 | 200 | - Support filtering on a values list subquery. [\#54](https://github.com/stphivos/django-mock-queries/pull/54) ([donkirkby](https://github.com/donkirkby)) 201 | 202 | **Merged pull requests:** 203 | 204 | - MockSet earliest/latest fields args [\#57](https://github.com/stphivos/django-mock-queries/pull/57) ([stphivos](https://github.com/stphivos)) 205 | - Not obligatory field parameter in latest and earliest query function [\#56](https://github.com/stphivos/django-mock-queries/pull/56) ([grabekm90](https://github.com/grabekm90)) 206 | - Fix a missed model parameter for MockSet in ModelMocker [\#55](https://github.com/stphivos/django-mock-queries/pull/55) ([grabekm90](https://github.com/grabekm90)) 207 | 208 | ## [v1.0.2](https://github.com/stphivos/django-mock-queries/tree/v1.0.2) (2017-10-16) 209 | 210 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v1.0.0...v1.0.2) 211 | 212 | **Merged pull requests:** 213 | 214 | - Fix hardcoded requirement to six library [\#53](https://github.com/stphivos/django-mock-queries/pull/53) ([stphivos](https://github.com/stphivos)) 215 | 216 | ## [v1.0.0](https://github.com/stphivos/django-mock-queries/tree/v1.0.0) (2017-10-15) 217 | 218 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v0.0.16...v1.0.0) 219 | 220 | **Closed issues:** 221 | 222 | - Model.objects.create can't take an instance parameter [\#50](https://github.com/stphivos/django-mock-queries/issues/50) 223 | - Q objects with negation [\#48](https://github.com/stphivos/django-mock-queries/issues/48) 224 | 225 | **Merged pull requests:** 226 | 227 | - Verify support of Django 1.11 [\#52](https://github.com/stphivos/django-mock-queries/pull/52) ([stphivos](https://github.com/stphivos)) 228 | - Use function find\_field\_names to determine appropriate field names fo… [\#51](https://github.com/stphivos/django-mock-queries/pull/51) ([stphivos](https://github.com/stphivos)) 229 | - Add support for Q objects with negation [\#49](https://github.com/stphivos/django-mock-queries/pull/49) ([stphivos](https://github.com/stphivos)) 230 | - Added missing lookups [\#47](https://github.com/stphivos/django-mock-queries/pull/47) ([szykin](https://github.com/szykin)) 231 | - dates\(\) and datetimes\(\) support [\#46](https://github.com/stphivos/django-mock-queries/pull/46) ([szykin](https://github.com/szykin)) 232 | - Feature/range requirements [\#45](https://github.com/stphivos/django-mock-queries/pull/45) ([stphivos](https://github.com/stphivos)) 233 | - Update model-mommy req [\#44](https://github.com/stphivos/django-mock-queries/pull/44) ([orf](https://github.com/orf)) 234 | 235 | ## [v0.0.16](https://github.com/stphivos/django-mock-queries/tree/v0.0.16) (2017-03-14) 236 | 237 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v0.0.15...v0.0.16) 238 | 239 | **Merged pull requests:** 240 | 241 | - Upload v0.0.16 to pypi [\#43](https://github.com/stphivos/django-mock-queries/pull/43) ([stphivos](https://github.com/stphivos)) 242 | - Comparisons: regex, iregex, date, datetime rework. [\#42](https://github.com/stphivos/django-mock-queries/pull/42) ([szykin](https://github.com/szykin)) 243 | - Fix one-to-many field lookup to use model name [\#41](https://github.com/stphivos/django-mock-queries/pull/41) ([stphivos](https://github.com/stphivos)) 244 | 245 | ## [v0.0.15](https://github.com/stphivos/django-mock-queries/tree/v0.0.15) (2017-03-06) 246 | 247 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v0.0.14...v0.0.15) 248 | 249 | **Merged pull requests:** 250 | 251 | - Update missing qs methods, lookups, aggregations. [\#39](https://github.com/stphivos/django-mock-queries/pull/39) ([stphivos](https://github.com/stphivos)) 252 | - Add support for nested fields to values and values\_list [\#38](https://github.com/stphivos/django-mock-queries/pull/38) ([stphivos](https://github.com/stphivos)) 253 | - Add support for aggregate on related fields [\#37](https://github.com/stphivos/django-mock-queries/pull/37) ([stphivos](https://github.com/stphivos)) 254 | - MockOptions, get\_or\_create\_with defaults [\#36](https://github.com/stphivos/django-mock-queries/pull/36) ([szykin](https://github.com/szykin)) 255 | - Add docs todo, remove completed decorators todo. [\#35](https://github.com/stphivos/django-mock-queries/pull/35) ([stphivos](https://github.com/stphivos)) 256 | - Add some quirky queries supported by Django: pk is in a subquery and child is equal. [\#34](https://github.com/stphivos/django-mock-queries/pull/34) ([donkirkby](https://github.com/donkirkby)) 257 | - Raise specific DoesNotExist exception for the model. [\#32](https://github.com/stphivos/django-mock-queries/pull/32) ([donkirkby](https://github.com/donkirkby)) 258 | - Add decorators for unified method patching/replacement [\#31](https://github.com/stphivos/django-mock-queries/pull/31) ([stphivos](https://github.com/stphivos)) 259 | - \_meta, values\(\), values\_list\(\) [\#30](https://github.com/stphivos/django-mock-queries/pull/30) ([szykin](https://github.com/szykin)) 260 | - Add mocked\_relations decorator for all related models. [\#28](https://github.com/stphivos/django-mock-queries/pull/28) ([donkirkby](https://github.com/donkirkby)) 261 | 262 | ## [v0.0.14](https://github.com/stphivos/django-mock-queries/tree/v0.0.14) (2016-12-15) 263 | 264 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/v0.0.13...v0.0.14) 265 | 266 | **Merged pull requests:** 267 | 268 | - Upload v0.0.14 to PyPI [\#25](https://github.com/stphivos/django-mock-queries/pull/25) ([stphivos](https://github.com/stphivos)) 269 | - Feature/aggregate multi params [\#24](https://github.com/stphivos/django-mock-queries/pull/24) ([szykin](https://github.com/szykin)) 270 | - Mock django db [\#23](https://github.com/stphivos/django-mock-queries/pull/23) ([donkirkby](https://github.com/donkirkby)) 271 | - django-rest-framework serializer assert function [\#5](https://github.com/stphivos/django-mock-queries/pull/5) ([stphivos](https://github.com/stphivos)) 272 | - Test remaining crud functions and exception scenarios [\#4](https://github.com/stphivos/django-mock-queries/pull/4) ([stphivos](https://github.com/stphivos)) 273 | - Test query aggregate, create and get functionality [\#3](https://github.com/stphivos/django-mock-queries/pull/3) ([stphivos](https://github.com/stphivos)) 274 | - Fix and test query filtering by q objects [\#2](https://github.com/stphivos/django-mock-queries/pull/2) ([stphivos](https://github.com/stphivos)) 275 | - Fix/remove pytest ini [\#1](https://github.com/stphivos/django-mock-queries/pull/1) ([stphivos](https://github.com/stphivos)) 276 | 277 | ## [v0.0.13](https://github.com/stphivos/django-mock-queries/tree/v0.0.13) (2016-06-08) 278 | 279 | [Full Changelog](https://github.com/stphivos/django-mock-queries/compare/7c9d6917856d495c15fcee3b058a8e3eecd267b2...v0.0.13) 280 | 281 | 282 | 283 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 284 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2016] [Phivos Stylianides] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.cfg README.md requirements/core.txt 2 | recursive-include django_mock_queries *.py 3 | recursive-include tests *.py 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Version](https://img.shields.io/pypi/v/django_mock_queries.svg)](https://pypi.python.org/pypi/django_mock_queries) 2 | [![Build Status](https://github.com/stphivos/django-mock-queries/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/stphivos/django-mock-queries/actions/workflows/ci.yml) 3 | [![Code Coverage](https://codecov.io/github/stphivos/django-mock-queries/coverage.svg?branch=master)](https://codecov.io/github/stphivos/django-mock-queries?branch=master) 4 | [![Code Climate](https://codeclimate.com/github/stphivos/django-mock-queries/badges/gpa.svg)](https://codeclimate.com/github/stphivos/django-mock-queries) 5 | 6 | # Django Mock Queries 7 | 8 | A library for mocking Django queryset functions in memory for testing 9 | 10 | ## Features 11 | 12 | * QuerySet style support for method chaining 13 | * Filtering with Q objects 14 | * Aggregates generation 15 | * CRUD functions 16 | * Field lookups 17 | * django-rest-framework serializer asserts 18 | 19 | ## Examples 20 | 21 | ```python 22 | from django.db.models import Avg, Q 23 | from django_mock_queries.query import MockSet, MockModel 24 | 25 | qs = MockSet( 26 | MockModel(mock_name='john', email='john@gmail.com'), 27 | MockModel(mock_name='jeff', email='jeff@hotmail.com'), 28 | MockModel(mock_name='bill', email='bill@gmail.com'), 29 | ) 30 | 31 | print([x for x in qs.all().filter(email__icontains='gmail.com').select_related('address')]) 32 | # Outputs: [john, bill] 33 | 34 | qs = MockSet( 35 | MockModel(mock_name='model s', msrp=70000), 36 | MockModel(mock_name='model x', msrp=80000), 37 | MockModel(mock_name='model 3', msrp=35000), 38 | ) 39 | 40 | print(qs.all().aggregate(Avg('msrp'))) 41 | # Outputs: {'msrp__avg': 61666} 42 | 43 | qs = MockSet( 44 | MockModel(mock_name='model x', make='tesla', country='usa'), 45 | MockModel(mock_name='s-class', make='mercedes', country='germany'), 46 | MockModel(mock_name='s90', make='volvo', country='sweden'), 47 | ) 48 | 49 | print([x for x in qs.all().filter(Q(make__iexact='tesla') | Q(country__iexact='germany'))]) 50 | # Outputs: [model x, s-class] 51 | 52 | qs = MockSet(cls=MockModel) 53 | print(qs.create(mock_name='my_object', foo='1', bar='a')) 54 | # Outputs: my_object 55 | 56 | print([x for x in qs]) 57 | # Outputs: [my_object] 58 | ``` 59 | 60 | ### Test function that uses Django QuerySet: 61 | 62 | ```python 63 | from unittest import TestCase 64 | from unittest.mock import patch 65 | from django_mock_queries.query import MockSet, MockModel 66 | 67 | 68 | class TestApi(TestCase): 69 | """ 70 | Test function applies expected filters by patching Django's user model Manager or Queryset with a MockSet. 71 | """ 72 | users = MockSet() 73 | user_objects = patch('django.contrib.auth.models.User.objects', users) 74 | 75 | def active_users(self): 76 | """ 77 | Query active users. 78 | """ 79 | return User.objects.filter(is_active=True).all() 80 | 81 | @user_objects 82 | def test_api_active_users_filters_by_is_active_true(self): 83 | self.users.add( 84 | MockModel(mock_name='active user', is_active=True), 85 | MockModel(mock_name='inactive user', is_active=False) 86 | ) 87 | 88 | for user in self.active_users(): 89 | self.assertTrue(user.is_active) 90 | ``` 91 | 92 | ### Test django-rest-framework model serializer: 93 | 94 | ```python 95 | class CarSerializer(serializers.ModelSerializer): 96 | """ 97 | Car model serializer that includes a nested serializer and a method field. 98 | """ 99 | make = ManufacturerSerializer() 100 | speed = serializers.SerializerMethodField() 101 | 102 | def get_speed(self, obj): 103 | return obj.format_speed() 104 | 105 | class Meta: 106 | model = Car 107 | fields = ('id', 'make', 'model', 'speed',) 108 | 109 | 110 | def test_car_serializer_fields(self): 111 | """ 112 | Test serializer returns fields with expected values and mock the result of nested serializer for field `make`. 113 | """ 114 | car = Car(id=1, make=Manufacturer(id=1, name='vw'), model='golf', speed=300) 115 | 116 | values = { 117 | 'id': car.id, 118 | 'model': car.model, 119 | 'speed': car.formatted_speed(), 120 | } 121 | 122 | assert_serializer(CarSerializer) \ 123 | .instance(car) \ 124 | .returns('id', 'make', 'model', 'speed') \ 125 | .values(**values) \ 126 | .mocks('make') \ 127 | .run() 128 | ``` 129 | 130 | ### Full Example 131 | 132 | There is a full Django application in the `examples/users` folder. It shows how 133 | to configure `django_mock_queries` in your tests and run them with or without 134 | setting up a Django database. Running the mock tests without a database can be 135 | much faster when your Django application has a lot of database migrations. 136 | 137 | To run your Django tests without a database, add a new settings file, and call 138 | `monkey_patch_test_db()`. Use a wildcard import to get all the regular settings 139 | as well. 140 | 141 | ```python 142 | # settings_mocked.py 143 | from django_mock_queries.mocks import monkey_patch_test_db 144 | 145 | from users.settings import * 146 | 147 | monkey_patch_test_db() 148 | ``` 149 | 150 | Then run your Django tests with the new settings file: 151 | 152 | ./manage.py test --settings=users.settings_mocked 153 | 154 | Here's the pytest equivalent: 155 | 156 | pytest --ds=users.settings_mocked 157 | 158 | That will run your tests without setting up a test database. All of your tests 159 | that use Django mock queries should run fine, but what about the tests that 160 | really need a database? 161 | 162 | ERROR: test_create (examples.users.analytics.tests.TestApi) 163 | ---------------------------------------------------------------------- 164 | Traceback (most recent call last): 165 | File "/.../examples/users/analytics/tests.py", line 28, in test_create 166 | start_count = User.objects.count() 167 | [...] 168 | NotSupportedError: Mock database tried to execute SQL for User model. 169 | 170 | If you want to run your tests without a database, you need to tell Django 171 | to skip the tests that need a database. You can do that by putting a skip 172 | decorator on the test classes or test methods that need a database. 173 | 174 | ```python 175 | @skipIfDBFeature('is_mocked') 176 | class TestApi(TestCase): 177 | def test_create(self): 178 | start_count = User.objects.count() 179 | 180 | User.objects.create(username='bob') 181 | final_count = User.objects.count() 182 | 183 | self.assertEqual(start_count + 1, final_count) 184 | ``` 185 | 186 | ## Installation 187 | 188 | ```bash 189 | pip install django_mock_queries 190 | ``` 191 | 192 | ## Contributing 193 | 194 | Anything missing or not functioning correctly? PRs are always welcome! Otherwise, you can create an issue so someone else does it when time allows. 195 | 196 | You can follow these guidelines: 197 | 198 | * Fork the repo from this page 199 | * Clone your fork: 200 | ```bash 201 | git clone https://github.com/{your-username}/django-mock-queries.git 202 | cd django-mock-queries 203 | git checkout -b feature/your_cool_feature 204 | ``` 205 | * Implement feature/fix 206 | * Add/modify relevant tests 207 | * Run tox to verify all tests and flake8 quality checks pass 208 | ```bash 209 | tox 210 | ``` 211 | * Commit and push local branch to your origin 212 | ```bash 213 | git commit . -m "New cool feature does this" 214 | git push -u origin HEAD 215 | ``` 216 | * Create pull request 217 | 218 | ## TODO 219 | 220 | * Add docs as a service like readthedocs with examples for every feature 221 | * Add support for missing QuerySet methods/Field lookups/Aggregation functions: 222 | * Methods that return new QuerySets: `annotate`, `reverse`, `none`, `extra`, `raw` 223 | * Methods that do not return QuerySets: `bulk_create`, `in_bulk`, `as_manager` 224 | * Field lookups: `search` 225 | * Aggregation functions: `StdDev`, `Variance` 226 | -------------------------------------------------------------------------------- /django_mock_queries/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stphivos/django-mock-queries/d4395d39c8e805bc2e44668be6ba4fdb86637f0f/django_mock_queries/__init__.py -------------------------------------------------------------------------------- /django_mock_queries/asserts.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, Mock 2 | 3 | from model_bakery import baker 4 | 5 | from .constants import * 6 | 7 | SkipField = locate('rest_framework.fields.SkipField') 8 | 9 | 10 | class SerializerAssert: 11 | _obj = None 12 | _serializer = None 13 | _return_fields = [] 14 | _mock_fields = [] 15 | _expected_values = {} 16 | 17 | def __init__(self, cls): 18 | self._cls = cls 19 | 20 | def _get_obj(self): 21 | obj = self._obj or baker.prepare(self._cls.Meta.model, _fill_optional=True) 22 | return obj 23 | 24 | def _get_attr(self, serializer, field): 25 | if field.field_name in self._expected_values: 26 | return self._expected_values[field.field_name] 27 | try: 28 | attribute = field.get_attribute(serializer.instance) 29 | 30 | if attribute is not None: 31 | attribute = field.to_representation(attribute) 32 | 33 | return attribute 34 | except SkipField: 35 | return SkipField 36 | 37 | def _get_values_patchers(self, serializer): 38 | values = {} 39 | patchers = [] 40 | 41 | for field in serializer._readable_fields: 42 | if field.field_name not in self._mock_fields: 43 | values[field.field_name] = self._get_attr(serializer, field) 44 | continue 45 | 46 | value = None 47 | values[field.field_name] = value 48 | 49 | patchers.append(patch.object(type(field), 'to_representation', Mock(return_value=value))) 50 | 51 | return values, patchers 52 | 53 | def _test_expected_fields(self, data, values): 54 | for field in self._return_fields: 55 | if field in values and values[field] == SkipField: 56 | continue 57 | 58 | assert field in data, \ 59 | 'Field {0} missing from serializer {1}.'.format(field, self._cls) 60 | 61 | assert data[field] == values[field], \ 62 | 'Field {0} equals {1}, expected {2}.'.format(field, data[field], values[field]) 63 | 64 | def _validate_args(self): 65 | for field in self._mock_fields: 66 | if field in self._expected_values: 67 | raise AttributeError('Cannot specify expected value for a mocked field ({0}.{1}).' 68 | .format(self._cls.Meta.model, field)) 69 | 70 | @property 71 | def serializer(self): 72 | if not self._serializer: 73 | obj = self._get_obj() 74 | self._serializer = self._cls(obj) 75 | return self._serializer 76 | 77 | def instance(self, obj): 78 | self._obj = obj 79 | return self 80 | 81 | def returns(self, *fields): 82 | self._return_fields = fields 83 | return self 84 | 85 | def mocks(self, *fields): 86 | self._mock_fields = fields 87 | return self 88 | 89 | def values(self, **attrs): 90 | self._expected_values = attrs 91 | return self 92 | 93 | def run(self): 94 | self._validate_args() 95 | 96 | values, patchers = self._get_values_patchers(self.serializer) 97 | 98 | try: 99 | for patcher in patchers: 100 | patcher.start() 101 | 102 | self._test_expected_fields(self.serializer.data, values) 103 | finally: 104 | for patcher in patchers: 105 | patcher.stop() 106 | 107 | 108 | def assert_serializer(cls): 109 | return SerializerAssert(cls) 110 | -------------------------------------------------------------------------------- /django_mock_queries/comparisons.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def exact_comparison(first, second): 5 | return first == second 6 | 7 | 8 | def iexact_comparison(first, second): 9 | return first.lower() == second.lower() 10 | 11 | 12 | def contains_comparison(first, second): 13 | if isinstance(first, (list, tuple)): 14 | return set(second).issubset(first) 15 | 16 | return second in first 17 | 18 | 19 | def icontains_comparison(first, second): 20 | return second.lower() in first.lower() 21 | 22 | 23 | def gt_comparison(first, second): 24 | return first > second if first is not None else False 25 | 26 | 27 | def gte_comparison(first, second): 28 | return first >= second if first is not None else False 29 | 30 | 31 | def lt_comparison(first, second): 32 | return first < second if first is not None else False 33 | 34 | 35 | def lte_comparison(first, second): 36 | return first <= second if first is not None else False 37 | 38 | 39 | def in_comparison(first, second): 40 | if isinstance(first, list): 41 | return bool(set(first).intersection(set(second))) 42 | 43 | return first in second if first is not None else False 44 | 45 | 46 | def startswith_comparison(first, second): 47 | return first.startswith(second) 48 | 49 | 50 | def istartswith_comparison(first, second): 51 | return first.lower().startswith(second.lower()) 52 | 53 | 54 | def endswith_comparison(first, second): 55 | return first.endswith(second) 56 | 57 | 58 | def iendswith_comparison(first, second): 59 | return first.lower().endswith(second.lower()) 60 | 61 | 62 | def isnull_comparison(first, second): 63 | return (first is None) == bool(second) 64 | 65 | 66 | def regex_comparison(first, second): 67 | return re.search(second, first) is not None 68 | 69 | 70 | def iregex_comparison(first, second): 71 | return re.search(second, first, flags=re.I) is not None 72 | 73 | 74 | def range_comparison(first, second): 75 | return second[0] <= first <= second[1] 76 | 77 | 78 | def overlap_comparison(first, second): 79 | return bool(set(first).intersection(set(second))) 80 | -------------------------------------------------------------------------------- /django_mock_queries/constants.py: -------------------------------------------------------------------------------- 1 | from pydoc import locate 2 | 3 | COMPARISON_EXACT = 'exact' 4 | COMPARISON_IEXACT = 'iexact' 5 | COMPARISON_CONTAINS = 'contains' 6 | COMPARISON_ICONTAINS = 'icontains' 7 | COMPARISON_GT = 'gt' 8 | COMPARISON_GTE = 'gte' 9 | COMPARISON_LT = 'lt' 10 | COMPARISON_LTE = 'lte' 11 | COMPARISON_IN = 'in' 12 | COMPARISON_STARTSWITH = 'startswith' 13 | COMPARISON_ISTARTSWITH = 'istartswith' 14 | COMPARISON_ENDSWITH = 'endswith' 15 | COMPARISON_IENDSWITH = 'iendswith' 16 | COMPARISON_ISNULL = 'isnull' 17 | COMPARISON_REGEX = 'regex' 18 | COMPARISON_IREGEX = 'iregex' 19 | COMPARISON_DATE = 'date' 20 | COMPARISON_YEAR = 'year' 21 | COMPARISON_MONTH = 'month' 22 | COMPARISON_DAY = 'day' 23 | COMPARISON_WEEK_DAY = 'week_day' 24 | COMPARISON_HOUR = 'hour' 25 | COMPARISON_MINUTE = 'minute' 26 | COMPARISON_SECOND = 'second' 27 | COMPARISON_RANGE = 'range' 28 | COMPARISON_OVERLAP = 'overlap' 29 | COMPARISONS = ( 30 | COMPARISON_EXACT, 31 | COMPARISON_IEXACT, 32 | COMPARISON_CONTAINS, 33 | COMPARISON_ICONTAINS, 34 | COMPARISON_GT, 35 | COMPARISON_GTE, 36 | COMPARISON_LT, 37 | COMPARISON_LTE, 38 | COMPARISON_IN, 39 | COMPARISON_STARTSWITH, 40 | COMPARISON_ISTARTSWITH, 41 | COMPARISON_ENDSWITH, 42 | COMPARISON_IENDSWITH, 43 | COMPARISON_ISNULL, 44 | COMPARISON_REGEX, 45 | COMPARISON_IREGEX, 46 | COMPARISON_RANGE, 47 | COMPARISON_OVERLAP, 48 | ) 49 | DATE_COMPARISONS = ( 50 | COMPARISON_DATE, 51 | COMPARISON_YEAR, 52 | COMPARISON_MONTH, 53 | COMPARISON_DAY, 54 | COMPARISON_WEEK_DAY, 55 | ) 56 | DATETIME_COMPARISONS = ( 57 | COMPARISON_DATE, 58 | COMPARISON_YEAR, 59 | COMPARISON_MONTH, 60 | COMPARISON_DAY, 61 | COMPARISON_WEEK_DAY, 62 | COMPARISON_HOUR, 63 | COMPARISON_MINUTE, 64 | COMPARISON_SECOND, 65 | ) 66 | 67 | MONTH_BOUNDS = (1, 12) 68 | DAY_BOUNDS = (1, 31) 69 | WEEK_DAY_BOUNDS = (1, 7) 70 | HOUR_BOUNDS = (0, 23) 71 | MINUTE_BOUNDS = (0, 59) 72 | SECOND_BOUNDS = (0, 59) 73 | 74 | CONNECTORS_OR = 'OR' 75 | CONNECTORS_AND = 'AND' 76 | CONNECTORS = ( 77 | CONNECTORS_OR, 78 | CONNECTORS_AND, 79 | ) 80 | 81 | AGGREGATES_SUM = 'SUM' 82 | AGGREGATES_COUNT = 'COUNT' 83 | AGGREGATES_MAX = 'MAX' 84 | AGGREGATES_MIN = 'MIN' 85 | AGGREGATES_AVG = 'AVG' 86 | AGGREGATES_ARRAY = 'ARRAY_AGG' 87 | AGGREGATES = ( 88 | AGGREGATES_SUM, 89 | AGGREGATES_COUNT, 90 | AGGREGATES_MAX, 91 | AGGREGATES_MIN, 92 | AGGREGATES_AVG, 93 | AGGREGATES_ARRAY, 94 | ) 95 | 96 | DjangoQ = locate('django.db.models.Q') 97 | DjangoQuerySet = locate('django.db.models.QuerySet') 98 | DjangoDbRouter = locate('django.db.router') 99 | DjangoModelDeletionCollector = locate('django.db.models.deletion.Collector') 100 | ObjectDoesNotExist = locate('django.core.exceptions.ObjectDoesNotExist') 101 | MultipleObjectsReturned = locate('django.core.exceptions.MultipleObjectsReturned') 102 | -------------------------------------------------------------------------------- /django_mock_queries/exceptions.py: -------------------------------------------------------------------------------- 1 | class ModelNotSpecified(Exception): 2 | pass 3 | 4 | 5 | class ArgumentNotSupported(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /django_mock_queries/mocks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import django 4 | import weakref 5 | from django.apps import apps 6 | from django.db import connections 7 | from django.db.backends.base import creation 8 | from django.db.models import Model 9 | from django.db.utils import ConnectionHandler, NotSupportedError 10 | from functools import partial 11 | from itertools import chain 12 | from unittest.mock import Mock, MagicMock, patch, PropertyMock 13 | 14 | from types import MethodType 15 | 16 | from .query import MockSet 17 | 18 | # noinspection PyUnresolvedReferences 19 | patch_object = patch.object 20 | 21 | 22 | def monkey_patch_test_db(disabled_features=None): 23 | """ Replace the real database connection with a mock one. 24 | 25 | This is useful for running Django tests without the cost of setting up a 26 | test database. 27 | Any database queries will raise a clear error, and the test database 28 | creation and tear down are skipped. 29 | Tests that require the real database should be decorated with 30 | @skipIfDBFeature('is_mocked') 31 | :param disabled_features: a list of strings that should be marked as 32 | *False* on the connection features list. All others will default 33 | to True. 34 | """ 35 | 36 | # noinspection PyUnusedLocal 37 | def create_mock_test_db(self, *args, **kwargs): 38 | mock_django_connection(disabled_features) 39 | 40 | # noinspection PyUnusedLocal 41 | def destroy_mock_test_db(self, *args, **kwargs): 42 | pass 43 | 44 | creation.BaseDatabaseCreation.create_test_db = create_mock_test_db 45 | creation.BaseDatabaseCreation.destroy_test_db = destroy_mock_test_db 46 | 47 | 48 | def mock_django_setup(settings_module, disabled_features=None): 49 | """ Must be called *AT IMPORT TIME* to pretend that Django is set up. 50 | 51 | This is useful for running tests without using the Django test runner. 52 | This must be called before any Django models are imported, or they will 53 | complain. Call this from a module in the calling project at import time, 54 | then be sure to import that module at the start of all mock test modules. 55 | Another option is to call it from the test package's init file, so it runs 56 | before all the test modules are imported. 57 | :param settings_module: the module name of the Django settings file, 58 | like 'myapp.settings' 59 | :param disabled_features: a list of strings that should be marked as 60 | *False* on the connection features list. All others will default 61 | to True. 62 | """ 63 | if apps.ready: 64 | # We're running in a real Django unit test, don't do anything. 65 | return 66 | 67 | if 'DJANGO_SETTINGS_MODULE' not in os.environ: 68 | os.environ['DJANGO_SETTINGS_MODULE'] = settings_module 69 | django.setup() 70 | mock_django_connection(disabled_features) 71 | 72 | 73 | def mock_django_connection(disabled_features=None): 74 | """ Overwrite the Django database configuration with a mocked version. 75 | 76 | This is a helper function that does the actual monkey patching. 77 | """ 78 | db = connections.databases['default'] 79 | db['PASSWORD'] = '****' 80 | db['USER'] = '**Database disabled for unit tests**' 81 | ConnectionHandler.__getitem__ = MagicMock(name='mock_connection') 82 | # noinspection PyUnresolvedReferences 83 | mock_connection = ConnectionHandler.__getitem__.return_value 84 | if disabled_features: 85 | for feature in disabled_features: 86 | setattr(mock_connection.features, feature, False) 87 | mock_ops = mock_connection.ops 88 | 89 | # noinspection PyUnusedLocal 90 | def compiler(queryset, using=None, connection=None, elide_empty=True, **kwargs): 91 | result = MagicMock(name='mock_connection.ops.compiler()') 92 | # noinspection PyProtectedMember 93 | result.execute_sql.side_effect = NotSupportedError( 94 | "Mock database tried to execute SQL for {} model.".format( 95 | queryset.model._meta.object_name)) 96 | result.has_results.side_effect = result.execute_sql.side_effect 97 | return result 98 | 99 | mock_ops.compiler.return_value.side_effect = compiler 100 | mock_ops.integer_field_range.return_value = (-sys.maxsize - 1, sys.maxsize) 101 | mock_ops.max_name_length.return_value = sys.maxsize 102 | 103 | Model.refresh_from_db = Mock() # Make this into a noop. 104 | 105 | 106 | class MockMap: 107 | def __init__(self, original): 108 | """ Wrap a mock mapping around the original one-to-many relation. """ 109 | self.map = {} 110 | self.original = original 111 | 112 | def __set__(self, instance, value): 113 | """ Set a related object for an instance. """ 114 | 115 | self.map[id(instance)] = (weakref.ref(instance), value) 116 | 117 | def __getattr__(self, name): 118 | """ Delegate all other calls to the original. """ 119 | 120 | return getattr(self.original, name) 121 | 122 | 123 | class MockOneToManyMap(MockMap): 124 | def __get__(self, instance, owner): 125 | """ Look in the map to see if there is a related set. 126 | 127 | If not, create a new set. 128 | """ 129 | 130 | if instance is None: 131 | # Call was to the class, not an object. 132 | return self 133 | 134 | instance_id = id(instance) 135 | entry = self.map.get(instance_id) 136 | old_instance = related_objects = None 137 | if entry is not None: 138 | old_instance_weak, related_objects = entry 139 | old_instance = old_instance_weak() 140 | if entry is None or old_instance is None: 141 | related = getattr(self.original, 'related', self.original) 142 | related_objects = MockSet(model=related.field.model) 143 | self.__set__(instance, related_objects) 144 | 145 | return related_objects 146 | 147 | 148 | class MockOneToOneMap(MockMap): 149 | def __get__(self, instance, owner): 150 | """ Look in the map to see if there is a related object. 151 | 152 | If not (the default) raise the expected exception. 153 | """ 154 | 155 | if instance is None: 156 | # Call was to the class, not an object. 157 | return self 158 | 159 | entry = self.map.get(id(instance)) 160 | old_instance = related_object = None 161 | if entry is not None: 162 | old_instance_weak, related_object = entry 163 | old_instance = old_instance_weak() 164 | if entry is None or old_instance is None: 165 | raise self.original.RelatedObjectDoesNotExist( 166 | "Mock %s has no %s." % ( 167 | owner.__name__, 168 | self.original.related.get_accessor_name() 169 | ) 170 | ) 171 | return related_object 172 | 173 | 174 | def find_all_models(models): 175 | """ Yield all models and their parents. """ 176 | for model in models: 177 | yield model 178 | # noinspection PyProtectedMember 179 | for parent in model._meta.parents.keys(): 180 | for parent_model in find_all_models((parent,)): 181 | yield parent_model 182 | 183 | 184 | def _patch_save(model, name): 185 | return patch_object( 186 | model, 187 | 'save', 188 | new_callable=partial(Mock, name=name + '.save') 189 | ) 190 | 191 | 192 | def _patch_objects(model, name): 193 | return patch_object( 194 | model, 'objects', 195 | new_callable=partial(MockSet, mock_name=name + '.objects', model=model) 196 | ) 197 | 198 | 199 | def _patch_relation(model, name, related_object): 200 | relation = getattr(model, name) 201 | 202 | if related_object.one_to_one: 203 | new_callable = partial(MockOneToOneMap, relation) 204 | else: 205 | new_callable = partial(MockOneToManyMap, relation) 206 | 207 | return patch_object(model, name, new_callable=new_callable) 208 | 209 | 210 | # noinspection PyProtectedMember 211 | def mocked_relations(*models): 212 | """ Mock all related field managers to make pure unit tests possible. 213 | 214 | The resulting patcher can be used just like one from the mock module: 215 | As a test method decorator, a test class decorator, a context manager, 216 | or by just calling start() and stop(). 217 | 218 | @mocked_relations(Dataset): 219 | def test_dataset(self): 220 | dataset = Dataset() 221 | check = dataset.content_checks.create() # returns a ContentCheck object 222 | """ 223 | patchers = [] 224 | 225 | for model in find_all_models(models): 226 | if isinstance(model.save, Mock): 227 | # already mocked, so skip it 228 | continue 229 | 230 | model_name = model._meta.object_name 231 | patchers.append(_patch_save(model, model_name)) 232 | 233 | if hasattr(model, 'objects'): 234 | patchers.append(_patch_objects(model, model_name)) 235 | 236 | for related_object in chain(model._meta.related_objects, 237 | model._meta.many_to_many): 238 | name = related_object.name 239 | 240 | if name not in model.__dict__ and related_object.one_to_many: 241 | name += '_set' 242 | 243 | if name in model.__dict__: 244 | # Only mock direct relations, not inherited ones. 245 | if getattr(model, name, None): 246 | patchers.append(_patch_relation( 247 | model, name, related_object 248 | )) 249 | 250 | return PatcherChain(patchers, pass_mocks=False) 251 | 252 | 253 | class PatcherChain: 254 | """ Chain a list of mock patchers into one. 255 | 256 | The resulting patcher can be used just like one from the mock module: 257 | As a test method decorator, a test class decorator, a context manager, 258 | or by just calling start() and stop(). 259 | """ 260 | 261 | def __init__(self, patchers, pass_mocks=True): 262 | """ Initialize a patcher. 263 | 264 | :param patchers: a list of patchers that should all be applied 265 | :param pass_mocks: True if any mock objects created by the patchers 266 | should be passed to any decorated test methods. 267 | """ 268 | self.patchers = patchers 269 | self.pass_mocks = pass_mocks 270 | 271 | def __call__(self, func): 272 | if isinstance(func, type): 273 | decorated = self.decorate_class(func) 274 | else: 275 | decorated = self.decorate_callable(func) 276 | # keep the previous class/function name 277 | decorated.__name__ = func.__name__ 278 | 279 | return decorated 280 | 281 | def decorate_class(self, cls): 282 | for attr in dir(cls): 283 | # noinspection PyUnresolvedReferences 284 | if not attr.startswith(patch.TEST_PREFIX): 285 | continue 286 | 287 | attr_value = getattr(cls, attr) 288 | if not hasattr(attr_value, "__call__"): 289 | continue 290 | 291 | setattr(cls, attr, self(attr_value)) 292 | return cls 293 | 294 | def decorate_callable(self, target): 295 | """ Called as a decorator. """ 296 | 297 | # noinspection PyUnusedLocal 298 | def absorb_mocks(test_case, *args): 299 | return target(test_case) 300 | 301 | should_absorb = not (self.pass_mocks or isinstance(target, type)) 302 | result = absorb_mocks if should_absorb else target 303 | for patcher in self.patchers: 304 | result = patcher(result) 305 | return result 306 | 307 | def __enter__(self): 308 | """ Starting a context manager. 309 | 310 | All the patched objects are passed as a list to the with statement. 311 | """ 312 | return [patcher.__enter__() for patcher in self.patchers] 313 | 314 | def __exit__(self, exc_type, exc_val, exc_tb): 315 | """ Ending a context manager. """ 316 | for patcher in self.patchers: 317 | patcher.__exit__(exc_type, exc_val, exc_tb) 318 | 319 | def start(self): 320 | return [patcher.start() for patcher in self.patchers] 321 | 322 | def stop(self): 323 | for patcher in reversed(self.patchers): 324 | patcher.stop() 325 | 326 | 327 | class Mocker: 328 | """ 329 | A decorator that patches multiple class methods with a magic mock instance that does nothing. 330 | """ 331 | 332 | def __init__(self, cls, *methods, **kwargs): 333 | self.cls = cls 334 | self.methods = methods 335 | 336 | self.inst_mocks = {} 337 | self.inst_patchers = {} 338 | self.inst_original = {} 339 | 340 | def __enter__(self): 341 | self._patch_object_methods(self.cls, *self.methods) 342 | return self 343 | 344 | def __call__(self, func): 345 | def decorated(*args, **kwargs): 346 | with self: 347 | return func(*((args[0], self) + args[1:]), **kwargs) 348 | 349 | # keep the previous method name 350 | decorated.__name__ = func.__name__ 351 | 352 | return decorated 353 | 354 | def __exit__(self, exc_type, exc_val, exc_tb): 355 | for patcher in self.inst_patchers.values(): 356 | patcher.stop() 357 | 358 | def _key(self, method, obj=None): 359 | return '{}.{}'.format(obj or self.cls, method) 360 | 361 | def _method_obj(self, name, obj, *sources): 362 | d = {} 363 | [d.update(s) for s in sources] 364 | return d[self._key(name, obj=obj)] 365 | 366 | def method(self, name, obj=None): 367 | return self._method_obj(name, obj, self.inst_mocks) 368 | 369 | def original_method(self, name, obj=None): 370 | return self._method_obj(name, obj, self.inst_original) 371 | 372 | def _get_source_method(self, obj, method): 373 | source_obj = obj 374 | parts = method.split('.') 375 | 376 | source_method = parts[-1] 377 | parts = parts[:-1] 378 | 379 | while parts: 380 | source_obj = getattr(source_obj, parts[0], None) or getattr(source_obj.model, '_' + parts[0]) 381 | parts.pop(0) 382 | 383 | return source_obj, source_method 384 | 385 | def _patch_method(self, method_name, source_obj, source_method): 386 | target_name = '_'.join(method_name.split('.')) 387 | target_obj = getattr(self, target_name, None) 388 | 389 | if target_obj is None: 390 | mock_args = dict(new=MagicMock()) 391 | elif type(target_obj) == MethodType: 392 | mock_args = dict(new=MagicMock(autospec=True, side_effect=target_obj)) 393 | else: 394 | mock_args = dict(new=PropertyMock(return_value=target_obj)) 395 | 396 | return patch_object(source_obj, source_method, **mock_args) 397 | 398 | def _patch_object_methods(self, obj, *methods, **kwargs): 399 | original, patchers, mocks = self.inst_original, self.inst_patchers, self.inst_mocks 400 | 401 | for method in methods: 402 | key = self._key(method, obj=obj) 403 | 404 | source_obj, source_method = self._get_source_method(obj, method) 405 | 406 | if key not in original: 407 | original[key] = getattr(source_obj, source_method) 408 | 409 | if key not in patchers: 410 | patcher = self._patch_method(method, source_obj, source_method) 411 | patchers[key] = patcher 412 | mocks[key] = patcher.start() 413 | 414 | 415 | class ModelMocker(Mocker): 416 | """ 417 | A decorator that patches django base model's db read/write methods and wires them to a MockSet. 418 | """ 419 | 420 | default_methods = ['objects', '_do_update', 'delete'] 421 | 422 | if django.VERSION[0] >= 3: 423 | default_methods += ['_base_manager._insert', ] 424 | else: 425 | default_methods += ['_meta.base_manager._insert', ] 426 | 427 | default_methods = tuple(default_methods) 428 | 429 | def __init__(self, cls, *methods, **kwargs): 430 | super(ModelMocker, self).__init__(cls, *(self.default_methods + methods), **kwargs) 431 | 432 | self.objects = MockSet(model=self.cls) 433 | self.objects.on('added', self._on_added) 434 | 435 | def __enter__(self): 436 | result = super(ModelMocker, self).__enter__() 437 | return result 438 | 439 | def _obj_pk(self, obj): 440 | return getattr(obj, self.cls._meta.pk.attname, None) 441 | 442 | def _on_added(self, obj): 443 | pk = max([self._obj_pk(x) or 0 for x in self.objects] + [0]) + 1 444 | setattr(obj, self.cls._meta.pk.attname, pk) 445 | 446 | def _meta_base_manager__insert(self, objects, *_, **__): 447 | obj = objects[0] 448 | self.objects.add(obj) 449 | 450 | return self._obj_pk(obj) 451 | 452 | def _base_manager__insert(self, objects, *_, **__): 453 | obj = objects[0] 454 | self.objects.add(obj) 455 | 456 | # Do not set anything on the model instance itself, as we do not get any values from the database. 457 | # The object ID is being set automatically. 458 | # Reference: `django.db.models.base.Model._save_table` 459 | return [] 460 | 461 | def _do_update(self, *args, **_): 462 | _, _, pk_val, values, _, _ = args 463 | objects = self.objects.filter(pk=pk_val) 464 | 465 | if objects.exists(): 466 | attrs = {field.attname: value for field, _, value in values if value is not None} 467 | self.objects.update(**attrs) 468 | return True 469 | else: 470 | return False 471 | 472 | def delete(self, *_args, **_kwargs): 473 | pk = self._obj_pk(self.objects[0]) 474 | if not pk: 475 | raise ValueError( 476 | f"{self.cls._meta.object_name} object can't be deleted because " 477 | f'its {self.cls._meta.pk.attname} attribute is set to None.' 478 | ) 479 | return self.objects.filter(pk=pk).delete() 480 | -------------------------------------------------------------------------------- /django_mock_queries/query.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | from collections import OrderedDict, defaultdict, namedtuple 4 | from unittest.mock import Mock, MagicMock, PropertyMock 5 | 6 | from .constants import * 7 | from .exceptions import * 8 | from .utils import ( 9 | matches, get_attribute, validate_mock_set, is_list_like_iter, flatten_list, truncate, 10 | hash_dict, filter_results, get_nested_attr 11 | ) 12 | 13 | 14 | class MockSetMeta(type): 15 | def __call__(cls, *initial_items, **kwargs): 16 | obj = super(MockSetMeta, cls).__call__(**kwargs) 17 | obj.add(*initial_items) 18 | return obj 19 | 20 | 21 | class MockSet(MagicMock, metaclass=MockSetMeta): 22 | EVENT_ADDED = 'added' 23 | EVENT_UPDATED = 'updated' 24 | EVENT_SAVED = 'saved' 25 | EVENT_DELETED = 'deleted' 26 | SUPPORTED_EVENTS = [EVENT_ADDED, EVENT_UPDATED, EVENT_SAVED, EVENT_DELETED] 27 | RETURN_SELF_METHODS = [ 28 | 'all', 29 | 'only', 30 | 'defer', 31 | 'using', 32 | 'select_related', 33 | 'prefetch_related', 34 | 'select_for_update', 35 | 'iterator' 36 | ] 37 | 38 | def __init__(self, *initial_items, **kwargs): 39 | clone = kwargs.pop('clone', None) 40 | model = kwargs.pop('model', None) 41 | 42 | for x in self.RETURN_SELF_METHODS: 43 | kwargs.update({x: self._return_self}) 44 | 45 | super(MockSet, self).__init__(spec=DjangoQuerySet, **kwargs) 46 | 47 | self.items = list() 48 | self.clone = clone 49 | self.model = getattr(clone, 'model', model) 50 | self.events = {} 51 | 52 | self.add(*initial_items) 53 | 54 | self.__len__ = lambda s: len(s.items) 55 | self.__iter__ = lambda s: iter(s.items) 56 | self.__getitem__ = lambda s, k: self.items[k] 57 | self.__bool__ = self.__nonzero__ = lambda s: len(s.items) > 0 58 | 59 | def _return_self(self, *_, **__): 60 | return self 61 | 62 | def _mockset_class(self): 63 | return type(self) 64 | 65 | def count(self): 66 | return len(self.items) 67 | 68 | def fire(self, obj, *events): 69 | for name in events: 70 | for handler in self.events.get(name, []): 71 | handler(obj) 72 | 73 | def on(self, event, handler): 74 | assert event in self.SUPPORTED_EVENTS, event 75 | self.events[event] = self.events.get(event, []) + [handler] 76 | 77 | def _register_fields(self, obj): 78 | if not (isinstance(obj, MockModel) or isinstance(obj, Mock)): 79 | return 80 | 81 | for f in self.model._meta.fields: 82 | if f.name not in obj.keys(): 83 | setattr(obj, f.name, None) 84 | 85 | def add(self, *models): 86 | if self.model: 87 | # Initialize MockModel default fields from MockSet model fields if defined 88 | for obj in models: 89 | self._register_fields(obj) 90 | 91 | for model in models: 92 | self.items.append(model) 93 | self.fire(model, self.EVENT_ADDED, self.EVENT_SAVED) 94 | 95 | def filter(self, *args, **attrs): 96 | results = list(self.items) 97 | for x in args: 98 | if not isinstance(x, DjangoQ): 99 | raise ArgumentNotSupported() 100 | 101 | if len(x) > 0: 102 | results = filter_results(results, x) 103 | 104 | return self._mockset_class()(*matches(*results, **attrs), clone=self) 105 | 106 | def exclude(self, *args, **attrs): 107 | excluded = set(self.filter(*args, **attrs)) 108 | results = [item for item in self.items if item not in excluded] 109 | return self._mockset_class()(*results, clone=self) 110 | 111 | def exists(self): 112 | return len(self.items) > 0 113 | 114 | def in_bulk(self, id_list=None, *, field_name='pk'): 115 | result = {} 116 | for model in self.items: 117 | if id_list is None or getattr(model, field_name) in id_list: 118 | result[getattr(model, field_name)] = model 119 | return result 120 | 121 | def annotate(self, **kwargs): 122 | results = list(self.items) 123 | for key, value in kwargs.items(): 124 | for row in results: 125 | if not (hasattr(row, '_annotated_fields') and isinstance(row._annotated_fields, list)): 126 | row._annotated_fields = [] 127 | row._annotated_fields.append(key) 128 | setattr(row, key, get_attribute(row, value)[0]) 129 | 130 | return self._mockset_class()(*results, clone=self) 131 | 132 | def aggregate(self, *args, **kwargs): 133 | result = {} 134 | 135 | for expr in set(args): 136 | kwargs['{0}__{1}'.format(expr.source_expressions[0].name, expr.function).lower()] = expr 137 | 138 | for alias, expr in kwargs.items(): 139 | values = [] 140 | expr_result = None 141 | 142 | for x in self.items: 143 | val = get_attribute(x, expr.source_expressions[0].name)[0] 144 | if val is None: 145 | continue 146 | values.extend(val if is_list_like_iter(val) else [val]) 147 | 148 | if len(values) > 0: 149 | expr_result = { 150 | AGGREGATES_SUM: lambda: sum(values), 151 | AGGREGATES_COUNT: lambda: len(values), 152 | AGGREGATES_MAX: lambda: max(values), 153 | AGGREGATES_MIN: lambda: min(values), 154 | AGGREGATES_AVG: lambda: sum(values) / len(values), 155 | AGGREGATES_ARRAY: lambda: values, 156 | }[expr.function]() 157 | 158 | if len(values) == 0 and expr.function == AGGREGATES_COUNT: 159 | expr_result = 0 160 | 161 | result[alias] = expr_result 162 | 163 | return result 164 | 165 | def order_by(self, *fields): 166 | results = self.items 167 | for field in reversed(fields): 168 | if field == '?': 169 | random.shuffle(results) 170 | break 171 | is_reversed = field.startswith('-') 172 | attr = field[1:] if is_reversed else field 173 | results = sorted(results, 174 | key=lambda r: get_attribute(r, attr), 175 | reverse=is_reversed) 176 | return self._mockset_class()(*results, clone=self, ordered=True) 177 | 178 | def distinct(self, *fields): 179 | results = OrderedDict() 180 | for item in self.items: 181 | key = hash_dict(item, *fields) 182 | if key not in results: 183 | results[key] = item 184 | return self._mockset_class()(*results.values(), clone=self) 185 | 186 | def set(self, objs, **attrs): 187 | self.delete(**attrs) 188 | self.add(*objs) 189 | 190 | def _raise_does_not_exist(self): 191 | does_not_exist = getattr(self.model, 'DoesNotExist', ObjectDoesNotExist) 192 | raise does_not_exist() 193 | 194 | def _get_order_fields(self, fields, field_name): 195 | if fields and field_name is not None: 196 | raise ValueError('Cannot use both positional arguments and the field_name keyword argument.') 197 | 198 | if field_name is not None: 199 | # The field_name keyword argument is deprecated in favor of passing positional arguments. 200 | order_fields = (field_name,) 201 | elif fields: 202 | order_fields = fields 203 | else: 204 | order_fields = self.model._meta.get_latest_by 205 | if order_fields and not isinstance(order_fields, (tuple, list)): 206 | order_fields = (order_fields,) 207 | 208 | if order_fields is None: 209 | raise ValueError( 210 | "earliest() and latest() require either fields as positional " 211 | "arguments or 'get_latest_by' in the model's Meta." 212 | ) 213 | 214 | return order_fields 215 | 216 | def _earliest_or_latest(self, *fields, **field_kwargs): 217 | """ 218 | Mimic Django's behavior 219 | https://github.com/django/django/blob/746caf3ef821dbf7588797cb2600fa81b9df9d1d/django/db/models/query.py#L560 220 | """ 221 | field_name = field_kwargs.get('field_name', None) 222 | reverse = field_kwargs.get('reverse', False) 223 | order_fields = self._get_order_fields(fields, field_name) 224 | 225 | results = sorted( 226 | self.items, 227 | key=lambda obj: tuple(get_attribute(obj, key) for key in order_fields), 228 | reverse=reverse, 229 | ) 230 | 231 | if len(results) == 0: 232 | self._raise_does_not_exist() 233 | 234 | return results[0] 235 | 236 | def earliest(self, *fields, **field_kwargs): 237 | return self._earliest_or_latest(*fields, **field_kwargs) 238 | 239 | def latest(self, *fields, **field_kwargs): 240 | return self._earliest_or_latest(*fields, reverse=True, **field_kwargs) 241 | 242 | def first(self): 243 | for item in self.items: 244 | return item 245 | 246 | def last(self): 247 | return self.items and self.items[-1] or None 248 | 249 | def create(self, **attrs): 250 | validate_mock_set(self, **attrs) 251 | 252 | obj = self.model(**attrs) 253 | self.add(obj) 254 | 255 | return obj 256 | 257 | def update(self, **attrs): 258 | validate_mock_set(self, for_update=True, **attrs) 259 | 260 | count = 0 261 | for item in self.items: 262 | count += 1 263 | for k, v in attrs.items(): 264 | setattr(item, k, v) 265 | self.fire(item, self.EVENT_UPDATED, self.EVENT_SAVED) 266 | 267 | return count 268 | 269 | def _delete_recursive(self, *items_to_remove, **attrs): 270 | removed_items = defaultdict(int) 271 | 272 | for item in matches(*items_to_remove, **attrs): 273 | self.items.remove(item) 274 | self.fire(item, self.EVENT_DELETED) 275 | 276 | # Support returning detailed information about removed items 277 | # even if items are not `MockModel` instances 278 | item_label = get_nested_attr(item, '_meta.label', default=type(item).__name__) 279 | removed_items[item_label] += 1 280 | 281 | if self.clone is not None: 282 | self.clone._delete_recursive(*items_to_remove, **attrs) 283 | 284 | return sum(removed_items.values()), removed_items 285 | 286 | def delete(self, **attrs): 287 | # Delete normally doesn't take **attrs - they're only needed for remove 288 | return self._delete_recursive(*self.items, **attrs) 289 | 290 | # The following 2 methods were kept for backwards compatibility and 291 | # should be removed in the future since they are covered by filter & delete 292 | def clear(self, **attrs): 293 | return self.delete(**attrs) 294 | 295 | def remove(self, **attrs): 296 | return self.delete(**attrs) 297 | 298 | def get(self, *args, **attrs): 299 | results = self.filter(*args, **attrs) 300 | if not results.exists(): 301 | self._raise_does_not_exist() 302 | elif results.count() > 1: 303 | raise MultipleObjectsReturned() 304 | else: 305 | return results[0] 306 | 307 | def get_or_create(self, defaults=None, **attrs): 308 | if defaults is not None: 309 | validate_mock_set(self) 310 | defaults = defaults or {} 311 | lookup = attrs.copy() 312 | attrs.update(defaults) 313 | results = self.filter(**lookup) 314 | if not results.exists(): 315 | return self.create(**attrs), True 316 | elif results.count() > 1: 317 | raise MultipleObjectsReturned() 318 | else: 319 | return results[0], False 320 | 321 | def update_or_create(self, defaults=None, **attrs): 322 | if defaults is not None: 323 | validate_mock_set(self) 324 | defaults = defaults or {} 325 | lookup = attrs.copy() 326 | attrs.update(defaults) 327 | results = self.filter(**lookup) 328 | if not results.exists(): 329 | return self.create(**attrs), True 330 | elif results.count() > 1: 331 | raise MultipleObjectsReturned() 332 | else: 333 | obj = results[0] 334 | for k, v in attrs.items(): 335 | setattr(obj, k, v) 336 | self.fire(obj, self.EVENT_UPDATED, self.EVENT_SAVED) 337 | return obj, False 338 | 339 | def _item_values(self, item, fields): 340 | field_buckets = {} 341 | result_count = 1 342 | 343 | if len(fields) == 0: 344 | field_names = [f.attname for f in item._meta.concrete_fields] 345 | else: 346 | field_names = list(fields) 347 | 348 | for field in sorted(field_names, key=lambda k: k.count('__')): 349 | value = get_attribute(item, field)[0] 350 | 351 | if is_list_like_iter(value): 352 | value = flatten_list(value) 353 | result_count = max(result_count, len(value)) 354 | 355 | for bucket, data in field_buckets.items(): 356 | while len(data) < result_count: 357 | data.append(data[-1]) 358 | 359 | field_buckets[field] = value 360 | else: 361 | field_buckets[field] = [value] 362 | 363 | item_values = [] 364 | for i in range(result_count): 365 | item_values.append({k: v[i] for k, v in field_buckets.items()}) 366 | 367 | return item_values 368 | 369 | def values(self, *fields): 370 | result = [] 371 | 372 | for item in self.items: 373 | item_values = self._item_values(item, fields) 374 | result.extend(item_values) 375 | 376 | return self._mockset_class()(*result, clone=self) 377 | 378 | def _item_values_list(self, values_dict, fields, flat): 379 | if flat: 380 | return values_dict[fields[0]] 381 | else: 382 | data = [] 383 | for key in sorted(values_dict.keys(), key=lambda k: fields.index(k)): 384 | data.append(values_dict[key]) 385 | return tuple(data) 386 | 387 | def _values_row(self, values_dict, fields, **kwargs): 388 | flat = kwargs.pop('flat', False) 389 | named = kwargs.pop('named', False) 390 | 391 | if kwargs: 392 | raise TypeError('Unexpected keyword arguments to values_list: %s' % (list(kwargs),)) 393 | if flat and len(fields) > 1: 394 | raise TypeError('`flat` is not valid when values_list is called with more than one field.') 395 | if flat and named: 396 | raise TypeError('`flat` and `named` can\'t be used together.') 397 | 398 | if named: 399 | Row = namedtuple('Row', fields) 400 | row = Row(**values_dict) 401 | else: 402 | row = self._item_values_list(values_dict, fields, flat) 403 | 404 | return row 405 | 406 | def values_list(self, *fields, **kwargs): 407 | # Django doesn't complain about this: 408 | # https://github.com/django/django/blob/a4e6030904df63b3f10aa0729b86dc6942b0458e/django/db/models/query.py#L845 409 | # if len(fields) == 0: 410 | # raise NotImplementedError('values_list() with no arguments is not implemented') 411 | 412 | result = [] 413 | item_values_dicts = list(self.values(*fields)) 414 | 415 | for values_dict in item_values_dicts: 416 | result.append(self._values_row(values_dict, fields, **kwargs)) 417 | 418 | return self._mockset_class()(*result, clone=self) 419 | 420 | def _date_values(self, field, kind, order, key_func): 421 | initial_values = list(self.values_list(field, flat=True)) 422 | 423 | return self._mockset_class()(*sorted( 424 | {truncate(x, kind) for x in initial_values}, 425 | key=key_func, 426 | reverse=True if order == 'DESC' else False 427 | ), clone=self) 428 | 429 | def dates(self, field, kind, order='ASC'): 430 | assert kind in ("year", "month", "day"), "'kind' must be one of 'year', 'month' or 'day'." 431 | assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'." 432 | 433 | return self._date_values(field, kind, order, lambda y: datetime.date.timetuple(y)[:3]) 434 | 435 | def datetimes(self, field, kind, order='ASC'): 436 | # TODO: Handle `tzinfo` parameter 437 | assert kind in ("year", "month", "day", "hour", "minute", "second"), \ 438 | "'kind' must be one of 'year', 'month', 'day', 'hour', 'minute' or 'second'." 439 | assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'." 440 | 441 | return self._date_values(field, kind, order, lambda y: datetime.datetime.timetuple(y)[:6]) 442 | 443 | 444 | class MockModel(dict): 445 | def __init__(self, *args, **kwargs): 446 | super(MockModel, self).__init__(*args, **kwargs) 447 | 448 | self.save = PropertyMock() 449 | object_name = self.get('mock_name', type(self).__name__) 450 | self.__meta = MockOptions(object_name, *self.get_fields()) 451 | 452 | def __getattr__(self, item): 453 | return self.get(item, None) 454 | 455 | def __setattr__(self, key, value): 456 | self.__setitem__(key, value) 457 | 458 | def __hash__(self): 459 | return hash_dict(self) 460 | 461 | def __call__(self, *args, **kwargs): 462 | return MockModel(*args, **kwargs) 463 | 464 | def get_fields(self): 465 | skip_keys = ['save', '_MockModel__meta'] 466 | return [key for key in self.keys() if key not in skip_keys] 467 | 468 | @property 469 | def _meta(self): 470 | self.__meta.load_fields(*self.get_fields()) 471 | return self.__meta 472 | 473 | def __repr__(self): 474 | return self.get('mock_name', None) or super(MockModel, self).__repr__() 475 | 476 | 477 | def create_model(*fields): 478 | if len(fields) == 0: 479 | raise ValueError('create_model() is called without fields specified') 480 | return MockModel(**{f: None for f in fields}) 481 | 482 | 483 | class MockOptions: 484 | def __init__(self, object_name, *field_names): 485 | self.load_fields(*field_names) 486 | self.get_latest_by = None 487 | self.object_name = object_name 488 | self.model_name = object_name.lower() 489 | 490 | @property 491 | def label(self): 492 | return self.object_name 493 | 494 | @property 495 | def label_lower(self): 496 | return self.model_name 497 | 498 | def load_fields(self, *field_names): 499 | fields = {name: MockField(name) for name in field_names} 500 | 501 | for key in ('_forward_fields_map', 'parents', 'fields_map'): 502 | self.__dict__[key] = {} 503 | 504 | if key == '_forward_fields_map': 505 | for name, obj in fields.items(): 506 | self.__dict__[key][name] = obj 507 | 508 | for key in ('local_concrete_fields', 'concrete_fields', 'fields'): 509 | self.__dict__[key] = [] 510 | 511 | for name, obj in fields.items(): 512 | self.__dict__[key].append(obj) 513 | 514 | 515 | class MockField: 516 | def __init__(self, field): 517 | for key in ('name', 'attname'): 518 | self.__dict__[key] = field 519 | -------------------------------------------------------------------------------- /django_mock_queries/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, date 2 | from django.core.exceptions import FieldError 3 | from django.db.models import F, Value, Case 4 | from django.db.models.functions import Coalesce 5 | from unittest.mock import Mock 6 | 7 | from .comparisons import * 8 | from .constants import * 9 | from .exceptions import * 10 | 11 | import django_mock_queries.query 12 | 13 | 14 | def merge(first, second): 15 | return first + list(set(second) - set(first)) 16 | 17 | 18 | def intersect(first, second): 19 | return list(set(first).intersection(second)) 20 | 21 | 22 | def get_field_mapping(field): 23 | name = field.get_accessor_name() 24 | model_name = field.related_model._meta.model_name.lower() 25 | 26 | if name[-4:] == '_set': 27 | return {model_name: name} 28 | else: 29 | return {name: name} 30 | 31 | 32 | def find_field_names_from_meta(meta, annotated=None, **kwargs): 33 | field_names = {} 34 | annotated = annotated or [] 35 | concrete_only = kwargs.get('concrete_only', False) 36 | 37 | if concrete_only: 38 | fields_no_mapping = [f.attname for f in meta.concrete_fields] + annotated 39 | fields_with_mapping = [] 40 | else: 41 | fields_no_mapping = [f for f in meta._forward_fields_map.keys()] + annotated 42 | fields_with_mapping = [f for f in meta.fields_map.values()] 43 | 44 | for parent in meta.parents.keys(): 45 | fields_no_mapping.extend([key for key in find_field_names(parent)[0]]) 46 | 47 | for field in fields_no_mapping: 48 | field_names[field] = field 49 | 50 | for field in fields_with_mapping: 51 | field_names.update(get_field_mapping(field)) 52 | 53 | return list(field_names.keys()), list(field_names.values()) 54 | 55 | 56 | def find_field_names_from_obj(obj, **kwargs): 57 | lookup_fields, actual_fields = [], [] 58 | 59 | if type(obj) is dict: 60 | lookup_fields = actual_fields = list(obj.keys()) 61 | else: 62 | # It is possibly a MockSet. 63 | use_obj = getattr(obj, 'model', None) 64 | 65 | # Make it easier for MockSet, but Django's QuerySet will always have a model. 66 | if not use_obj and is_list_like_iter(obj) and len(obj) > 0: 67 | lookup_fields, actual_fields = find_field_names(obj[0], **kwargs) 68 | 69 | return lookup_fields, actual_fields 70 | 71 | 72 | def find_field_names(obj, **kwargs): 73 | if hasattr(obj, '_meta'): 74 | lookup_fields, actual_fields = find_field_names_from_meta( 75 | obj._meta, 76 | annotated=getattr(obj, '_annotated_fields', []), 77 | **kwargs 78 | ) 79 | else: 80 | lookup_fields, actual_fields = find_field_names_from_obj(obj, **kwargs) 81 | 82 | return lookup_fields, actual_fields 83 | 84 | 85 | def validate_field(field_name, model_fields, for_update=False): 86 | if '__' in field_name and for_update: 87 | raise FieldError( 88 | 'Cannot update model field %r (only non-relations and foreign keys permitted).' % field_name 89 | ) 90 | if field_name != 'pk' and field_name not in model_fields: 91 | message = "Cannot resolve keyword '{}' into field. Choices are {}.".format( 92 | field_name, 93 | ', '.join(map(repr, map(str, sorted(model_fields)))) 94 | ) 95 | raise FieldError(message) 96 | 97 | 98 | def get_field_value(obj, field_name, default=None): 99 | if type(obj) is dict: 100 | return obj.get(field_name, default) 101 | elif is_list_like_iter(obj): 102 | return [get_attribute(x, field_name, default)[0] for x in obj] 103 | elif is_like_date_or_datetime(obj): 104 | return obj 105 | else: 106 | return getattr(obj, field_name, default) 107 | 108 | 109 | def get_attribute(obj, attr, default=None): 110 | result = obj 111 | comparison = None 112 | if isinstance(attr, F): 113 | attr = attr.deconstruct()[1][0] 114 | elif isinstance(attr, Value): 115 | return attr.value, None 116 | elif isinstance(attr, Case): 117 | for case in attr.cases: 118 | if filter_results([obj], case.condition): 119 | return get_attribute(obj, case.result) 120 | else: 121 | return get_attribute(obj, attr.default) 122 | elif isinstance(attr, Coalesce): 123 | for expr in attr.source_expressions: 124 | res, comp = get_attribute(obj, expr) 125 | if res is not None: 126 | return res, comp 127 | parts = attr.split('__') 128 | 129 | for i, attr_part in enumerate(parts): 130 | if attr_part in COMPARISONS: 131 | comparison = attr_part 132 | elif attr_part in DATETIME_COMPARISONS and type(result) in [date, datetime]: 133 | comparison_type = parts[i + 1] if i + 1 < len(parts) else COMPARISON_EXACT 134 | comparison = (attr_part, comparison_type) 135 | break 136 | elif result is None: 137 | result = default 138 | break 139 | else: 140 | lookup_fields, actual_fields = find_field_names(result) 141 | 142 | if lookup_fields: 143 | validate_field(attr_part, lookup_fields) 144 | 145 | field = actual_fields[lookup_fields.index(attr_part)] if attr_part in lookup_fields else attr_part 146 | result = get_field_value(result, field, default) 147 | return result, comparison 148 | 149 | 150 | def is_match(first, second, comparison=None): 151 | if isinstance(first, django_mock_queries.query.MockSet): 152 | return is_match_in_children(comparison, first, second) 153 | if (isinstance(first, (int, str)) and isinstance(second, django_mock_queries.query.MockSet)): 154 | second = convert_to_pks(second) 155 | if (isinstance(first, date) or isinstance(first, datetime)) \ 156 | and isinstance(comparison, tuple) and len(comparison) == 2: 157 | first = extract(first, comparison[0]) 158 | comparison = comparison[1] 159 | if not comparison: 160 | return first == second 161 | return { 162 | COMPARISON_EXACT: exact_comparison, 163 | COMPARISON_IEXACT: iexact_comparison, 164 | COMPARISON_CONTAINS: contains_comparison, 165 | COMPARISON_ICONTAINS: icontains_comparison, 166 | COMPARISON_GT: gt_comparison, 167 | COMPARISON_GTE: gte_comparison, 168 | COMPARISON_LT: lt_comparison, 169 | COMPARISON_LTE: lte_comparison, 170 | COMPARISON_IN: in_comparison, 171 | COMPARISON_STARTSWITH: startswith_comparison, 172 | COMPARISON_ISTARTSWITH: istartswith_comparison, 173 | COMPARISON_ENDSWITH: endswith_comparison, 174 | COMPARISON_IENDSWITH: iendswith_comparison, 175 | COMPARISON_ISNULL: isnull_comparison, 176 | COMPARISON_REGEX: regex_comparison, 177 | COMPARISON_IREGEX: iregex_comparison, 178 | COMPARISON_RANGE: range_comparison, 179 | COMPARISON_OVERLAP: overlap_comparison, 180 | }[comparison](first, second) 181 | 182 | 183 | def extract(obj, comparison): 184 | result_dict = None 185 | if isinstance(obj, date): 186 | result_dict = { 187 | COMPARISON_DATE: obj, 188 | COMPARISON_YEAR: obj.year, 189 | COMPARISON_MONTH: obj.month, 190 | COMPARISON_DAY: obj.day, 191 | COMPARISON_WEEK_DAY: (obj.weekday() + 1) % 7 + 1, 192 | } 193 | if isinstance(obj, datetime): 194 | result_dict = { 195 | COMPARISON_DATE: obj.date(), 196 | COMPARISON_YEAR: obj.year, 197 | COMPARISON_MONTH: obj.month, 198 | COMPARISON_DAY: obj.day, 199 | COMPARISON_WEEK_DAY: (obj.weekday() + 1) % 7 + 1, 200 | COMPARISON_HOUR: obj.hour, 201 | COMPARISON_MINUTE: obj.minute, 202 | COMPARISON_SECOND: obj.second, 203 | } 204 | return result_dict[comparison] 205 | 206 | 207 | def convert_to_pks(query): 208 | try: 209 | return [item.pk for item in query] 210 | except AttributeError: 211 | return query # Didn't have pk's, keep original items 212 | 213 | 214 | def is_match_in_children(comparison, first, second): 215 | return any(is_match(item, second, comparison) 216 | for item in first) 217 | 218 | 219 | def is_disqualified(obj, attrs, negated): 220 | for attr_name, filter_value in attrs.items(): 221 | attr_value, comparison = get_attribute(obj, attr_name) 222 | match = is_match(attr_value, filter_value, comparison) 223 | 224 | if (match and negated) or (not match and not negated): 225 | return True 226 | 227 | return False 228 | 229 | 230 | def matches(*source, **attrs): 231 | negated = attrs.pop('negated', False) 232 | disqualified = [x for x in source if is_disqualified(x, attrs, negated)] 233 | 234 | return [x for x in source if x not in disqualified] 235 | 236 | 237 | def validate_mock_set(mock_set, for_update=False, **fields): 238 | if mock_set.model is None: 239 | raise ModelNotSpecified() 240 | 241 | _, actual_fields = find_field_names(mock_set.model) 242 | 243 | for k in fields.keys(): 244 | validate_field(k, actual_fields, for_update) 245 | 246 | 247 | def validate_date_or_datetime(value, comparison): 248 | mapping = { 249 | COMPARISON_YEAR: lambda: True, 250 | COMPARISON_MONTH: lambda: MONTH_BOUNDS[0] <= value <= MONTH_BOUNDS[1], 251 | COMPARISON_DAY: lambda: DAY_BOUNDS[0] <= value <= DAY_BOUNDS[1], 252 | COMPARISON_WEEK_DAY: lambda: WEEK_DAY_BOUNDS[0] <= value <= WEEK_DAY_BOUNDS[1], 253 | COMPARISON_HOUR: lambda: HOUR_BOUNDS[0] <= value <= HOUR_BOUNDS[1], 254 | COMPARISON_MINUTE: lambda: MINUTE_BOUNDS[0] <= value <= MINUTE_BOUNDS[1], 255 | COMPARISON_SECOND: lambda: SECOND_BOUNDS[0] <= value <= SECOND_BOUNDS[1], 256 | } 257 | if not mapping[comparison](): 258 | raise ValueError('{} is incorrect value for {}'.format(value, comparison)) 259 | 260 | 261 | def is_list_like_iter(obj): 262 | if isinstance(obj, django_mock_queries.query.MockModel): 263 | return False 264 | elif isinstance(obj, django_mock_queries.query.MockSet): 265 | return True 266 | elif isinstance(obj, Mock): 267 | return False 268 | 269 | return hasattr(obj, '__iter__') and not isinstance(obj, str) 270 | 271 | 272 | def is_like_date_or_datetime(obj): 273 | return type(obj) in [date, datetime] 274 | 275 | 276 | def flatten_list(source): 277 | target = [] 278 | for x in source: 279 | if not is_list_like_iter(x): 280 | target.append(x) 281 | else: 282 | target.extend(flatten_list(x)) 283 | return target 284 | 285 | 286 | def truncate(obj, kind): 287 | trunc_mapping = None 288 | if isinstance(obj, date): 289 | trunc_mapping = { 290 | 'year': obj.replace(month=1, day=1), 291 | 'month': obj.replace(day=1), 292 | 'day': obj 293 | } 294 | if isinstance(obj, datetime): 295 | trunc_mapping = { 296 | 'year': obj.replace(month=1, day=1, hour=0, minute=0, second=0), 297 | 'month': obj.replace(day=1, hour=0, minute=0, second=0), 298 | 'day': obj.replace(hour=0, minute=0, second=0), 299 | 'hour': obj.replace(minute=0, second=0), 300 | 'minute': obj.replace(second=0), 301 | 'second': obj 302 | } 303 | return trunc_mapping[kind] 304 | 305 | 306 | def hash_dict(obj, *fields): 307 | field_names = fields or find_field_names(obj, concrete_only=True)[1] 308 | obj_values = {f: get_field_value(obj, f) for f in field_names} 309 | 310 | return hash(tuple(sorted((k, v) for k, v in obj_values.items() if not fields or k in fields))) 311 | 312 | 313 | def filter_results(source, query): 314 | results = [] 315 | 316 | for child in query.children: 317 | filtered = _filter_single_q(source, child, query.negated) 318 | 319 | if filtered: 320 | if not results or query.connector == CONNECTORS_OR: 321 | results = merge(results, filtered) 322 | else: 323 | results = intersect(results, filtered) 324 | elif query.connector == CONNECTORS_AND: 325 | return [] 326 | 327 | return results 328 | 329 | 330 | def _filter_single_q(source, q_obj, negated): 331 | if isinstance(q_obj, DjangoQ): 332 | return filter_results(source, q_obj) 333 | else: 334 | return matches(negated=negated, *source, **{q_obj[0]: q_obj[1]}) 335 | 336 | 337 | def get_nested_attr(obj, attr_path, default=None): 338 | attrs = attr_path.split('.') 339 | try: 340 | for attr in attrs: 341 | obj = getattr(obj, attr) 342 | return obj 343 | except AttributeError: 344 | return default 345 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stphivos/django-mock-queries/d4395d39c8e805bc2e44668be6ba4fdb86637f0f/examples/__init__.py -------------------------------------------------------------------------------- /examples/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stphivos/django-mock-queries/d4395d39c8e805bc2e44668be6ba4fdb86637f0f/examples/users/__init__.py -------------------------------------------------------------------------------- /examples/users/analytics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stphivos/django-mock-queries/d4395d39c8e805bc2e44668be6ba4fdb86637f0f/examples/users/analytics/__init__.py -------------------------------------------------------------------------------- /examples/users/analytics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /examples/users/analytics/api.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from django.contrib.auth.models import User 3 | from django.db.models import Count 4 | 5 | 6 | class AnalyticsApi: 7 | def active_users(self): 8 | return User.objects.filter(is_active=True).all() 9 | 10 | def create_user(self, **attrs): 11 | return User.objects.create(**attrs) 12 | 13 | def today_visitors_count(self): 14 | result = User.objects.filter(last_login__gte=date.today()).aggregate(Count('last_login')) 15 | return result['last_login__count'] 16 | 17 | def staff_usernames(self): 18 | return User.objects.filter(is_staff=True).values_list('username', flat=True) 19 | -------------------------------------------------------------------------------- /examples/users/analytics/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/users/analytics/tests.py: -------------------------------------------------------------------------------- 1 | """ Example of mocking the database and using django_mock_queries. 2 | 3 | There are two kinds of test cases here: TestApi is a regular Django 4 | test that uses the database. The others use MockSet and mocked_relations 5 | to simulate QuerySets in memory, so they're much faster than regular Django 6 | tests. You can even run them without creating a database by using the 7 | settings_mocked module. 8 | ./manage.py test --settings=users.settings_mocked 9 | The regular tests can't run without the database, so make them skip 10 | if the database is mocked. Use this decorator: 11 | @skipIfDBFeature('is_mocked') 12 | """ 13 | 14 | from datetime import date, timedelta 15 | 16 | from django_mock_queries.query import MockModel 17 | from django_mock_queries.mocks import mocked_relations 18 | from django.contrib.auth.models import User 19 | from django.test import TestCase, skipIfDBFeature 20 | from model_bakery import baker 21 | from analytics.api import AnalyticsApi 22 | from analytics import views 23 | 24 | 25 | @skipIfDBFeature('is_mocked') 26 | class TestApi(TestCase): 27 | def setUp(self): 28 | self.api = AnalyticsApi() 29 | 30 | def test_api_create_user(self): 31 | _ = User.objects.create(username='plain1') 32 | _ = User.objects.create(username='plain2') 33 | staff1 = User.objects.create(username='staff1', is_staff=True) 34 | staff2 = User.objects.create(username='staff2', is_staff=True) 35 | 36 | usernames = [str(x) for x in self.api.staff_usernames()] 37 | self.assertEqual(usernames, [staff1.username, staff2.username]) 38 | 39 | 40 | @mocked_relations(User) 41 | class TestViews(TestCase): 42 | def test_views_active_users_contains_usernames_separated_by_comma(self): 43 | for i in range(5): 44 | User.objects.add(MockModel(username='user_%d' % i, 45 | is_active=True)) 46 | 47 | response = views.active_users() 48 | 49 | expected = ', '.join([x.username for x in User.objects.all()]) 50 | self.assertEqual(expected, response.content.decode('utf-8')) 51 | 52 | 53 | @mocked_relations(User) 54 | class TestMockedApi(TestCase): 55 | def setUp(self): 56 | self.api = AnalyticsApi() 57 | 58 | def test_mocked_api_active_users_filters_by_is_active_true(self): 59 | active_user = MockModel(mock_name='active user', is_active=True) 60 | inactive_user = MockModel(mock_name='inactive user', is_active=False) 61 | 62 | User.objects.add(*[active_user, inactive_user]) 63 | results = [x for x in self.api.active_users()] 64 | 65 | assert active_user in results 66 | assert inactive_user not in results 67 | 68 | def test_mocked_api_create_user(self): 69 | attrs = dict((k, v) for (k, v) in baker.prepare(User).__dict__.items() if k[0] != '_') 70 | user = self.api.create_user(**attrs) 71 | assert isinstance(user, User) 72 | 73 | for k, v in attrs.items(): 74 | assert getattr(user, k) == v 75 | 76 | def test_mocked_api_today_visitors_counts_todays_logins(self): 77 | past_visitors = [ 78 | MockModel(last_login=(date.today() - timedelta(days=1))), 79 | MockModel(last_login=(date.today() - timedelta(days=2))), 80 | MockModel(last_login=(date.today() - timedelta(days=3))), 81 | ] 82 | today_visitors = [ 83 | MockModel(last_login=date.today()), 84 | MockModel(last_login=date.today()) 85 | ] 86 | User.objects.add(*(past_visitors + today_visitors)) 87 | count = self.api.today_visitors_count() 88 | assert count == len(today_visitors) 89 | assert User.objects.filter(last_login__year__lte=date.today().year).exists() is True 90 | assert User.objects.filter(last_login__year__gt=date.today().year + 1).exists() is False 91 | 92 | 93 | # Comment out this decorator to see what happens when you forget to 94 | # mock some database access. It should work under "./manage.py test", but 95 | # raise a helpful error when run with "--settings=users.settings_mocked". 96 | @mocked_relations(User) 97 | class TestMockedUser(TestCase): 98 | def test_mocked_user_create(self): 99 | start_count = User.objects.count() 100 | 101 | User.objects.create(username='bob') 102 | final_count = User.objects.count() 103 | 104 | self.assertEqual(start_count + 1, final_count) 105 | -------------------------------------------------------------------------------- /examples/users/analytics/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | from analytics.api import AnalyticsApi 4 | api = AnalyticsApi() 5 | 6 | 7 | def active_users(*args, **kwargs): 8 | return HttpResponse(', '.join( 9 | [x.username for x in api.active_users()] 10 | )) 11 | -------------------------------------------------------------------------------- /examples/users/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "users.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /examples/users/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=users.settings 3 | python_files=test*.py -------------------------------------------------------------------------------- /examples/users/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stphivos/django-mock-queries/d4395d39c8e805bc2e44668be6ba4fdb86637f0f/examples/users/users/__init__.py -------------------------------------------------------------------------------- /examples/users/users/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for users project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | import django 16 | 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = '3sy3!6p&s&g2@t922(%@4z+(np+yc#amz0id80vyk03$x8&38$' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = ( 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'analytics', 42 | ) 43 | 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'users.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'users.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Internationalization 88 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 89 | 90 | LANGUAGE_CODE = 'en-us' 91 | 92 | TIME_ZONE = 'UTC' 93 | 94 | USE_I18N = True 95 | 96 | if django.VERSION[0] < 5: 97 | USE_L10N = True 98 | 99 | USE_TZ = True 100 | 101 | 102 | # Static files (CSS, JavaScript, Images) 103 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 104 | 105 | STATIC_URL = '/static/' 106 | -------------------------------------------------------------------------------- /examples/users/users/settings_mocked.py: -------------------------------------------------------------------------------- 1 | from django_mock_queries.mocks import monkey_patch_test_db 2 | 3 | from users.settings import * 4 | 5 | monkey_patch_test_db() 6 | -------------------------------------------------------------------------------- /examples/users/users/urls.py: -------------------------------------------------------------------------------- 1 | """users URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.urls import re_path 17 | 18 | from analytics import views 19 | 20 | urlpatterns = [ 21 | re_path(r'users/active', views.active_users), 22 | ] 23 | -------------------------------------------------------------------------------- /examples/users/users/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for users project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "users.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements/core.txt: -------------------------------------------------------------------------------- 1 | Django 2 | djangorestframework 3 | model-bakery>=1.0.0 4 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | pytest==7.3.1; python_version<"3.8" 2 | pytest>=7.4.0; python_version>"3.7" # New `ast` classes: https://github.com/pytest-dev/pytest/commit/9335a0b4453dbb08a07b6ac1b33d1ed3d17c45a5 3 | pytest-cov==4.0.0 4 | pytest-django 5 | flake8==6.0.0 6 | coverage==7.2.5 7 | tox==4.5.1 8 | virtualenv==20.26.6 9 | pypandoc==1.11 10 | setuptools==70.0.0 11 | twine==4.0.2 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | def read_md(filename): 5 | return open(filename).read() 6 | 7 | 8 | def parse_requirements(filename): 9 | reqs = [] 10 | with open(filename, 'r') as f: 11 | reqs = f.read().splitlines() 12 | if not reqs: 13 | raise RuntimeError("Unable to read requirements from '%s'" % filename) 14 | return reqs 15 | 16 | 17 | setup( 18 | name='django_mock_queries', 19 | version='2.3.0', 20 | description='A django library for mocking queryset functions in memory for testing', 21 | long_description=read_md('README.md'), 22 | long_description_content_type='text/markdown', 23 | url='https://github.com/stphivos/django-mock-queries', 24 | author='Phivos Stylianides', 25 | author_email='stphivos@gmail.com', 26 | license='MIT', 27 | classifiers=[ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Intended Audience :: Developers', 30 | 'Topic :: Software Development :: Testing', 31 | 'Topic :: Software Development :: Testing :: Mocking', 32 | 'Topic :: Software Development :: Testing :: Unit', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 3 :: Only', 36 | ], 37 | keywords='django orm mocking unit-testing tdd', 38 | packages=['django_mock_queries'], 39 | install_requires=parse_requirements('requirements/core.txt'), 40 | ) 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from django_mock_queries.mocks import mock_django_setup 2 | 3 | mock_django_setup('tests.mock_settings') 4 | -------------------------------------------------------------------------------- /tests/mock_models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from rest_framework import serializers 3 | 4 | 5 | class Manufacturer(models.Model): 6 | name = models.CharField(max_length=25) 7 | 8 | 9 | class ManufacturerSerializer(serializers.ModelSerializer): 10 | class Meta: 11 | model = Manufacturer 12 | fields = ('id', 'name',) 13 | 14 | 15 | class Car(models.Model): 16 | make = models.ForeignKey(Manufacturer, on_delete=models.CASCADE) 17 | model = models.CharField(max_length=25, blank=True, null=True) 18 | speed = models.IntegerField() 19 | 20 | def format_speed(self): 21 | return '{0} km/h'.format(self.speed) 22 | 23 | def validate_price(self): 24 | """ Validate price against manufacturer remote api """ 25 | 26 | def save(self, *args, **kwargs): 27 | self.validate_price() 28 | super(Car, self).save(*args, **kwargs) 29 | 30 | 31 | class CarVariation(models.Model): 32 | car = models.ForeignKey(Car, related_name='variations', on_delete=models.CASCADE) 33 | color = models.CharField(max_length=100) 34 | 35 | 36 | class Sedan(Car): 37 | pass 38 | 39 | 40 | class Passenger(models.Model): 41 | car = models.ForeignKey(Car, related_name='passengers', on_delete=models.CASCADE) 42 | name = models.CharField(max_length=25) 43 | 44 | 45 | class CarSerializer(serializers.ModelSerializer): 46 | make = ManufacturerSerializer() 47 | speed = serializers.SerializerMethodField() 48 | 49 | def get_speed(self, obj): 50 | return obj.format_speed() 51 | 52 | class Meta: 53 | model = Car 54 | fields = ('id', 'make', 'model', 'speed',) 55 | -------------------------------------------------------------------------------- /tests/mock_settings.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | from string import ascii_letters 3 | 4 | 5 | def gen_string(max_length): 6 | return str(''.join(choice(ascii_letters) for _ in range(max_length))) 7 | 8 | 9 | SECRET_KEY = gen_string(50) 10 | 11 | INSTALLED_APPS = ( 12 | 'django.contrib.contenttypes', 13 | 'tests', 14 | ) 15 | -------------------------------------------------------------------------------- /tests/test_asserts.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, skipIf 2 | from unittest.mock import patch 3 | 4 | from model_bakery import baker 5 | 6 | import django 7 | from django_mock_queries.asserts import assert_serializer, SerializerAssert 8 | from tests.mock_models import Car, CarSerializer, Manufacturer 9 | 10 | 11 | class TestQuery(TestCase): 12 | def setUp(self): 13 | self.make_model = baker.prepare(Manufacturer, id=1, _fill_optional=True) 14 | self.car_model = baker.prepare(Car, id=1, make=self.make_model, _fill_optional=True) 15 | self.serializer_assert = SerializerAssert(CarSerializer) 16 | 17 | def set_fields(*fields): 18 | self.serializer_assert.serializer.Meta.fields = fields 19 | 20 | declared = [x for x in self.serializer_assert.serializer._declared_fields] 21 | for x in declared: 22 | if x not in fields: 23 | self.serializer_assert.serializer._declared_fields.pop(x) 24 | 25 | self.set_serializer_fields = set_fields 26 | 27 | def test_assert_serializer_func_returns_assert_instance_with_cls(self): 28 | serializer = CarSerializer 29 | sa = assert_serializer(serializer) 30 | assert isinstance(sa, SerializerAssert) 31 | assert isinstance(sa._cls, type(serializer)) 32 | 33 | def test_serializer_assert_instance_sets_obj_returns_self(self): 34 | result = self.serializer_assert.instance(self.car_model) 35 | assert result == self.serializer_assert 36 | assert result._obj == self.car_model 37 | 38 | def test_serializer_assert_returns_sets_fields_returns_self(self): 39 | fields = ('id', 'name') 40 | result = self.serializer_assert.returns(*fields) 41 | assert result == self.serializer_assert 42 | assert result._return_fields == fields 43 | 44 | def test_serializer_assert_mocks_sets_fields_returns_self(self): 45 | fields = ('id', 'name') 46 | result = self.serializer_assert.mocks(*fields) 47 | assert result == self.serializer_assert 48 | assert result._mock_fields == fields 49 | 50 | def test_serializer_assert_values_sets_attrs_returns_self(self): 51 | attrs = {'id': 1, 'name': 'a'} 52 | result = self.serializer_assert.values(**attrs) 53 | assert result == self.serializer_assert 54 | assert result._expected_values == attrs 55 | 56 | def test_serializer_assert_run_does_not_allow_specifying_expected_value_for_mocked_field(self): 57 | sa = self.serializer_assert.mocks('make').values(make=self.make_model) 58 | self.assertRaises(AttributeError, sa.run) 59 | 60 | def test_serializer_assert_run_fails_when_expected_field_missing(self): 61 | fields = ('id', 'model', 'speed',) 62 | self.set_serializer_fields(*fields) 63 | sa = self.serializer_assert.instance(self.car_model).returns(*(fields + ('price',))) 64 | self.assertRaises(AssertionError, sa.run) 65 | 66 | def test_serializer_assert_run_succeeds_when_expected_fields_returned(self): 67 | fields = ('id', 'model', 'speed',) 68 | self.set_serializer_fields(*fields) 69 | sa = self.serializer_assert.instance(self.car_model).returns(*fields) 70 | sa.run() 71 | 72 | @patch('tests.mock_models.ManufacturerSerializer.to_representation') 73 | def test_serializer_assert_run_does_not_call_representation_on_mocked_fields(self, to_representation_mock): 74 | to_representation_mock.side_effect = NotImplementedError('Error on purpose to verify mocks') 75 | fields = ('id', 'make', 'model', 'speed',) 76 | self.set_serializer_fields(*fields) 77 | sa = self.serializer_assert.instance(self.car_model).mocks('make').returns(*fields) 78 | sa.run() 79 | 80 | def test_serializer_assert_run_fails_when_expected_field_value_not_equal_to_specified(self): 81 | values = { 82 | 'id': self.car_model.id + 1, 83 | } 84 | sa = self.serializer_assert.instance(self.car_model).returns(*values.keys()).values(**values) 85 | self.assertRaises(AssertionError, sa.run) 86 | 87 | def test_serializer_assert_run_succeeds_when_expected_field_values_all_equal_to_specified(self): 88 | values = { 89 | 'id': self.car_model.id, 90 | 'speed': self.car_model.format_speed() 91 | } 92 | sa = self.serializer_assert.instance(self.car_model).returns(*values.keys()).values(**values) 93 | sa.run() 94 | 95 | @skipIf(django.VERSION[:2] >= (1, 10), 96 | "Django 1.10 refreshes deleted fields from the database.") 97 | def test_serializer_assert_run_skips_check_for_null_field_excluded_from_serializer(self): 98 | delattr(self.car_model, 'model') 99 | sa = self.serializer_assert.instance(self.car_model).returns('model') 100 | sa.run() 101 | -------------------------------------------------------------------------------- /tests/test_mocks.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch, MagicMock, PropertyMock 3 | 4 | import django 5 | from django.db import connection 6 | from django.db.utils import NotSupportedError 7 | from django.db.backends.base.creation import BaseDatabaseCreation 8 | 9 | from django_mock_queries import mocks 10 | from django_mock_queries.mocks import monkey_patch_test_db, mock_django_connection, \ 11 | MockOneToOneMap, MockOneToManyMap, PatcherChain, mocked_relations, ModelMocker, Mocker 12 | from django_mock_queries.query import MockSet 13 | from tests.mock_models import Car, Sedan, Manufacturer, CarVariation 14 | 15 | 16 | class TestMocks(TestCase): 17 | def test_mock_sql_raises_error(self): 18 | """ Get a clear error if you forget to mock a database query. """ 19 | with self.assertRaisesRegex( 20 | NotSupportedError, 21 | "Mock database tried to execute SQL for Car model."): 22 | Car.objects.count() 23 | 24 | def test_exists_raises_error(self): 25 | """ Get a clear error if you forget to mock a database query. """ 26 | with self.assertRaisesRegex( 27 | NotSupportedError, 28 | "Mock database tried to execute SQL for Car model."): 29 | Car.objects.exists() 30 | 31 | def test_mock_django_setup_called_again(self): 32 | """ Shouldn't do anything the second time you call. """ 33 | mocks.mock_django_setup('tests.mock_settings') 34 | 35 | # noinspection PyUnresolvedReferences 36 | @patch('django_mock_queries.mocks.mock_django_connection') 37 | @patch.multiple('django.db.backends.base.creation.BaseDatabaseCreation', 38 | create_test_db=None, 39 | destroy_test_db=None) 40 | def test_monkey_patch_test_db(self, mock_method): 41 | monkey_patch_test_db() 42 | 43 | creation = BaseDatabaseCreation(None) 44 | creation.create_test_db() 45 | creation.destroy_test_db('foo_db') 46 | 47 | mock_method.assert_called_once_with(None) 48 | 49 | # noinspection PyUnusedLocal 50 | @patch('django.db.utils.ConnectionHandler') 51 | def test_mock_django_connection(self, mock_handler): 52 | is_foo_before = bool(getattr(connection.features, 'is_foo', False)) 53 | 54 | mock_django_connection(disabled_features=['is_foo']) 55 | 56 | is_foo_after = bool(getattr(connection.features, 'is_foo', False)) 57 | 58 | self.assertTrue(is_foo_before) 59 | self.assertFalse(is_foo_after) 60 | 61 | 62 | # noinspection PyUnresolvedReferences,PyStatementEffect 63 | class MockOneToOneTests(TestCase): 64 | def test_not_mocked(self): 65 | car = Car(id=99) 66 | 67 | with self.assertRaises(NotSupportedError): 68 | car.sedan 69 | 70 | @patch.object(Car, 'sedan', MockOneToOneMap(Car.sedan)) 71 | def test_not_set(self): 72 | car = Car(id=99) 73 | 74 | with self.assertRaises(Car.sedan.RelatedObjectDoesNotExist): 75 | car.sedan 76 | 77 | @patch.object(Car, 'sedan', MockOneToOneMap(Car.sedan)) 78 | def test_set(self): 79 | car = Car() 80 | sedan = Sedan() 81 | car.sedan = sedan 82 | 83 | self.assertIs(car.sedan, sedan) 84 | 85 | @patch.object(Car, 'sedan', MockOneToOneMap(Car.sedan)) 86 | def test_set_on_individual_object(self): 87 | car = Car() 88 | car2 = Car() 89 | car.sedan = Sedan() 90 | 91 | with self.assertRaises(Car.sedan.RelatedObjectDoesNotExist): 92 | car2.sedan 93 | 94 | @patch.object(Car, 'sedan', MockOneToOneMap(Car.sedan)) 95 | def test_delegation(self): 96 | if django.VERSION[0] < 2: 97 | self.assertEqual(Car.sedan.cache_name, '_sedan_cache') 98 | else: 99 | """ TODO - Refactored internal fields value cache: """ 100 | # https://github.com/django/django/commit/bfb746f983aa741afa3709794e70f1e0ab6040b5#diff-507b415116b409afa4f723e41a759a9e 101 | 102 | 103 | # noinspection PyUnresolvedReferences,PyStatementEffect 104 | class MockOneToManyTests(TestCase): 105 | def test_not_mocked(self): 106 | m = Manufacturer(id=99) 107 | 108 | with self.assertRaisesRegex( 109 | NotSupportedError, 110 | 'Mock database tried to execute SQL for Car model'): 111 | m.car_set.count() 112 | 113 | def test_mock_is_removed(self): 114 | m = Manufacturer(id=99) 115 | 116 | with patch.object(Manufacturer, 'car_set', MockOneToManyMap(Manufacturer.car_set)): 117 | m.car_set = MockSet(Car(speed=95)) 118 | self.assertEqual(1, m.car_set.count()) 119 | 120 | with self.assertRaisesRegex( 121 | NotSupportedError, 122 | 'Mock database tried to execute SQL for Car model'): 123 | m.car_set.count() 124 | 125 | @patch.object(Manufacturer, 'car_set', MockOneToManyMap(Manufacturer.car_set)) 126 | def test_not_set(self): 127 | m = Manufacturer() 128 | 129 | self.assertEqual(0, m.car_set.count()) 130 | 131 | @patch.object(Manufacturer, 'car_set', MockOneToManyMap(Manufacturer.car_set)) 132 | def test_set(self): 133 | m = Manufacturer() 134 | car_1 = Car(speed=95) 135 | car_2 = Car(speed=40) 136 | m.car_set.add(car_1) 137 | m.car_set.add(car_2) 138 | 139 | self.assertIs(m.car_set.first(), car_1) 140 | self.assertEqual(list(m.car_set.all()), [car_1, car_2]) 141 | 142 | @patch.object(Manufacturer, 'car_set', MockOneToManyMap(Manufacturer.car_set)) 143 | def test_set_on_individual_object(self): 144 | m = Manufacturer() 145 | m.car_set.add(Car(speed=95)) 146 | m2 = Manufacturer() 147 | 148 | self.assertEqual(0, m2.car_set.count()) 149 | 150 | @patch.object(Manufacturer, 'car_set', MockOneToManyMap(Manufacturer.car_set)) 151 | def test_set_explicit_collection(self): 152 | m = Manufacturer() 153 | m.car_set.add(Car(speed=95)) 154 | 155 | car = Car(speed=100) 156 | m.car_set = MockSet(car) 157 | 158 | self.assertIs(m.car_set.first(), car) 159 | 160 | @patch.object(Manufacturer, 'car_set', MockOneToManyMap(Manufacturer.car_set)) 161 | @patch.object(Car, 'save', MagicMock) 162 | def test_create(self): 163 | m = Manufacturer() 164 | car = m.car_set.create(speed=95) 165 | 166 | self.assertIsInstance(car, Car) 167 | self.assertEqual(95, car.speed) 168 | 169 | @patch.object(Manufacturer, 'car_set', MockOneToManyMap(Manufacturer.car_set)) 170 | def test_delegation(self): 171 | """ We can still access fields from the original relation manager. """ 172 | self.assertTrue(Manufacturer.car_set.related_manager_cls.do_not_call_in_templates) 173 | 174 | @patch.object(Manufacturer, 'car_set', MockOneToManyMap(Manufacturer.car_set)) 175 | def test_raises(self): 176 | """ Raises an error specific to the child class. """ 177 | m = Manufacturer() 178 | 179 | with self.assertRaises(Car.DoesNotExist): 180 | m.car_set.get(speed=0) 181 | 182 | 183 | # noinspection PyUnusedLocal 184 | def zero_sum(items): 185 | return 0 186 | 187 | 188 | class PatcherChainTest(TestCase): 189 | patch_mock_max = patch('builtins.max') 190 | patch_zero_sum = patch('builtins.sum', zero_sum) 191 | 192 | @patch_zero_sum 193 | def test_patch_dummy(self): 194 | sum_result = sum([1, 2, 3]) 195 | 196 | self.assertEqual(0, sum_result) 197 | 198 | @patch_mock_max 199 | def test_patch_mock(self, mock_max): 200 | mock_max.return_value = 42 201 | max_result = max([1, 2, 3]) 202 | 203 | self.assertEqual(42, max_result) 204 | 205 | @PatcherChain([patch_zero_sum, patch_mock_max]) 206 | def test_patch_both(self, mock_max): 207 | sum_result = sum([1, 2, 3]) 208 | mock_max.return_value = 42 209 | max_result = max([1, 2, 3]) 210 | 211 | self.assertEqual(0, sum_result) 212 | self.assertEqual(42, max_result) 213 | 214 | @PatcherChain([patch_mock_max, patch_zero_sum]) 215 | def test_patch_both_reversed(self, mock_max): 216 | sum_result = sum([1, 2, 3]) 217 | mock_max.return_value = 42 218 | max_result = max([1, 2, 3]) 219 | 220 | self.assertEqual(0, sum_result) 221 | self.assertEqual(42, max_result) 222 | 223 | @PatcherChain([patch_mock_max], pass_mocks=False) 224 | def test_mocks_not_passed(self): 225 | """ Create a new mock, but don't pass it to the test method. """ 226 | 227 | def test_context_manager(self): 228 | with PatcherChain([PatcherChainTest.patch_mock_max, 229 | PatcherChainTest.patch_zero_sum]) as mocked: 230 | sum_result = sum([1, 2, 3]) 231 | mocked[0].return_value = 42 232 | max_result = max([1, 2, 3]) 233 | 234 | self.assertEqual(0, sum_result) 235 | self.assertEqual(42, max_result) 236 | self.assertEqual(2, len(mocked)) 237 | self.assertIs(zero_sum, mocked[1]) 238 | 239 | def test_start(self): 240 | patcher = PatcherChain([PatcherChainTest.patch_mock_max, 241 | PatcherChainTest.patch_zero_sum]) 242 | mocked = patcher.start() 243 | self.addCleanup(patcher.stop) 244 | 245 | sum_result = sum([1, 2, 3]) 246 | mocked[0].return_value = 42 247 | max_result = max([1, 2, 3]) 248 | 249 | self.assertEqual(0, sum_result) 250 | self.assertEqual(42, max_result) 251 | self.assertEqual(2, len(mocked)) 252 | self.assertIs(zero_sum, mocked[1]) 253 | 254 | 255 | @PatcherChain([patch('builtins.max'), patch('builtins.sum', zero_sum)], 256 | pass_mocks=False) 257 | class PatcherChainOnClassTest(TestCase): 258 | test_example_attribute = 42 259 | 260 | def test_patch_dummy(self): 261 | sum_result = sum([1, 2, 3]) 262 | 263 | self.assertEqual(0, sum_result) 264 | 265 | def test_patch_mock(self): 266 | max_result = max([1, 2, 3]) 267 | 268 | self.assertIsInstance(max_result, MagicMock) 269 | 270 | def test_patch_both(self): 271 | sum_result = sum([1, 2, 3]) 272 | max_result = max([1, 2, 3]) 273 | 274 | self.assertEqual(0, sum_result) 275 | self.assertIsInstance(max_result, MagicMock) 276 | 277 | def test_attribute(self): 278 | self.assertEqual(42, PatcherChainOnClassTest.test_example_attribute) 279 | 280 | 281 | class MockedRelationsTest(TestCase): 282 | @mocked_relations(Manufacturer) 283 | def test_mocked_relations_decorator(self): 284 | m = Manufacturer() 285 | 286 | self.assertEqual(0, m.car_set.count()) 287 | m.car_set.add(Car()) 288 | self.assertEqual(1, m.car_set.count()) 289 | 290 | def test_mocked_relations_context_manager(self): 291 | m = Manufacturer() 292 | 293 | with mocked_relations(Manufacturer): 294 | self.assertEqual(0, m.car_set.count()) 295 | m.car_set.add(Car()) 296 | self.assertEqual(1, m.car_set.count()) 297 | 298 | def test_mocked_relations_reusing_patcher(self): 299 | patcher = mocked_relations(Manufacturer) 300 | with patcher: 301 | self.assertEqual(0, Manufacturer.objects.count()) 302 | Manufacturer.objects.add(Manufacturer()) 303 | self.assertEqual(1, Manufacturer.objects.count()) 304 | 305 | with patcher: 306 | self.assertEqual(0, Manufacturer.objects.count()) 307 | Manufacturer.objects.add(Manufacturer()) 308 | self.assertEqual(1, Manufacturer.objects.count()) 309 | 310 | @mocked_relations(Manufacturer) 311 | def test_mocked_relations_with_garbage_collection(self): 312 | self.longMessage = True 313 | for group_index in range(10): 314 | m = Manufacturer() 315 | self.assertEqual(0, 316 | m.car_set.count(), 317 | 'group_index: {}'.format(group_index)) 318 | m.car_set.add(Car()) 319 | self.assertEqual(1, m.car_set.count()) 320 | del m 321 | 322 | def test_mocked_relations_replaces_other_mocks(self): 323 | original_type = type(Manufacturer.car_set) 324 | self.assertIsInstance(Manufacturer.car_set, original_type) 325 | 326 | with mocked_relations(Manufacturer): 327 | Manufacturer.car_set = PropertyMock('Manufacturer.car_set') 328 | 329 | self.assertIsInstance(Manufacturer.car_set, original_type) 330 | 331 | @mocked_relations(Sedan) 332 | def test_mocked_relations_parent(self): 333 | sedan = Sedan(speed=95) 334 | 335 | self.assertEqual(0, sedan.passengers.count()) 336 | 337 | @mocked_relations(Sedan) 338 | def test_mocked_relations_mock_twice(self): 339 | """ Don't reset the mocking if a class is mocked twice. 340 | 341 | Could happen where Sedan is mocked on the class, and Car (the base 342 | class) is mocked on a method. 343 | """ 344 | Car.objects.add(Car(speed=95)) 345 | self.assertEqual(1, Car.objects.count()) 346 | 347 | with mocked_relations(Car): 348 | self.assertEqual(1, Car.objects.count()) 349 | 350 | @mocked_relations(Manufacturer) 351 | def test_mocked_relations_raises_specific_error(self): 352 | with self.assertRaises(Manufacturer.DoesNotExist): 353 | Manufacturer.objects.get(name='sam') 354 | 355 | @mocked_relations(Manufacturer, Car) 356 | def test_mocked_relations_is_match_in_children(self): 357 | car = Car() 358 | manufacturer = Manufacturer() 359 | manufacturer.car_set.add(car) 360 | Manufacturer.objects.add(manufacturer) 361 | 362 | car_manufacturers = Manufacturer.objects.filter(car=car) 363 | 364 | self.assertEqual([manufacturer], list(car_manufacturers)) 365 | 366 | @mocked_relations(Manufacturer, Car) 367 | def test_mocked_relations_create_foreign_key_with_kwargs(self): 368 | make = Manufacturer.objects.create(name='foo') 369 | Car.objects.create(make=make) 370 | 371 | 372 | class TestMockers(TestCase): 373 | class CarModelMocker(ModelMocker): 374 | def validate_price(self): 375 | """ The real implementation would call an external service that 376 | we would like to skip but verify it's called before save. """ 377 | 378 | def test_mocker_with_replacement_method(self): 379 | class Foo(object): 380 | def who(self): 381 | return 'foo' 382 | 383 | class Bar(Mocker): 384 | def who(self): 385 | return 'bar' 386 | 387 | with Bar(Foo, 'who') as mocker: 388 | self.assertEqual(Foo().who(), mocker.who()) 389 | 390 | def test_mocker_with_replacement_attribute(self): 391 | class Foo(object): 392 | who = 'foo' 393 | 394 | class Bar(Mocker): 395 | who = 'bar' 396 | 397 | with Bar(Foo, 'who') as mocker: 398 | self.assertEqual(Foo.who, mocker.who) 399 | 400 | def test_mocker_without_replacement(self): 401 | class Foo(object): 402 | def who(self): 403 | return 'foo' 404 | 405 | class Bar(Mocker): 406 | pass 407 | 408 | with Bar(Foo, 'who'): 409 | self.assertIsInstance(Foo().who, MagicMock) 410 | 411 | def test_model_mocker_instance_save(self): 412 | with ModelMocker(Car): 413 | # New instance gets inserted 414 | obj = Car(speed=4) 415 | obj.save() 416 | self.assertEqual(Car.objects.get(pk=obj.id), obj) 417 | 418 | # Another instances gets inserted and has a different ID 419 | obj2 = Car(speed=5) 420 | obj.save() 421 | self.assertNotEqual(obj.id, obj2.id) 422 | 423 | # Existing instance gets updated 424 | obj = Car(id=obj.id, speed=5) 425 | obj.save() 426 | self.assertEqual(Car.objects.get(pk=obj.id).speed, obj.speed) 427 | 428 | # Trying to update an instance that doesn't exists creates it 429 | obj = Car(id=123, speed=5) 430 | obj.save() 431 | self.assertEqual(Car.objects.get(pk=obj.id), obj) 432 | 433 | def test_model_mocker_objects_create(self): 434 | with ModelMocker(Car): 435 | obj = Car.objects.create(speed=10) 436 | self.assertEqual(Car.objects.get(pk=obj.id), obj) 437 | 438 | def test_model_mocker_update_fk_from_instance(self): 439 | with ModelMocker(Manufacturer): 440 | with ModelMocker(Car, outer=False): 441 | manufacturer = Manufacturer.objects.create(name='foo') 442 | obj = Car.objects.create(speed=10, make=manufacturer) 443 | obj.make = Manufacturer.objects.create(name='bar') 444 | obj.save() 445 | 446 | self.assertEqual(Car.objects.get(pk=obj.id).make.name, 'bar') 447 | 448 | def test_model_mocker_with_custom_method(self): 449 | with self.CarModelMocker(Car, 'validate_price') as mocker: 450 | obj = Car() 451 | obj.save() 452 | 453 | mocker.method('validate_price').assert_called_with() 454 | 455 | @CarModelMocker(Car, 'validate_price') 456 | def test_model_mocker_callable_with_custom_method(self, mocker): 457 | obj = Car() 458 | obj.save() 459 | 460 | mocker.method('validate_price').assert_called_with() 461 | 462 | def test_model_mocker_event_added_from_manager(self): 463 | objects = {} 464 | 465 | def car_added(obj): 466 | objects['added'] = obj 467 | 468 | with ModelMocker(Car) as mocker: 469 | mocker.objects.on('added', car_added) 470 | objects['car'] = Car.objects.create(speed=300) 471 | 472 | self.assertIsInstance(objects['added'], Car) 473 | self.assertEqual(objects['added'], objects['car']) 474 | 475 | def test_model_mocker_event_added_from_instance(self): 476 | objects = {} 477 | 478 | def car_added(obj): 479 | objects['added'] = obj 480 | 481 | with ModelMocker(Car) as mocker: 482 | mocker.objects.on('added', car_added) 483 | objects['car'] = Car(speed=300) 484 | objects['car'].save() 485 | 486 | self.assertIsInstance(objects['added'], Car) 487 | self.assertEqual(objects['added'], objects['car']) 488 | 489 | def test_model_mocker_delete_from_instance_with_nested_context_manager(self): 490 | 491 | def create_delete_models(): 492 | car = Car.objects.create(speed=10) 493 | car.delete() 494 | 495 | manufacturer = Manufacturer.objects.create(name='foo') 496 | manufacturer.delete() 497 | 498 | def models_exist(): 499 | return Manufacturer.objects.exists() or Car.objects.exists() 500 | 501 | with ModelMocker(Manufacturer), ModelMocker(Car): 502 | create_delete_models() 503 | assert not models_exist() 504 | 505 | # Test same scenario with reversed context manager order 506 | with ModelMocker(Car), ModelMocker(Manufacturer): 507 | create_delete_models() 508 | assert not models_exist() 509 | 510 | def test_model_mocker_event_updated_from_manager(self): 511 | objects = {} 512 | 513 | def car_updated(obj): 514 | objects['updated'] = obj 515 | 516 | with ModelMocker(Car) as mocker: 517 | mocker.objects.on('updated', car_updated) 518 | objects['car'] = Car.objects.create(speed=300) 519 | Car.objects.update(speed=400) 520 | 521 | self.assertIsInstance(objects['updated'], Car) 522 | self.assertEqual(objects['updated'], objects['car']) 523 | 524 | def test_model_mocker_event_updated_from_instance(self): 525 | objects = {} 526 | 527 | def car_updated(obj): 528 | objects['updated'] = obj 529 | 530 | with ModelMocker(Car) as mocker: 531 | mocker.objects.on('updated', car_updated) 532 | objects['car'] = Car.objects.create(speed=300) 533 | objects['car'].save() 534 | 535 | self.assertIsInstance(objects['updated'], Car) 536 | self.assertEqual(objects['updated'], objects['car']) 537 | 538 | def test_model_mocker_event_deleted_from_manager(self): 539 | objects = {} 540 | 541 | def car_deleted(obj): 542 | objects['deleted'] = obj 543 | 544 | with ModelMocker(Car) as mocker: 545 | mocker.objects.on('deleted', car_deleted) 546 | objects['car'] = Car.objects.create(speed=300) 547 | Car.objects.delete() 548 | 549 | self.assertIsInstance(objects['deleted'], Car) 550 | self.assertEqual(objects['deleted'], objects['car']) 551 | 552 | def test_model_mocker_event_deleted_from_instance(self): 553 | objects = {} 554 | 555 | def car_deleted(obj): 556 | objects['deleted'] = obj 557 | 558 | with ModelMocker(Car) as mocker: 559 | mocker.objects.on('deleted', car_deleted) 560 | objects['car'] = Car.objects.create(speed=300) 561 | objects['car'].delete() 562 | 563 | self.assertIsInstance(objects['deleted'], Car) 564 | self.assertEqual(objects['deleted'], objects['car']) 565 | 566 | def test_model_mocker_does_not_interfere_with_non_mocked_models(self): 567 | original_objects = CarVariation.objects 568 | 569 | with ModelMocker(Manufacturer) as make_mocker: 570 | self.assertEqual(Manufacturer.objects, make_mocker.objects) 571 | 572 | with ModelMocker(Car, outer=False) as car_mocker: 573 | self.assertEqual(Car.objects, car_mocker.objects) 574 | self.assertEqual(CarVariation.objects, original_objects) 575 | 576 | with self.assertRaises(NotSupportedError): 577 | CarVariation.objects.create(color='blue') 578 | 579 | with self.assertRaises(NotSupportedError): 580 | CarVariation(color='blue').save() 581 | 582 | with self.assertRaises(NotSupportedError): 583 | CarVariation(id=1, color='blue').save() 584 | 585 | with self.assertRaises(NotSupportedError): 586 | CarVariation(pk=1).delete() 587 | 588 | with self.assertRaises(NotSupportedError): 589 | CarVariation.objects.all().delete() 590 | -------------------------------------------------------------------------------- /tests/test_query.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import warnings 3 | from unittest import TestCase 4 | from unittest.mock import MagicMock 5 | 6 | from django.core.exceptions import FieldError 7 | from django.core.paginator import Paginator 8 | from django.db import models 9 | from django.db.models import Q, Avg 10 | from django.db.models.functions import Coalesce 11 | 12 | from django_mock_queries.constants import * 13 | from django_mock_queries.exceptions import ModelNotSpecified, ArgumentNotSupported 14 | from django_mock_queries.query import MockSet, MockModel, create_model 15 | from django_mock_queries.mocks import mocked_relations 16 | from tests.mock_models import Car, CarVariation, Sedan, Manufacturer 17 | 18 | 19 | class TestQuery(TestCase): 20 | def setUp(self): 21 | self.mock_set = MockSet() 22 | 23 | def tearDown(self): 24 | self.mock_set.clear() 25 | 26 | def test_query_counts_items_in_set(self): 27 | items = [1, 2, 3] 28 | assert MockSet(*items).count() == len(items) 29 | 30 | def test_query_adds_items_to_set(self): 31 | items = [1, 2, 3] 32 | self.mock_set.add(*items) 33 | assert list(self.mock_set) == items 34 | 35 | def test_query_removes_items_from_set(self): 36 | item_1 = MockModel(foo=1) 37 | item_2 = MockModel(foo=2) 38 | 39 | self.mock_set.add(item_1, item_2) 40 | self.mock_set.remove(foo=1) 41 | items = list(self.mock_set) 42 | 43 | assert item_1 not in items 44 | assert item_2 in items 45 | 46 | def test_query_filters_items_by_attributes(self): 47 | item_1 = MockModel(foo=1, bar='a') 48 | item_2 = MockModel(foo=1, bar='b') 49 | item_3 = MockModel(foo=2, bar='b') 50 | 51 | self.mock_set.add(item_1, item_2, item_3) 52 | results = list(self.mock_set.filter(foo=1, bar='b')) 53 | 54 | assert item_1 not in results 55 | assert item_2 in results 56 | assert item_3 not in results 57 | 58 | def test_query_filters_items_by_boolean_attributes(self): 59 | item_1 = MockModel(foo=True, bar=True) 60 | item_2 = MockModel(foo=True, bar=False) 61 | item_3 = MockModel(foo=False, bar=False) 62 | 63 | self.mock_set.add(item_1, item_2, item_3) 64 | results = list(self.mock_set.filter(foo=True, bar=False)) 65 | 66 | assert item_1 not in results 67 | assert item_2 in results 68 | assert item_3 not in results 69 | 70 | def test_query_filters_items_by_q_object_or(self): 71 | item_1 = MockModel(mock_name='#1', foo=1) 72 | item_2 = MockModel(mock_name='#2', foo=2) 73 | item_3 = MockModel(mock_name='#3', foo=3) 74 | 75 | self.mock_set.add(item_1, item_2, item_3) 76 | results = list(self.mock_set.filter(Q(foo=1) | Q(foo=2))) 77 | 78 | assert item_1 in results 79 | assert item_2 in results 80 | assert item_3 not in results 81 | 82 | def test_query_filters_items_by_q_object_and(self): 83 | item_1 = MockModel(mock_name='#1', foo=1, bar='a') 84 | item_2 = MockModel(mock_name='#2', foo=1, bar='b') 85 | item_3 = MockModel(mock_name='#3', foo=3, bar='b') 86 | 87 | self.mock_set.add(item_1, item_2, item_3) 88 | results = list(self.mock_set.filter(Q(foo=1) & Q(bar='b'))) 89 | 90 | assert item_1 not in results 91 | assert item_2 in results 92 | assert item_3 not in results 93 | 94 | def test_query_filters_items_by_q_object_with_negation(self): 95 | item_1 = MockModel(mock_name='#1', foo=1, bar='a') 96 | item_2 = MockModel(mock_name='#2', foo=1, bar='b') 97 | item_3 = MockModel(mock_name='#3', foo=3, bar='b') 98 | 99 | self.mock_set.add(item_1, item_2, item_3) 100 | results = list(self.mock_set.filter(~Q(foo=1) | Q(bar='a'))) 101 | 102 | assert item_1 in results 103 | assert item_2 not in results 104 | assert item_3 in results 105 | 106 | def test_query_filters_items_by_q_object_and_with_one_empty(self): 107 | item_3 = MockModel(mock_name='#1', foo=3, bar='b') 108 | 109 | self.mock_set.add(item_3) 110 | results = list(self.mock_set.filter(Q(bar='b', foo=1))) 111 | 112 | assert item_3 not in results 113 | 114 | def test_query_filters_items_by_unsupported_object(self): 115 | bogus_filter = 'This is not a filter.' 116 | 117 | with self.assertRaises(ArgumentNotSupported): 118 | self.mock_set.filter(bogus_filter) 119 | 120 | def test_query_filters_model_objects(self): 121 | item_1 = Car(speed=1) 122 | item_2 = Sedan(speed=2) 123 | item_3 = Car(speed=3) 124 | 125 | item_2.sedan = item_2 126 | 127 | self.mock_set.add(item_1, item_2, item_3) 128 | results = list(self.mock_set.filter(speed=3)) 129 | 130 | assert results == [item_3] 131 | 132 | def test_query_filters_related_model_objects(self): 133 | item_1 = Car(make=Manufacturer(name='apple')) 134 | item_2 = Car(make=Manufacturer(name='banana')) 135 | item_3 = Car(make=Manufacturer(name='cherry')) 136 | 137 | self.mock_set.add(item_1, item_2, item_3) 138 | results = list(self.mock_set.filter(make__name='cherry')) 139 | 140 | assert results == [item_3] 141 | 142 | def test_query_filters_model_objects_by_subclass(self): 143 | item_1 = Car(speed=1) 144 | item_2 = Sedan(speed=2) 145 | item_3 = Car(speed=3) 146 | 147 | item_2.sedan = item_2 148 | 149 | self.mock_set.add(item_1, item_2, item_3) 150 | results = list(self.mock_set.filter(sedan__isnull=False)) 151 | 152 | assert results == [item_2] 153 | 154 | def test_query_filters_model_objects_by_pk(self): 155 | item_1 = Car(speed=1, id=101) 156 | item_2 = Car(speed=2, id=102) 157 | 158 | self.mock_set.add(item_1, item_2) 159 | results = list(self.mock_set.filter(pk=102)) 160 | 161 | assert results == [item_2] 162 | 163 | def test_query_convert_to_pks(self): 164 | car1 = Car(id=101) 165 | car2 = Car(id=102) 166 | car3 = Car(id=103) 167 | 168 | old_cars = MockSet(car1, car2) 169 | all_cars = MockSet(car1, car2, car3) 170 | 171 | matches = all_cars.filter(pk__in=old_cars) 172 | 173 | self.assertEqual(list(old_cars), list(matches)) 174 | 175 | def test_query_convert_values_list_to_pks(self): 176 | car1 = Car(id=101) 177 | car2 = Car(id=102) 178 | car3 = Car(id=103) 179 | 180 | old_cars = MockSet(car1, car2) 181 | old_car_pks = old_cars.values_list("pk", flat=True) 182 | all_cars = MockSet(car1, car2, car3) 183 | 184 | matches = all_cars.filter(pk__in=old_car_pks) 185 | 186 | self.assertEqual(list(old_cars), list(matches)) 187 | 188 | def test_query_filters_model_objects_by_bad_field(self): 189 | item_1 = Car(speed=1) 190 | item_2 = Sedan(speed=2) 191 | item_3 = Car(speed=3) 192 | 193 | item_2.sedan = item_2 194 | 195 | self.mock_set.add(item_1, item_2, item_3) 196 | with self.assertRaisesRegex( 197 | FieldError, 198 | r"Cannot resolve keyword 'bad_field' into field\. " 199 | r"Choices are 'id', 'make', 'make_id', 'model', 'passengers', 'sedan', 'speed', 'variations'\."): 200 | self.mock_set.filter(bad_field='bogus') 201 | 202 | def test_query_filters_reverse_relationship_by_in_comparison(self): 203 | with mocked_relations(Manufacturer): 204 | cars = [Car(speed=1)] 205 | 206 | make = Manufacturer() 207 | make.car_set = MockSet(*cars) 208 | 209 | self.mock_set.add(make) 210 | 211 | result = self.mock_set.filter(car__speed__in=[1, 2]) 212 | assert result.count() == 1 213 | 214 | def test_query_exclude(self): 215 | item_1 = MockModel(foo=1, bar='a') 216 | item_2 = MockModel(foo=1, bar='b') 217 | item_3 = MockModel(foo=2, bar='b') 218 | 219 | self.mock_set.add(item_1, item_2, item_3) 220 | results = list(self.mock_set.exclude(foo=1, bar='b')) 221 | 222 | assert item_1 in results, results 223 | assert item_2 not in results, results 224 | assert item_3 in results, results 225 | 226 | def test_query_clears_all_items_from_set(self): 227 | self.mock_set.add(1, 2, 3) 228 | self.mock_set.clear() 229 | assert list(self.mock_set) == [] 230 | 231 | def test_query_exists_returns_true_when_items_above_zero_otherwise_false(self): 232 | assert self.mock_set.exists() is False 233 | self.mock_set.add(1) 234 | assert self.mock_set.exists() is True 235 | 236 | def test_query_indexing_set_returns_nth_item(self): 237 | items = [1, 2, 3] 238 | self.mock_set.add(*items) 239 | assert self.mock_set[1] == items[1] 240 | 241 | def test_query_aggregate_performs_sum_on_queryset_field(self): 242 | items = [ 243 | MockModel(foo=5), 244 | MockModel(foo=10), 245 | MockModel(foo=15), 246 | MockModel(foo=None) 247 | ] 248 | self.mock_set.add(*items) 249 | 250 | expr = MagicMock(function=AGGREGATES_SUM, source_expressions=[MockModel(name='foo')]) 251 | result = self.mock_set.aggregate(expr) 252 | 253 | assert result['foo__sum'] == sum([x.foo for x in items if x.foo is not None]) 254 | 255 | def test_query_aggregate_on_related_field(self): 256 | with mocked_relations(Manufacturer): 257 | cars = [Car(speed=1), Car(speed=2), Car(speed=3)] 258 | 259 | make = Manufacturer() 260 | make.car_set = MockSet(*cars) 261 | 262 | self.mock_set.add(make) 263 | 264 | result = self.mock_set.aggregate(Avg('car__speed')) 265 | assert result['car__speed__avg'] == sum([c.speed for c in cars]) / len(cars) 266 | 267 | def test_query_aggregate_performs_count_on_queryset_field(self): 268 | items = [ 269 | MockModel(foo=5), 270 | MockModel(foo=10), 271 | MockModel(foo=15), 272 | MockModel(foo=None) 273 | ] 274 | self.mock_set.add(*items) 275 | 276 | expr = MagicMock(function=AGGREGATES_COUNT, source_expressions=[MockModel(name='foo')]) 277 | result = self.mock_set.aggregate(expr) 278 | 279 | assert result['foo__count'] == len([x.foo for x in items if x.foo is not None]) 280 | 281 | def test_query_aggregate_performs_max_on_queryset_field(self): 282 | items = [ 283 | MockModel(foo=5), 284 | MockModel(foo=10), 285 | MockModel(foo=15), 286 | MockModel(foo=None) 287 | ] 288 | self.mock_set.add(*items) 289 | 290 | expr = MagicMock(function=AGGREGATES_MAX, source_expressions=[MockModel(name='foo')]) 291 | result = self.mock_set.aggregate(expr) 292 | 293 | assert result['foo__max'] == max([x.foo for x in items if x.foo is not None]) 294 | 295 | def test_query_aggregate_performs_min_on_queryset_field(self): 296 | items = [ 297 | MockModel(foo=5), 298 | MockModel(foo=10), 299 | MockModel(foo=15), 300 | MockModel(foo=None) 301 | ] 302 | self.mock_set.add(*items) 303 | 304 | expr = MagicMock(function=AGGREGATES_MIN, source_expressions=[MockModel(name='foo')]) 305 | result = self.mock_set.aggregate(expr) 306 | 307 | assert result['foo__min'] == min([x.foo for x in items if x.foo is not None]) 308 | 309 | def test_query_aggregate_performs_avg_on_queryset_field(self): 310 | items = [ 311 | MockModel(foo=5), 312 | MockModel(foo=10), 313 | MockModel(foo=15), 314 | MockModel(foo=None) 315 | ] 316 | self.mock_set.add(*items) 317 | 318 | expr = MagicMock(function=AGGREGATES_AVG, source_expressions=[MockModel(name='foo')]) 319 | result = self.mock_set.aggregate(expr) 320 | 321 | assert result['foo__avg'] == sum( 322 | [x.foo for x in items if x.foo is not None] 323 | ) / len( 324 | [x.foo for x in items if x.foo is not None] 325 | ) 326 | 327 | def test_query_aggregate_performs_array_on_queryset_field(self): 328 | items = [ 329 | MockModel(foo=5), 330 | MockModel(foo=10), 331 | MockModel(foo=15), 332 | ] 333 | self.mock_set.add(*items) 334 | 335 | expr = MagicMock(function=AGGREGATES_ARRAY, source_expressions=[MockModel(name='foo')]) 336 | result = self.mock_set.aggregate(expr) 337 | 338 | assert result['foo__array_agg'] == [x.foo for x in items] 339 | 340 | def test_query_aggregate_with_none_only_field_values_performs_correct_aggregation(self): 341 | items = [ 342 | MockModel(foo=None), 343 | MockModel(foo=None), 344 | MockModel(foo=None), 345 | MockModel(foo=None) 346 | ] 347 | self.mock_set.add(*items) 348 | 349 | expr_sum = MagicMock(function=AGGREGATES_SUM, source_expressions=[MockModel(name='foo')]) 350 | expr_max = MagicMock(function=AGGREGATES_MAX, source_expressions=[MockModel(name='foo')]) 351 | expr_min = MagicMock(function=AGGREGATES_MIN, source_expressions=[MockModel(name='foo')]) 352 | expr_count = MagicMock(function=AGGREGATES_COUNT, source_expressions=[MockModel(name='foo')]) 353 | expr_avg = MagicMock(function=AGGREGATES_AVG, source_expressions=[MockModel(name='foo')]) 354 | 355 | result_sum = self.mock_set.aggregate(expr_sum) 356 | result_max = self.mock_set.aggregate(expr_max) 357 | result_min = self.mock_set.aggregate(expr_min) 358 | result_count = self.mock_set.aggregate(expr_count) 359 | result_avg = self.mock_set.aggregate(expr_avg) 360 | 361 | assert result_sum['foo__sum'] is None 362 | assert result_max['foo__max'] is None 363 | assert result_min['foo__min'] is None 364 | assert result_count['foo__count'] == 0 365 | assert result_avg['foo__avg'] is None 366 | 367 | def test_query_aggregate_multiple_params_aggregation(self): 368 | items = [ 369 | MockModel(foo=5), 370 | MockModel(foo=10), 371 | MockModel(foo=15), 372 | MockModel(foo=None) 373 | ] 374 | self.mock_set.add(*items) 375 | 376 | expr_sum = MagicMock(function=AGGREGATES_SUM, source_expressions=[MockModel(name='foo')]) 377 | expr_max = MagicMock(function=AGGREGATES_MAX, source_expressions=[MockModel(name='foo')]) 378 | expr_min = MagicMock(function=AGGREGATES_MIN, source_expressions=[MockModel(name='foo')]) 379 | expr_count = MagicMock(function=AGGREGATES_COUNT, source_expressions=[MockModel(name='foo')]) 380 | expr_avg = MagicMock(function=AGGREGATES_AVG, source_expressions=[MockModel(name='foo')]) 381 | 382 | result = self.mock_set.aggregate(expr_sum, expr_max, expr_min, expr_count, expr_avg, 383 | sum=expr_sum, max=expr_max, min=expr_min, count=expr_count, avg=expr_avg) 384 | 385 | assert result['foo__sum'] == sum([x.foo for x in items if x.foo is not None]) 386 | assert result['foo__max'] == max([x.foo for x in items if x.foo is not None]) 387 | assert result['foo__min'] == min([x.foo for x in items if x.foo is not None]) 388 | assert result['foo__count'] == len([x.foo for x in items if x.foo is not None]) 389 | assert result['foo__avg'] == sum( 390 | [x.foo for x in items if x.foo is not None] 391 | ) / len( 392 | [x.foo for x in items if x.foo is not None] 393 | ) 394 | assert result['sum'] == sum([x.foo for x in items if x.foo is not None]) 395 | assert result['max'] == max([x.foo for x in items if x.foo is not None]) 396 | assert result['min'] == min([x.foo for x in items if x.foo is not None]) 397 | assert result['count'] == len([x.foo for x in items if x.foo is not None]) 398 | assert result['avg'] == sum( 399 | [x.foo for x in items if x.foo is not None] 400 | ) / len( 401 | [x.foo for x in items if x.foo is not None] 402 | ) 403 | 404 | def test_query_aggregate_multiple_params_with_none_only_field_values_aggregation_with_none(self): 405 | items = [ 406 | MockModel(foo=None), 407 | MockModel(foo=None), 408 | MockModel(foo=None), 409 | MockModel(foo=None) 410 | ] 411 | self.mock_set.add(*items) 412 | 413 | expr_sum = MagicMock(function=AGGREGATES_SUM, source_expressions=[MockModel(name='foo')]) 414 | expr_max = MagicMock(function=AGGREGATES_MAX, source_expressions=[MockModel(name='foo')]) 415 | expr_min = MagicMock(function=AGGREGATES_MIN, source_expressions=[MockModel(name='foo')]) 416 | expr_count = MagicMock(function=AGGREGATES_COUNT, source_expressions=[MockModel(name='foo')]) 417 | expr_avg = MagicMock(function=AGGREGATES_AVG, source_expressions=[MockModel(name='foo')]) 418 | 419 | result = self.mock_set.aggregate(expr_sum, expr_max, expr_min, expr_count, expr_avg, 420 | sum=expr_sum, max=expr_max, min=expr_min, count=expr_count, avg=expr_avg) 421 | 422 | assert result['foo__sum'] is None 423 | assert result['foo__max'] is None 424 | assert result['foo__min'] is None 425 | assert result['foo__count'] == 0 426 | assert result['foo__avg'] is None 427 | assert result['sum'] is None 428 | assert result['max'] is None 429 | assert result['min'] is None 430 | assert result['count'] == 0 431 | assert result['avg'] is None 432 | 433 | def test_query_aggregate_with_no_params_returns_empty_dict(self): 434 | assert self.mock_set.aggregate() == {} 435 | 436 | def test_query_aggregate_multiple_params_expression_distinction(self): 437 | expr_sum = MagicMock(function=AGGREGATES_SUM, source_expressions=[MockModel(name='foo')]) 438 | expr_max = MagicMock(function=AGGREGATES_MAX, source_expressions=[MockModel(name='foo')]) 439 | expr_min = MagicMock(function=AGGREGATES_MIN, source_expressions=[MockModel(name='foo')]) 440 | expr_count = MagicMock(function=AGGREGATES_COUNT, source_expressions=[MockModel(name='foo')]) 441 | expr_avg = MagicMock(function=AGGREGATES_AVG, source_expressions=[MockModel(name='foo')]) 442 | 443 | result = self.mock_set.aggregate(expr_sum, expr_max, expr_min, expr_count, expr_avg, 444 | expr_sum, expr_max, expr_min, expr_count, expr_avg, 445 | a=expr_max, b=expr_max, c=expr_min, d=expr_min, 446 | e=expr_sum, f=expr_sum, g=expr_avg, h=expr_avg, 447 | i=expr_count, j=expr_count) 448 | 449 | assert len(result) == 15 450 | 451 | def test_query_first_none(self): 452 | first = self.mock_set.first() 453 | 454 | assert first is None, first 455 | 456 | def test_query_first(self): 457 | item_1 = MockModel(foo=1) 458 | item_2 = MockModel(foo=2) 459 | item_3 = MockModel(foo=3) 460 | 461 | self.mock_set.add(item_3, item_1, item_2) 462 | first = self.mock_set.first() 463 | 464 | assert first == item_3, first 465 | 466 | def test_query_last_none(self): 467 | last = self.mock_set.last() 468 | 469 | assert last is None, last 470 | 471 | def test_query_last(self): 472 | item_1 = MockModel(foo=1) 473 | item_2 = MockModel(foo=2) 474 | item_3 = MockModel(foo=3) 475 | 476 | self.mock_set.add(item_3, item_1, item_2) 477 | last = self.mock_set.last() 478 | 479 | assert last == item_2, last 480 | 481 | def test_query_latest_returns_the_last_element_from_ordered_set_using_fields_args(self): 482 | item_1 = MockModel(foo=1) 483 | item_2 = MockModel(foo=2) 484 | item_3 = MockModel(foo=3) 485 | 486 | self.mock_set.add(item_3, item_1, item_2) 487 | latest = self.mock_set.latest('foo') 488 | 489 | assert latest == item_3 490 | 491 | def test_query_latest_returns_the_last_element_from_ordered_set_using_field_name_kwarg(self): 492 | item_1 = MockModel(foo=1) 493 | item_2 = MockModel(foo=2) 494 | item_3 = MockModel(foo=3) 495 | 496 | self.mock_set.add(item_3, item_1, item_2) 497 | latest = self.mock_set.latest(field_name='foo') 498 | 499 | assert latest == item_3 500 | 501 | def test_query_latest_returns_the_last_element_from_ordered_set_using_meta_get_latest_by(self): 502 | item_1 = MagicMock(foo=1) 503 | item_2 = MagicMock(foo=2) 504 | item_3 = MagicMock(foo=3) 505 | 506 | objects = MockSet(item_3, item_1, item_2, model=MockModel()) 507 | objects.model._meta.get_latest_by = 'foo' 508 | latest = objects.latest() 509 | 510 | assert latest == item_3 511 | 512 | def test_query_latest_raises_error_when_both_fields_args_and_field_name_kwarg_supplied(self): 513 | item_1 = MockModel(foo=1, bar='a') 514 | item_2 = MockModel(foo=2, bar='b') 515 | item_3 = MockModel(foo=3, bar='c') 516 | 517 | self.mock_set.add(item_3, item_1, item_2) 518 | 519 | self.assertRaises(ValueError, self.mock_set.latest, 'foo', field_name='bar') 520 | 521 | def test_query_latest_raises_error_when_no_fields_supplied(self): 522 | item_1 = MagicMock(foo=1) 523 | item_2 = MagicMock(foo=2) 524 | item_3 = MagicMock(foo=3) 525 | 526 | objects = MockSet(item_3, item_1, item_2, model=MockModel()) 527 | 528 | self.assertRaises(ValueError, objects.latest) 529 | 530 | def test_query_latest_raises_error_exist_when_empty_set(self): 531 | self.mock_set.clear() 532 | self.assertRaises(ObjectDoesNotExist, self.mock_set.latest, 'foo') 533 | 534 | def test_query_earliest_returns_the_first_element_from_ordered_set(self): 535 | item_1 = MockModel(foo=1) 536 | item_2 = MockModel(foo=2) 537 | item_3 = MockModel(foo=3) 538 | 539 | self.mock_set.add(item_3, item_1, item_2) 540 | latest = self.mock_set.earliest('foo') 541 | 542 | assert latest == item_1 543 | 544 | def test_query_earliest_raises_error_exist_when_empty_set(self): 545 | self.mock_set.clear() 546 | self.assertRaises(ObjectDoesNotExist, self.mock_set.earliest, 'foo') 547 | 548 | def test_query_order_by(self): 549 | item_1 = MockModel(foo=1, bar='a', mock_name='item_1') 550 | item_2 = MockModel(foo=1, bar='c', mock_name='item_2') 551 | item_3 = MockModel(foo=2, bar='b', mock_name='item_3') 552 | 553 | self.mock_set.add(item_1, item_3, item_2) 554 | results = list(self.mock_set.order_by('foo', 'bar')) 555 | 556 | assert results == [item_1, item_2, item_3], results 557 | 558 | def test_query_order_by_descending(self): 559 | item_1 = MockModel(foo=1, bar='c', mock_name='item_1') 560 | item_2 = MockModel(foo=1, bar='a', mock_name='item_2') 561 | item_3 = MockModel(foo=2, bar='b', mock_name='item_3') 562 | 563 | self.mock_set.add(item_2, item_3, item_1) 564 | results = list(self.mock_set.order_by('foo', '-bar')) 565 | 566 | assert results == [item_1, item_2, item_3], results 567 | 568 | def test_query_order_by_random(self): 569 | def make_model(idx): 570 | return MockModel( 571 | mock_name='test{}'.format(idx), 572 | email='test{}@domain.com'.format(idx), 573 | ) 574 | 575 | qs = MockSet(*[make_model(i) for i in range(10)]) 576 | assert any(list(qs) != list(qs.order_by('?')) for _ in range(5)) 577 | 578 | def test_ordered_queryset_pagination_does_not_raise_warning(self): 579 | item_1 = MockModel(foo=1, bar='a', mock_name='item_1') 580 | item_2 = MockModel(foo=1, bar='c', mock_name='item_2') 581 | item_3 = MockModel(foo=2, bar='b', mock_name='item_3') 582 | 583 | self.mock_set.add(item_1, item_3, item_2) 584 | 585 | qs = self.mock_set.order_by('foo', 'bar') 586 | 587 | with warnings.catch_warnings(record=True) as w: 588 | warnings.simplefilter("always") 589 | 590 | Paginator(qs, 2) 591 | 592 | assert 0 == len(w) 593 | 594 | def test_query_distinct(self): 595 | item_1 = MockModel(foo=1, mock_name='item_1') 596 | item_2 = MockModel(foo=2, mock_name='item_2') 597 | item_3 = MockModel(foo=3, mock_name='item_3') 598 | 599 | self.mock_set.add(item_2, item_3, item_1, item_3) 600 | results = list(self.mock_set.distinct().order_by('foo')) 601 | 602 | assert results == [item_1, item_2, item_3], results 603 | 604 | def test_query_distinct_django_model(self): 605 | item_1 = Car(speed=1) 606 | item_2 = Car(speed=2) 607 | item_3 = Car(speed=3) 608 | 609 | self.mock_set.add(item_2, item_3, item_1, item_3) 610 | results = list(self.mock_set.distinct().order_by('speed')) 611 | 612 | assert results == [item_1, item_2, item_3], results 613 | 614 | def test_query_distinct_values(self): 615 | item_1 = MockModel(foo=1, mock_name='item_1') 616 | item_2 = MockModel(foo=2, mock_name='item_2') 617 | item_3 = MockModel(foo=3, mock_name='item_3') 618 | 619 | self.mock_set.add(item_2, item_3, item_1, item_3) 620 | results = list(self.mock_set.values('foo').distinct().order_by('foo')) 621 | 622 | expected = [ 623 | {'foo': item_1.foo}, 624 | {'foo': item_2.foo}, 625 | {'foo': item_3.foo}, 626 | ] 627 | assert results == expected, results 628 | 629 | def test_query_distinct_with_fields(self): 630 | item_1 = MockModel(foo=1, bar='c', foo_bar='x', mock_name='item_1') 631 | item_2 = MockModel(foo=2, bar='a', foo_bar='y', mock_name='item_2') 632 | item_3 = MockModel(foo=1, bar='c', foo_bar='z', mock_name='item_3') 633 | 634 | self.mock_set.add(item_2, item_3, item_1, item_3) 635 | results = list(self.mock_set.order_by('foo', 'bar', 'foo_bar').distinct('foo', 'bar')) 636 | 637 | assert results == [item_1, item_2], results 638 | 639 | def test_query_distinct_django_model_with_fields(self): 640 | item_1 = Car(speed=1, model='a') 641 | item_2 = Car(speed=2, model='b') 642 | item_3 = Car(speed=1, model='c') 643 | 644 | self.mock_set.add(item_2, item_3, item_1, item_3) 645 | results = list(self.mock_set.order_by('speed', 'model').distinct('speed')) 646 | 647 | assert results == [item_1, item_2], results 648 | 649 | def test_query_implements_iterator_on_items(self): 650 | items = [1, 2, 3] 651 | assert [x for x in MockSet(*items)] == items 652 | 653 | def test_query_creates_new_model_and_adds_to_set(self): 654 | qs = MockSet(model=create_model('foo', 'bar', 'none')) 655 | attrs = dict(foo=1, bar='a') 656 | obj = qs.create(**attrs) 657 | 658 | assert obj in [x for x in qs] 659 | assert hasattr(obj, 'foo') and obj.foo == 1 660 | assert hasattr(obj, 'bar') and obj.bar == 'a' 661 | assert hasattr(obj, 'none') and obj.none is None 662 | 663 | def test_query_create_raises_model_not_specified_when_mockset_model_is_none(self): 664 | qs = MockSet() 665 | attrs = dict(foo=1, bar='a') 666 | self.assertRaises(ModelNotSpecified, qs.create, **attrs) 667 | 668 | def test_query_create_raises_value_error_when_kwarg_key_is_not_in_concrete_fields(self): 669 | qs = MockSet( 670 | model=create_model('first', 'second', 'third') 671 | ) 672 | attrs = dict(first=1, second=2, third=3, fourth=4) 673 | with self.assertRaises(FieldError): 674 | qs.create(**attrs) 675 | 676 | def test_query_update_returns_number_of_affected_rows(self): 677 | objects = [MockModel(foo=1), MockModel(foo=1), MockModel(foo=2)] 678 | qs = MockSet(*objects, model=create_model('foo', 'bar')) 679 | count = qs.filter(foo=1).update(bar=2) 680 | 681 | assert count == len(objects) - 1, count 682 | 683 | def test_query_update_with_multiple_values(self): 684 | objects = [MockModel(foo=1), MockModel(foo=2), MockModel(foo=3)] 685 | qs = MockSet(*objects, model=create_model('foo', 'bar')) 686 | 687 | set_foo, set_bar = 4, 5 688 | qs.update(foo=set_foo, bar=set_bar) 689 | 690 | for x in qs: 691 | assert x.foo == set_foo, x.foo 692 | assert x.bar == set_bar, x.bar 693 | 694 | def test_query_update_does_not_allow_related_model_fields(self): 695 | objects = [MockModel(foo=MockModel(bar=1)), MockModel(foo=MockModel(bar=2))] 696 | qs = MockSet(*objects, model=create_model('foo')) 697 | 698 | target = dict(foo__bar=2) 699 | with self.assertRaises(FieldError) as cm: 700 | qs.update(**target) 701 | 702 | assert 'Cannot update model field \'{}\''.format(next(iter(target))) in str(cm.exception) 703 | 704 | def test_query_delete_all_entries(self): 705 | item_1 = MockModel(foo=1, bar='a', mock_name='item_1') 706 | item_2 = MockModel(foo=1, bar='b', mock_name='item_2') 707 | 708 | self.mock_set.add(item_1, item_2) 709 | deleted_count, deleted_items = self.mock_set.delete() 710 | 711 | assert len(self.mock_set) == 0, len(self.mock_set) 712 | assert deleted_count == 2 713 | assert deleted_items == {'item_1': 1, 'item_2': 1} 714 | 715 | def test_query_delete_non_model_entries(self): 716 | items = [1, 2, 3, 'foo', 'bar', True] 717 | self.mock_set.add(*items) 718 | 719 | deleted_count, deleted_items = self.mock_set.delete() 720 | assert deleted_count == 6 721 | assert deleted_items == {'int': 3, 'str': 2, 'bool': 1} 722 | 723 | def test_query_delete_entries_propagated_from_nested_qs(self): 724 | item_1 = MockModel(foo=1, bar='a', mock_name='item_1') 725 | item_2 = MockModel(foo=1, bar='b', mock_name='item_2') 726 | 727 | self.mock_set.add(item_1, item_2) 728 | deleted_count, deleted_items = self.mock_set.filter(bar='b').delete() 729 | 730 | assert len(self.mock_set) == 1, len(self.mock_set) 731 | assert item_1 in self.mock_set 732 | assert item_2 not in self.mock_set 733 | assert deleted_count == 1 734 | assert deleted_items == {'item_2': 1} 735 | 736 | def test_query_gets_unique_match_by_attrs_from_set(self): 737 | item_1 = MockModel(foo=1) 738 | item_2 = MockModel(foo=2) 739 | item_3 = MockModel(foo=3) 740 | 741 | self.mock_set.add(item_1, item_2, item_3) 742 | result = self.mock_set.get(foo=2) 743 | 744 | assert item_2 == result 745 | 746 | def test_query_gets_unique_match_by_q_object(self): 747 | item_1 = MockModel(mock_name='#1', foo=1) 748 | item_2 = MockModel(mock_name='#2', foo=2) 749 | item_3 = MockModel(mock_name='#3', foo=3) 750 | 751 | self.mock_set.add(item_1, item_2, item_3) 752 | assert self.mock_set.get(Q(foo=1)) == item_1 753 | 754 | def test_query_get_raises_does_not_exist_when_no_match(self): 755 | item_1 = MockModel(foo=1) 756 | item_2 = MockModel(foo=2) 757 | item_3 = MockModel(foo=3) 758 | 759 | self.mock_set.add(item_1, item_2, item_3) 760 | self.assertRaises(ObjectDoesNotExist, self.mock_set.get, foo=4) 761 | 762 | def test_query_get_raises_specific_exception(self): 763 | item_1 = Car(model='battle') 764 | item_2 = Car(model='pious') 765 | item_3 = Car(model='hummus') 766 | 767 | self.mock_set = MockSet(item_1, item_2, item_3, model=Car) 768 | self.assertRaises(Car.DoesNotExist, self.mock_set.get, model='clowncar') 769 | 770 | def test_query_filter_keeps_class(self): 771 | item_1 = Car(model='battle') 772 | item_2 = Car(model='pious') 773 | item_3 = Car(model='hummus') 774 | 775 | self.mock_set = MockSet(item_1, item_2, item_3, model=Car) 776 | filtered = self.mock_set.filter(model__endswith='s') 777 | self.assertRaises(Car.DoesNotExist, filtered.get, model='clowncar') 778 | 779 | def test_query_get_raises_does_multiple_objects_returned_when_more_than_one_match(self): 780 | item_1 = MockModel(foo=1) 781 | item_2 = MockModel(foo=1) 782 | item_3 = MockModel(foo=2) 783 | 784 | self.mock_set.add(item_1, item_2, item_3) 785 | self.assertRaises(MultipleObjectsReturned, self.mock_set.get, foo=1) 786 | 787 | def test_query_get_or_create_gets_existing_unique_match(self): 788 | item_1 = MockModel(foo=1) 789 | item_2 = MockModel(foo=2) 790 | item_3 = MockModel(foo=3) 791 | 792 | self.mock_set.add(item_1, item_2, item_3) 793 | obj, created = self.mock_set.get_or_create(foo=2) 794 | 795 | assert obj == item_2 796 | assert created is False 797 | 798 | def test_query_get_or_create_raises_does_multiple_objects_returned_when_more_than_one_match(self): 799 | item_1 = MockModel(foo=1) 800 | item_2 = MockModel(foo=1) 801 | item_3 = MockModel(foo=2) 802 | 803 | self.mock_set.add(item_1, item_2, item_3) 804 | self.assertRaises(MultipleObjectsReturned, self.mock_set.get_or_create, foo=1) 805 | 806 | def test_query_get_or_create_creates_new_model_when_no_match(self): 807 | item_1 = MockModel(foo=1) 808 | item_2 = MockModel(foo=2) 809 | item_3 = MockModel(foo=3) 810 | 811 | qs = MockSet(model=create_model('foo')) 812 | qs.add(item_1, item_2, item_3) 813 | obj, created = qs.get_or_create(foo=4) 814 | 815 | assert hasattr(obj, 'foo') and obj.foo == 4 816 | assert created is True 817 | 818 | def test_query_get_or_create_gets_existing_unique_match_with_defaults(self): 819 | qs = MockSet( 820 | model=create_model('first', 'second', 'third') 821 | ) 822 | item_1 = MockModel(first=1) 823 | item_2 = MockModel(second=2) 824 | item_3 = MockModel(third=3) 825 | qs.add(item_1, item_2, item_3) 826 | 827 | obj, created = qs.get_or_create(defaults={'first': 3, 'third': 1}, second=2) 828 | 829 | assert hasattr(obj, 'second') and obj.second == 2 830 | assert created is False 831 | 832 | def test_query_get_or_create_raises_does_multiple_objects_returned_when_more_than_one_match_with_defaults(self): 833 | qs = MockSet( 834 | model=create_model('first', 'second', 'third') 835 | ) 836 | item_1 = MockModel(first=1) 837 | item_2 = MockModel(first=1) 838 | item_3 = MockModel(third=3) 839 | qs.add(item_1, item_2, item_3) 840 | 841 | qs.add(item_1, item_2, item_3) 842 | with self.assertRaises(MultipleObjectsReturned): 843 | qs.get_or_create(first=1, defaults={'second': 2}) 844 | 845 | def test_query_get_or_create_creates_new_model_when_no_match_with_defaults(self): 846 | qs = MockSet( 847 | model=create_model('first', 'second', 'third') 848 | ) 849 | item_1 = MockModel(first=1) 850 | item_2 = MockModel(second=2) 851 | item_3 = MockModel(third=3) 852 | qs.add(item_1, item_2, item_3) 853 | 854 | obj, created = qs.get_or_create(defaults={'first': 3}, second=1) 855 | 856 | assert hasattr(obj, 'first') and obj.first == 3 857 | assert hasattr(obj, 'second') and obj.second == 1 858 | assert hasattr(obj, 'third') and obj.third is None 859 | assert created is True 860 | 861 | def test_query_get_or_create_raises_model_not_specified_with_defaults_when_mockset_model_is_none(self): 862 | qs = MockSet() 863 | item_1 = MockModel(first=1) 864 | item_2 = MockModel(second=2) 865 | item_3 = MockModel(third=3) 866 | qs.add(item_1, item_2, item_3) 867 | 868 | with self.assertRaises(ModelNotSpecified): 869 | qs.get_or_create(defaults={'first': 3, 'third': 2}, second=1) 870 | 871 | def test_query_update_or_create_gets_existing_unique_match(self): 872 | item_1 = MockModel(foo=1) 873 | item_2 = MockModel(foo=2) 874 | item_3 = MockModel(foo=3) 875 | 876 | self.mock_set.add(item_1, item_2, item_3) 877 | obj, created = self.mock_set.update_or_create(foo=2) 878 | 879 | assert obj == item_2 880 | assert created is False 881 | 882 | def test_query_update_or_create_raises_does_multiple_objects_returned_when_more_than_one_match(self): 883 | item_1 = MockModel(foo=1) 884 | item_2 = MockModel(foo=1) 885 | item_3 = MockModel(foo=2) 886 | 887 | self.mock_set.add(item_1, item_2, item_3) 888 | self.assertRaises(MultipleObjectsReturned, self.mock_set.update_or_create, foo=1) 889 | 890 | def test_query_update_or_create_creates_new_model_when_no_match(self): 891 | item_1 = MockModel(foo=1) 892 | item_2 = MockModel(foo=2) 893 | item_3 = MockModel(foo=3) 894 | 895 | qs = MockSet(model=create_model('foo')) 896 | qs.add(item_1, item_2, item_3) 897 | obj, created = qs.update_or_create(foo=4) 898 | 899 | assert hasattr(obj, 'foo') and obj.foo == 4 900 | assert created is True 901 | 902 | def test_query_update_or_create_gets_existing_unique_match_with_defaults(self): 903 | qs = MockSet( 904 | model=create_model('first', 'second', 'third') 905 | ) 906 | item_1 = MockModel(first=1) 907 | item_2 = MockModel(second=2) 908 | item_3 = MockModel(third=3) 909 | qs.add(item_1, item_2, item_3) 910 | 911 | obj, created = qs.update_or_create(defaults={'first': 3, 'third': 1}, second=2) 912 | 913 | assert hasattr(obj, 'second') and obj.second == 2 914 | assert created is False 915 | 916 | def test_query_update_or_create_raises_does_multiple_objects_returned_when_more_than_one_match_with_defaults(self): 917 | qs = MockSet( 918 | model=create_model('first', 'second', 'third') 919 | ) 920 | item_1 = MockModel(first=1) 921 | item_2 = MockModel(first=1) 922 | item_3 = MockModel(third=3) 923 | qs.add(item_1, item_2, item_3) 924 | 925 | qs.add(item_1, item_2, item_3) 926 | with self.assertRaises(MultipleObjectsReturned): 927 | qs.update_or_create(first=1, defaults={'second': 2}) 928 | 929 | def test_query_update_or_create_creates_new_model_when_no_match_with_defaults(self): 930 | qs = MockSet( 931 | model=create_model('first', 'second', 'third') 932 | ) 933 | item_1 = MockModel(first=1) 934 | item_2 = MockModel(second=2) 935 | item_3 = MockModel(third=3) 936 | qs.add(item_1, item_2, item_3) 937 | 938 | obj, created = qs.update_or_create(defaults={'first': 3}, second=1) 939 | 940 | assert hasattr(obj, 'first') and obj.first == 3 941 | assert hasattr(obj, 'second') and obj.second == 1 942 | assert hasattr(obj, 'third') and obj.third is None 943 | assert created is True 944 | 945 | def test_query_update_or_create_updates_match_with_defaults(self): 946 | qs = MockSet( 947 | model=create_model('first', 'second', 'third') 948 | ) 949 | item_1 = MockModel(first=1) 950 | item_2 = MockModel(second=2) 951 | item_3 = MockModel(third=3) 952 | qs.add(item_1, item_2, item_3) 953 | 954 | obj, created = qs.update_or_create(defaults={'first': 3, 'third': 1}, second=2) 955 | 956 | assert hasattr(obj, 'first') and obj.first == 3 957 | assert hasattr(obj, 'second') and obj.second == 2 958 | assert hasattr(obj, 'third') and obj.third == 1 959 | assert created is False 960 | 961 | def test_query_update_or_create_raises_model_not_specified_with_defaults_when_mockset_model_is_none(self): 962 | qs = MockSet() 963 | item_1 = MockModel(first=1) 964 | item_2 = MockModel(second=2) 965 | item_3 = MockModel(third=3) 966 | qs.add(item_1, item_2, item_3) 967 | 968 | with self.assertRaises(ModelNotSpecified): 969 | qs.update_or_create(defaults={'first': 3, 'third': 2}, second=1) 970 | 971 | def test_query_return_self_methods_accept_any_parameters_and_return_instance(self): 972 | qs = MockSet(MockModel(foo=1), MockModel(foo=2)) 973 | assert qs == qs.all() 974 | assert qs == qs.only('f1') 975 | assert qs == qs.defer('f2', 'f3') 976 | assert qs == qs.using('default') 977 | assert qs == qs.select_related('t1', 't2') 978 | assert qs == qs.prefetch_related('t3', 't4') 979 | assert qs == qs.select_for_update() 980 | 981 | def test_query_values_list_raises_type_error_when_kwargs_other_than_flat_specified(self): 982 | qs = MockSet(MockModel(foo=1), MockModel(foo=2)) 983 | self.assertRaises(TypeError, qs.values_list, arg='value') 984 | 985 | def test_query_values_list_raises_type_error_when_flat_specified_with_multiple_fields(self): 986 | qs = MockSet(MockModel(foo=1, bar=1), MockModel(foo=2, bar=2)) 987 | self.assertRaises(TypeError, qs.values_list, 'foo', 'bar', flat=True) 988 | 989 | def test_query_values_list_raises_attribute_error_when_field_is_not_in_meta_concrete_fields(self): 990 | qs = MockSet(MockModel(foo=1), MockModel(foo=2)) 991 | self.assertRaises(FieldError, qs.values_list, 'bar') 992 | 993 | def test_query_values_list(self): 994 | item_1 = MockModel(foo=1, bar=3) 995 | item_2 = MockModel(foo=2, bar=4) 996 | 997 | qs = MockSet(item_1, item_2) 998 | results_flat = qs.values_list('foo', flat=True) 999 | results_single_fields = qs.values_list('foo') 1000 | results_with_fields = qs.values_list('foo', 'bar') 1001 | 1002 | assert results_flat[0] == 1 1003 | assert results_flat[1] == 2 1004 | assert results_single_fields[0] == (1,) 1005 | assert results_single_fields[1] == (2,) 1006 | assert results_with_fields[0] == (1, 3) 1007 | assert results_with_fields[1] == (2, 4) 1008 | 1009 | def test_query_values_list_raises_type_error_if_flat_and_named_are_true(self): 1010 | qs = MockSet(MockModel(foo=1), MockModel(foo=2)) 1011 | self.assertRaises(TypeError, qs.values_list, flat=True, named=True) 1012 | 1013 | def test_named_query_values_list(self): 1014 | item_1 = MockModel(foo=1, bar=3) 1015 | item_2 = MockModel(foo=2, bar=4) 1016 | 1017 | qs = MockSet(item_1, item_2) 1018 | results_with_named_fields_fields = qs.values_list('foo', 'bar', named=True) 1019 | assert results_with_named_fields_fields[0].foo == 1 1020 | assert results_with_named_fields_fields[0].bar == 3 1021 | assert results_with_named_fields_fields[1].foo == 2 1022 | assert results_with_named_fields_fields[1].bar == 4 1023 | 1024 | def test_query_values_list_of_nested_field(self): 1025 | with mocked_relations(Manufacturer, Car): 1026 | make = Manufacturer(name='vw') 1027 | self.mock_set.add(make) 1028 | 1029 | polo = Car(make=make, model='polo', speed=240) 1030 | golf = Car(make=make, model='golf', speed=260) 1031 | 1032 | polo_white = CarVariation(car=polo, color='white') 1033 | golf_white = CarVariation(car=golf, color='white') 1034 | golf_black = CarVariation(car=golf, color='black') 1035 | 1036 | make.car_set = MockSet(polo, golf) 1037 | polo.variations = MockSet(polo_white) 1038 | golf.variations = MockSet(golf_white, golf_black) 1039 | 1040 | data = list(self.mock_set.values_list('name', 'car__model', 'car__variations__color')) 1041 | 1042 | assert (make.name, polo.model, polo_white.color) in data 1043 | assert (make.name, golf.model, golf_black.color) in data 1044 | 1045 | def test_in_bulk(self): 1046 | golf = Car(model='golf', id=1) 1047 | polo = Car(model='polo', id=2) 1048 | kia = Car(model='kia', id=4) 1049 | qs = MockSet(golf, polo, kia) 1050 | 1051 | self.assertEqual(qs.in_bulk(), {1: golf, 2: polo, 4: kia}) 1052 | self.assertEqual(qs.in_bulk(id_list=['kia'], field_name='model'), {'kia': kia}) 1053 | 1054 | def test_annotate(self): 1055 | qs = MockSet(CarVariation(color='green', car=Car(model='golf', id=1), id=1), 1056 | CarVariation(color='red', car=Car(model='polo', id=2), id=2), 1057 | CarVariation(color=None, car=Car(model='kia', id=3), id=3), 1058 | ) 1059 | qs = qs.annotate( 1060 | model=models.F('car__model'), 1061 | str_value=models.Value('data', output_field=models.TextField()), 1062 | bool_value=models.Value(True, output_field=models.BooleanField()), 1063 | int_value=models.Value(10, output_field=models.IntegerField()), 1064 | is_golf=models.Case( 1065 | models.When(car__model='golf', then=True), 1066 | default=False, 1067 | output_field=models.BooleanField() 1068 | ), 1069 | color_or_car=Coalesce('color', models.F('car__model')), 1070 | ) 1071 | 1072 | values_res = list(qs.values('model', 'str_value', 'bool_value', 'int_value', 'is_golf', 'color_or_car')) 1073 | self.assertEqual([ 1074 | { 1075 | 'model': 'golf', 1076 | 'int_value': 10, 1077 | 'str_value': 'data', 1078 | 'bool_value': True, 1079 | 'is_golf': True, 1080 | 'color_or_car': 'green' 1081 | }, 1082 | { 1083 | 'model': 'polo', 1084 | 'int_value': 10, 1085 | 'str_value': 'data', 1086 | 'bool_value': True, 1087 | 'is_golf': False, 1088 | 'color_or_car': 'red' 1089 | }, 1090 | { 1091 | 'model': 'kia', 1092 | 'int_value': 10, 1093 | 'str_value': 'data', 1094 | 'bool_value': True, 1095 | 'is_golf': False, 1096 | 'color_or_car': 'kia' 1097 | }, 1098 | ], values_res) 1099 | 1100 | first = qs[0] 1101 | self.assertEqual(first.model, 'golf') 1102 | self.assertEqual(first.color_or_car, 'green') 1103 | self.assertEqual(first.is_golf, True) 1104 | self.assertEqual(first.int_value, 10) 1105 | self.assertEqual(first.str_value, 'data') 1106 | self.assertEqual(first.bool_value, True) 1107 | 1108 | second = qs[1] 1109 | self.assertEqual(second.model, 'polo') 1110 | self.assertEqual(second.color_or_car, 'red') 1111 | self.assertEqual(second.is_golf, False) 1112 | self.assertEqual(second.int_value, 10) 1113 | self.assertEqual(second.str_value, 'data') 1114 | self.assertEqual(second.bool_value, True) 1115 | 1116 | self.assertEqual(qs[2].color_or_car, 'kia') 1117 | 1118 | def test_annotate_returns_current_class_instance(self): 1119 | class CustomMockSet(MockSet): 1120 | pass 1121 | 1122 | qs = CustomMockSet(Car(model='golf', id=1)) 1123 | self.assertIsInstance(qs.annotate(model=models.F('model')), CustomMockSet) 1124 | 1125 | def test_query_values_raises_attribute_error_when_field_is_not_in_meta_concrete_fields(self): 1126 | qs = MockSet(MockModel(foo=1), MockModel(foo=2)) 1127 | self.assertRaises(FieldError, qs.values, 'bar') 1128 | 1129 | def test_query_values(self): 1130 | item_1 = MockModel(foo=1, bar=3, foobar=5) 1131 | item_2 = MockModel(foo=2, bar=4, foobar=6) 1132 | 1133 | qs = MockSet(item_1, item_2) 1134 | 1135 | results_all = qs.values() 1136 | results_with_fields = qs.values('foo', 'bar') 1137 | 1138 | assert results_all[0]['foo'] == 1 1139 | assert results_all[0]['bar'] == 3 1140 | assert results_all[0]['foobar'] == 5 1141 | assert results_all[1]['foo'] == 2 1142 | assert results_all[1]['bar'] == 4 1143 | assert results_all[1]['foobar'] == 6 1144 | 1145 | assert results_with_fields[0]['foo'] == 1 1146 | assert results_with_fields[0]['bar'] == 3 1147 | assert results_with_fields[1]['foo'] == 2 1148 | assert results_with_fields[1]['bar'] == 4 1149 | 1150 | def test_query_values_of_nested_field(self): 1151 | with mocked_relations(Manufacturer, Car): 1152 | make = Manufacturer(name='vw') 1153 | self.mock_set.add(make) 1154 | 1155 | polo = Car(make=make, model='polo', speed=240) 1156 | golf = Car(make=make, model='golf', speed=260) 1157 | 1158 | polo_white = CarVariation(car=polo, color='white') 1159 | golf_white = CarVariation(car=golf, color='white') 1160 | golf_black = CarVariation(car=golf, color='black') 1161 | 1162 | make.car_set = MockSet(polo, golf) 1163 | polo.variations = MockSet(polo_white) 1164 | golf.variations = MockSet(golf_white, golf_black) 1165 | 1166 | data = list(self.mock_set.values('car__model', 'car__variations__color', 'name')) 1167 | assert {'name': make.name, 'car__model': polo.model, 'car__variations__color': polo_white.color} in data 1168 | assert {'name': make.name, 'car__model': golf.model, 'car__variations__color': golf_white.color} in data 1169 | assert {'name': make.name, 'car__model': golf.model, 'car__variations__color': golf_black.color} in data 1170 | 1171 | def test_query_length1(self): 1172 | q = MockSet(MockModel()) 1173 | 1174 | n = len(q) 1175 | 1176 | self.assertEqual(1, n) 1177 | 1178 | def test_query_length2(self): 1179 | q = MockSet(MockModel(), MockModel()) 1180 | 1181 | n = len(q) 1182 | 1183 | self.assertEqual(2, n) 1184 | 1185 | def test_query_create_model_raises_value_error_with_zero_arguments(self): 1186 | with self.assertRaises(ValueError): 1187 | create_model() 1188 | 1189 | def test_query_model_repr_returns_mock_name(self): 1190 | model = MockModel(mock_name='model_name') 1191 | assert repr(model) == model.mock_name 1192 | 1193 | def test_query_dates_year(self): 1194 | qs = MockSet(model=create_model('date_begin')) 1195 | 1196 | item1 = MockModel(date_begin=datetime.date(2017, 1, 2)) 1197 | item2 = MockModel(date_begin=datetime.date(2017, 3, 12)) 1198 | item3 = MockModel(date_begin=datetime.date(2016, 3, 4)) 1199 | 1200 | qs.add(item1, item2, item3) 1201 | 1202 | result = qs.dates('date_begin', 'year', 'ASC') 1203 | 1204 | assert len(result) == 2 1205 | assert result[0] == datetime.date(2016, 1, 1) 1206 | assert result[1] == datetime.date(2017, 1, 1) 1207 | 1208 | result = qs.dates('date_begin', 'year', 'DESC') 1209 | 1210 | assert len(result) == 2 1211 | assert result[0] == datetime.date(2017, 1, 1) 1212 | assert result[1] == datetime.date(2016, 1, 1) 1213 | 1214 | def test_query_dates_month(self): 1215 | qs = MockSet(model=create_model('date_begin')) 1216 | 1217 | item1 = MockModel(date_begin=datetime.date(2017, 1, 2)) 1218 | item2 = MockModel(date_begin=datetime.date(2017, 1, 19)) 1219 | item3 = MockModel(date_begin=datetime.date(2017, 2, 4)) 1220 | qs.add(item1, item2, item3) 1221 | 1222 | result = qs.dates('date_begin', 'month', 'ASC') 1223 | 1224 | assert len(result) == 2 1225 | assert result[0] == datetime.date(2017, 1, 1) 1226 | assert result[1] == datetime.date(2017, 2, 1) 1227 | 1228 | result = qs.dates('date_begin', 'month', 'DESC') 1229 | 1230 | assert len(result) == 2 1231 | assert result[0] == datetime.date(2017, 2, 1) 1232 | assert result[1] == datetime.date(2017, 1, 1) 1233 | 1234 | def test_query_dates_day(self): 1235 | qs = MockSet(model=create_model('date_begin')) 1236 | 1237 | item1 = MockModel(date_begin=datetime.date(2017, 1, 2)) 1238 | item2 = MockModel(date_begin=datetime.date(2017, 2, 14)) 1239 | item3 = MockModel(date_begin=datetime.date(2017, 2, 14)) 1240 | 1241 | qs.add(item1, item2, item3) 1242 | 1243 | result = qs.dates('date_begin', 'day', 'ASC') 1244 | 1245 | assert len(result) == 2 1246 | assert result[0] == datetime.date(2017, 1, 2) 1247 | assert result[1] == datetime.date(2017, 2, 14) 1248 | 1249 | result = qs.dates('date_begin', 'day', 'DESC') 1250 | 1251 | assert len(result) == 2 1252 | assert result[0] == datetime.date(2017, 2, 14) 1253 | assert result[1] == datetime.date(2017, 1, 2) 1254 | 1255 | def test_query_datetimes_year(self): 1256 | qs = MockSet(model=create_model('date_begin')) 1257 | 1258 | item1 = MockModel(date_begin=datetime.datetime(2017, 1, 2, 1, 2, 3)) 1259 | item2 = MockModel(date_begin=datetime.datetime(2017, 3, 12, 4, 5, 6)) 1260 | item3 = MockModel(date_begin=datetime.datetime(2016, 3, 4, 7, 8, 9)) 1261 | 1262 | qs.add(item1, item2, item3) 1263 | 1264 | result = qs.datetimes('date_begin', 'year', 'ASC') 1265 | 1266 | assert len(result) == 2 1267 | assert result[0] == datetime.datetime(2016, 1, 1, 0, 0, 0) 1268 | assert result[1] == datetime.datetime(2017, 1, 1, 0, 0, 0) 1269 | 1270 | result = qs.datetimes('date_begin', 'year', 'DESC') 1271 | 1272 | assert len(result) == 2 1273 | assert result[0] == datetime.datetime(2017, 1, 1, 0, 0, 0) 1274 | assert result[1] == datetime.datetime(2016, 1, 1, 0, 0, 0) 1275 | 1276 | def test_query_datetimes_month(self): 1277 | qs = MockSet(model=create_model('date_begin')) 1278 | 1279 | item1 = MockModel(date_begin=datetime.datetime(2017, 1, 2, 1, 2, 3)) 1280 | item2 = MockModel(date_begin=datetime.datetime(2017, 1, 19, 4, 5, 6)) 1281 | item3 = MockModel(date_begin=datetime.datetime(2017, 2, 4, 7, 8, 9)) 1282 | qs.add(item1, item2, item3) 1283 | 1284 | result = qs.datetimes('date_begin', 'month', 'ASC') 1285 | 1286 | assert len(result) == 2 1287 | assert result[0] == datetime.datetime(2017, 1, 1, 0, 0, 0) 1288 | assert result[1] == datetime.datetime(2017, 2, 1, 0, 0, 0) 1289 | 1290 | result = qs.datetimes('date_begin', 'month', 'DESC') 1291 | 1292 | assert len(result) == 2 1293 | assert result[0] == datetime.datetime(2017, 2, 1, 0, 0, 0) 1294 | assert result[1] == datetime.datetime(2017, 1, 1, 0, 0, 0) 1295 | 1296 | def test_query_datetimes_day(self): 1297 | qs = MockSet(model=create_model('date_begin')) 1298 | 1299 | item1 = MockModel(date_begin=datetime.datetime(2017, 1, 2, 1, 2, 3)) 1300 | item2 = MockModel(date_begin=datetime.datetime(2017, 2, 14, 4, 5, 6)) 1301 | item3 = MockModel(date_begin=datetime.datetime(2017, 2, 14, 7, 8, 9)) 1302 | 1303 | qs.add(item1, item2, item3) 1304 | 1305 | result = qs.datetimes('date_begin', 'day', 'ASC') 1306 | 1307 | assert len(result) == 2 1308 | assert result[0] == datetime.datetime(2017, 1, 2, 0, 0, 0) 1309 | assert result[1] == datetime.datetime(2017, 2, 14, 0, 0, 0) 1310 | 1311 | result = qs.datetimes('date_begin', 'day', 'DESC') 1312 | 1313 | assert len(result) == 2 1314 | assert result[0] == datetime.datetime(2017, 2, 14, 0, 0, 0) 1315 | assert result[1] == datetime.datetime(2017, 1, 2, 0, 0, 0) 1316 | 1317 | def test_query_datetimes_hour(self): 1318 | qs = MockSet(model=create_model('date_begin')) 1319 | 1320 | item1 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 1, 2, 3)) 1321 | item2 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 1, 5, 6)) 1322 | item3 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 2, 8, 9)) 1323 | 1324 | qs.add(item1, item2, item3) 1325 | 1326 | result = qs.datetimes('date_begin', 'hour', 'ASC') 1327 | 1328 | assert len(result) == 2 1329 | assert result[0] == datetime.datetime(2017, 1, 10, 1, 0, 0) 1330 | assert result[1] == datetime.datetime(2017, 1, 10, 2, 0, 0) 1331 | 1332 | result = qs.datetimes('date_begin', 'hour', 'DESC') 1333 | 1334 | assert len(result) == 2 1335 | assert result[0] == datetime.datetime(2017, 1, 10, 2, 0, 0) 1336 | assert result[1] == datetime.datetime(2017, 1, 10, 1, 0, 0) 1337 | 1338 | def test_query_datetimes_minute(self): 1339 | qs = MockSet(model=create_model('date_begin')) 1340 | 1341 | item1 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 1, 2, 3)) 1342 | item2 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 1, 2, 6)) 1343 | item3 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 1, 3, 9)) 1344 | 1345 | qs.add(item1, item2, item3) 1346 | 1347 | result = qs.datetimes('date_begin', 'minute', 'ASC') 1348 | 1349 | assert len(result) == 2 1350 | assert result[0] == datetime.datetime(2017, 1, 10, 1, 2, 0) 1351 | assert result[1] == datetime.datetime(2017, 1, 10, 1, 3, 0) 1352 | 1353 | result = qs.datetimes('date_begin', 'minute', 'DESC') 1354 | 1355 | assert len(result) == 2 1356 | assert result[0] == datetime.datetime(2017, 1, 10, 1, 3, 0) 1357 | assert result[1] == datetime.datetime(2017, 1, 10, 1, 2, 0) 1358 | 1359 | def test_query_datetimes_second(self): 1360 | qs = MockSet(model=create_model('date_begin')) 1361 | 1362 | item1 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 1, 2, 3)) 1363 | item2 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 1, 2, 3)) 1364 | item3 = MockModel(date_begin=datetime.datetime(2017, 1, 10, 1, 2, 9)) 1365 | 1366 | qs.add(item1, item2, item3) 1367 | 1368 | result = qs.datetimes('date_begin', 'second', 'ASC') 1369 | 1370 | assert len(result) == 2 1371 | assert result[0] == datetime.datetime(2017, 1, 10, 1, 2, 3) 1372 | assert result[1] == datetime.datetime(2017, 1, 10, 1, 2, 9) 1373 | 1374 | result = qs.datetimes('date_begin', 'second', 'DESC') 1375 | 1376 | assert len(result) == 2 1377 | assert result[0] == datetime.datetime(2017, 1, 10, 1, 2, 9) 1378 | assert result[1] == datetime.datetime(2017, 1, 10, 1, 2, 3) 1379 | 1380 | def test_empty_queryset_bool_converts_to_false(self): 1381 | qs = MockSet() 1382 | assert not bool(qs) 1383 | 1384 | def test_empty_queryset_filter(self): 1385 | car1 = Car(id=101) 1386 | car2 = Car(id=102) 1387 | 1388 | mockset = MockSet(car1, car2) 1389 | self.assertEqual(mockset.count(), 2) 1390 | self.assertEqual(mockset.filter(Q()).count(), 2) 1391 | 1392 | def test_mock_set_annotation_by_nested_mock_model(self): 1393 | mockset = MockSet( 1394 | MockModel(id=1, nested_mock=MockModel(id=1, field1="field_value")) 1395 | ) 1396 | field1 = mockset.annotate(field1=models.F("nested_mock__field1")).values_list("field1")[0][0] 1397 | assert field1 == "field_value" 1398 | 1399 | def test_set_replaces_all_items(self): 1400 | mockset = MockSet( 1401 | MockModel(id=1, field="value_1", mock_name="item1"), 1402 | MockModel(id=2, field="value_2", mock_name="item2"), 1403 | ) 1404 | mockset.set([MockModel(id=3, field="value_3", mock_name="item3")]) 1405 | 1406 | assert len(mockset) == 1 1407 | assert mockset[0].id == 3 1408 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | from unittest import TestCase 3 | from unittest.mock import patch, MagicMock 4 | 5 | from django_mock_queries import utils, constants 6 | 7 | 8 | class TestUtils(TestCase): 9 | def test_merge_concatenates_lists(self): 10 | l1 = [1, 2, 3] 11 | l2 = [4, 5, 6] 12 | result = utils.merge(l1, l2) 13 | for x in (l1 + l2): 14 | assert x in result 15 | 16 | def test_merge_eliminates_duplicate_entries(self): 17 | l1 = [1, 2] 18 | l2 = [2, 3] 19 | result = utils.merge(l1, l2) 20 | for x in (l1 + l2): 21 | assert result.count(x) == 1 22 | 23 | def test_intersect_creates_list_with_common_elements(self): 24 | l1 = [1, 2] 25 | l2 = [2, 3] 26 | result = utils.intersect(l1, l2) 27 | for x in (l1 + l2): 28 | if x in l1 and x in l2: 29 | assert x in result 30 | else: 31 | assert x not in result 32 | 33 | def test_get_attribute_returns_value_with_default_comparison(self): 34 | obj = MagicMock(foo='test') 35 | value, comparison = utils.get_attribute(obj, 'foo') 36 | assert value == 'test' 37 | assert comparison is None 38 | 39 | def test_get_attribute_returns_false_when_value_is_false(self): 40 | obj = MagicMock(foo=False) 41 | value, comparison = utils.get_attribute(obj, 'foo') 42 | assert value is False 43 | assert comparison is None 44 | 45 | def test_get_attribute_returns_value_with_defined_comparison(self): 46 | obj = MagicMock(foo='test') 47 | value, comparison = utils.get_attribute(obj, 'foo__' + constants.COMPARISON_IEXACT) 48 | assert value == 'test' 49 | assert comparison == constants.COMPARISON_IEXACT 50 | 51 | def test_get_attribute_returns_none_with_isnull_comparison(self): 52 | obj = MagicMock(foo=None) 53 | value, comparison = utils.get_attribute(obj, 'foo__' + constants.COMPARISON_ISNULL) 54 | assert value is None 55 | assert comparison == constants.COMPARISON_ISNULL, comparison 56 | 57 | def test_get_attribute_returns_nested_object_value(self): 58 | obj = MagicMock(child=MagicMock(foo='test')) 59 | value, comparison = utils.get_attribute(obj, 'child__foo__' + constants.COMPARISON_IEXACT) 60 | assert value == 'test' 61 | assert comparison == constants.COMPARISON_IEXACT 62 | 63 | def test_get_attribute_returns_default_value_when_object_is_none(self): 64 | obj = None 65 | default_value = '' 66 | value, comparison = utils.get_attribute(obj, 'foo', default_value) 67 | assert value == default_value 68 | assert comparison is None 69 | 70 | def test_get_attribute_with_date(self): 71 | obj = MagicMock(foo=date(2017, 12, 31)) 72 | value, comparison = utils.get_attribute( 73 | obj, 'foo__' + constants.COMPARISON_YEAR + '__' + constants.COMPARISON_GT 74 | ) 75 | assert value == date(2017, 12, 31) 76 | assert comparison == (constants.COMPARISON_YEAR, constants.COMPARISON_GT) 77 | 78 | def test_get_attribute_returns_tuple_with_exact_as_default_comparison(self): 79 | obj = MagicMock(foo=datetime(2017, 1, 1)) 80 | value, comparison = utils.get_attribute(obj, 'foo__' + constants.COMPARISON_YEAR) 81 | assert value == datetime(2017, 1, 1) 82 | assert comparison == (constants.COMPARISON_YEAR, constants.COMPARISON_EXACT) 83 | 84 | def test_validate_date_or_datetime_raises_value_error(self): 85 | with self.assertRaisesRegex(ValueError, r'13 is incorrect value for month'): 86 | utils.validate_date_or_datetime(13, constants.COMPARISON_MONTH) 87 | 88 | def test_is_match_equality_check_when_comparison_none(self): 89 | result = utils.is_match(1, 1) 90 | assert result is True 91 | 92 | result = utils.is_match('a', 'a') 93 | assert result is True 94 | 95 | result = utils.is_match(1, '1') 96 | assert result is False 97 | 98 | def test_is_match_case_sensitive_equality_check(self): 99 | result = utils.is_match('a', 'A', constants.COMPARISON_EXACT) 100 | assert result is False 101 | 102 | result = utils.is_match('a', 'a', constants.COMPARISON_EXACT) 103 | assert result is True 104 | 105 | def test_is_match_case_insensitive_equality_check(self): 106 | result = utils.is_match('a', 'A', constants.COMPARISON_IEXACT) 107 | assert result is True 108 | 109 | result = utils.is_match('a', 'a', constants.COMPARISON_IEXACT) 110 | assert result is True 111 | 112 | def test_is_match_case_sensitive_contains_check(self): 113 | result = utils.is_match('abc', 'A', constants.COMPARISON_CONTAINS) 114 | assert result is False 115 | 116 | result = utils.is_match('abc', 'a', constants.COMPARISON_CONTAINS) 117 | assert result is True 118 | 119 | def test_is_match_case_list_contains_check(self): 120 | result = utils.is_match([1, 2, 3], [1, 2], constants.COMPARISON_CONTAINS) 121 | assert result is True 122 | 123 | result = utils.is_match([1, 2, 3], [1, 4], constants.COMPARISON_CONTAINS) 124 | assert result is False 125 | 126 | result = utils.is_match((1, 2, 3), (1, 2), constants.COMPARISON_CONTAINS) 127 | assert result is True 128 | 129 | result = utils.is_match((1, 2, 3), (1, 2, 3, 4), constants.COMPARISON_CONTAINS) 130 | assert result is False 131 | 132 | def test_is_match_case_insensitive_contains_check(self): 133 | result = utils.is_match('abc', 'A', constants.COMPARISON_ICONTAINS) 134 | assert result is True 135 | 136 | result = utils.is_match('abc', 'a', constants.COMPARISON_ICONTAINS) 137 | assert result is True 138 | 139 | def test_is_match_startswith_check(self): 140 | result = utils.is_match('abc', 'a', constants.COMPARISON_STARTSWITH) 141 | assert result is True 142 | 143 | result = utils.is_match('abc', 'A', constants.COMPARISON_STARTSWITH) 144 | assert result is False 145 | 146 | def test_is_match_istartswith_check(self): 147 | result = utils.is_match('abc', 'a', constants.COMPARISON_ISTARTSWITH) 148 | assert result is True 149 | 150 | result = utils.is_match('abc', 'A', constants.COMPARISON_ISTARTSWITH) 151 | assert result is True 152 | 153 | def test_is_match_endswith_check(self): 154 | result = utils.is_match('abc', 'c', constants.COMPARISON_ENDSWITH) 155 | assert result is True 156 | 157 | result = utils.is_match('abc', 'C', constants.COMPARISON_ENDSWITH) 158 | assert result is False 159 | 160 | def test_is_match_iendswith_check(self): 161 | result = utils.is_match('abc', 'c', constants.COMPARISON_IENDSWITH) 162 | assert result is True 163 | 164 | result = utils.is_match('abc', 'C', constants.COMPARISON_IENDSWITH) 165 | assert result is True 166 | 167 | def test_is_match_greater_than_value_check(self): 168 | result = utils.is_match(5, 3, constants.COMPARISON_GT) 169 | assert result is True 170 | 171 | result = utils.is_match(3, 5, constants.COMPARISON_GT) 172 | assert result is False 173 | 174 | result = utils.is_match(None, 5, constants.COMPARISON_GT) 175 | assert result is False 176 | 177 | result = utils.is_match(0, -2, constants.COMPARISON_GT) 178 | assert result is True 179 | 180 | def test_is_match_greater_than_equal_to_value_check(self): 181 | result = utils.is_match(5, 3, constants.COMPARISON_GTE) 182 | assert result is True 183 | 184 | result = utils.is_match(5, 5, constants.COMPARISON_GTE) 185 | assert result is True 186 | 187 | result = utils.is_match(3, 5, constants.COMPARISON_GTE) 188 | assert result is False 189 | 190 | result = utils.is_match(None, 5, constants.COMPARISON_GTE) 191 | assert result is False 192 | 193 | def test_is_match_less_than_value_check(self): 194 | result = utils.is_match(1, 2, constants.COMPARISON_LT) 195 | assert result is True 196 | 197 | result = utils.is_match(2, 2, constants.COMPARISON_LT) 198 | assert result is False 199 | 200 | result = utils.is_match(None, 5, constants.COMPARISON_LT) 201 | assert result is False 202 | 203 | def test_is_match_less_than_equal_to_value_check(self): 204 | result = utils.is_match(1, 2, constants.COMPARISON_LTE) 205 | assert result is True 206 | 207 | result = utils.is_match(1, 1, constants.COMPARISON_LTE) 208 | assert result is True 209 | 210 | result = utils.is_match(2, 1, constants.COMPARISON_LTE) 211 | assert result is False 212 | 213 | result = utils.is_match(None, 5, constants.COMPARISON_LTE) 214 | assert result is False 215 | 216 | def test_is_match_isnull_check(self): 217 | result = utils.is_match(1, True, constants.COMPARISON_ISNULL) 218 | assert result is False 219 | 220 | result = utils.is_match(1, False, constants.COMPARISON_ISNULL) 221 | assert result is True 222 | 223 | result = utils.is_match(None, True, constants.COMPARISON_ISNULL) 224 | assert result is True 225 | 226 | result = utils.is_match(None, False, constants.COMPARISON_ISNULL) 227 | assert result is False 228 | 229 | result = utils.is_match(None, 1, constants.COMPARISON_ISNULL) 230 | assert result is True 231 | 232 | def test_is_match_in_value_check(self): 233 | result = utils.is_match(2, [1, 3], constants.COMPARISON_IN) 234 | assert result is False 235 | 236 | result = utils.is_match(1, [1, 3], constants.COMPARISON_IN) 237 | assert result is True 238 | 239 | result = utils.is_match([3], [1, 2], constants.COMPARISON_IN) 240 | assert result is False 241 | 242 | result = utils.is_match([1, 3], [1, 2], constants.COMPARISON_IN) 243 | assert result is True 244 | 245 | @patch('django_mock_queries.utils.get_attribute') 246 | @patch('django_mock_queries.utils.is_match', MagicMock(return_value=True)) 247 | def test_matches_includes_object_in_results_when_match(self, get_attr_mock): 248 | source = [ 249 | MagicMock(foo=1), 250 | MagicMock(foo=2), 251 | ] 252 | 253 | get_attr_mock.return_value = None, None 254 | results = utils.matches(*source, foo__gt=0) 255 | 256 | for x in source: 257 | assert x in results 258 | 259 | @patch('django_mock_queries.utils.get_attribute') 260 | @patch('django_mock_queries.utils.is_match', MagicMock(return_value=False)) 261 | def test_matches_excludes_object_from_results_when_not_match(self, get_attr_mock): 262 | source = [ 263 | MagicMock(foo=1), 264 | MagicMock(foo=2), 265 | ] 266 | 267 | get_attr_mock.return_value = None, None 268 | results = utils.matches(*source, foo__gt=5) 269 | 270 | for x in source: 271 | assert x not in results 272 | 273 | def test_is_match_regex(self): 274 | result = utils.is_match('Monty Python 1234', r'M\w+\sPython\s\d+', constants.COMPARISON_REGEX) 275 | assert result is True 276 | 277 | result = utils.is_match('Monty Python 1234', r'm\w+\spython\s\d+', constants.COMPARISON_REGEX) 278 | assert result is False 279 | 280 | result = utils.is_match('Monty Python 1234', r'm\w+Holy Grail\s\d+', constants.COMPARISON_REGEX) 281 | assert result is False 282 | 283 | def test_is_match_iregex(self): 284 | result = utils.is_match('Monty Python 1234', r'M\w+\sPython\s\d+', constants.COMPARISON_IREGEX) 285 | assert result is True 286 | 287 | result = utils.is_match('Monty Python 1234', r'm\w+\spython\s\d+', constants.COMPARISON_IREGEX) 288 | assert result is True 289 | 290 | result = utils.is_match('Monty Python 1234', r'm\w+Holy Grail\s\d+', constants.COMPARISON_IREGEX) 291 | assert result is False 292 | 293 | def test_is_match_processes_datetime_field(self): 294 | result = utils.is_match(datetime(2017, 1, 1, 2, 3, 4), 1, (constants.COMPARISON_HOUR, constants.COMPARISON_LT)) 295 | assert result is False 296 | 297 | def test_is_match_processes_date_field(self): 298 | result = utils.is_match(date(2017, 1, 1), 2016, (constants.COMPARISON_YEAR, constants.COMPARISON_GT)) 299 | assert result is True 300 | 301 | def test_is_match_range_date_and_datetime(self): 302 | result = utils.is_match(date(2017, 1, 1), (date(2017, 1, 1), date(2017, 1, 2)), constants.COMPARISON_RANGE) 303 | assert result is True 304 | 305 | result = utils.is_match( 306 | datetime(2017, 1, 1, 0, 0, 0), 307 | (datetime(2017, 1, 1, 0, 0, 0), datetime(2017, 1, 1, 0, 0, 1)), 308 | constants.COMPARISON_RANGE 309 | ) 310 | assert result is True 311 | 312 | result = utils.is_match(date(2017, 1, 1), (date(2017, 1, 2), date(2017, 1, 3)), constants.COMPARISON_RANGE) 313 | assert result is False 314 | 315 | result = utils.is_match( 316 | datetime(2015, 1, 1, 0, 0, 0), 317 | (datetime(2015, 1, 1, 0, 0, 1), datetime(2015, 1, 1, 0, 0, 2)), 318 | constants.COMPARISON_RANGE 319 | ) 320 | assert result is False 321 | 322 | def test_is_match_range_numeric(self): 323 | result = utils.is_match(2, (2, 3), constants.COMPARISON_RANGE) 324 | assert result is True 325 | 326 | result = utils.is_match(1, (2, 3), constants.COMPARISON_RANGE) 327 | assert result is False 328 | 329 | def test_is_match_range_string(self): 330 | result = utils.is_match('b', ('b', 'c'), constants.COMPARISON_RANGE) 331 | assert result is True 332 | 333 | result = utils.is_match('a', ('b', 'c'), constants.COMPARISON_RANGE) 334 | assert result is False 335 | 336 | def test_is_match_overlap_string(self): 337 | result = utils.is_match(['a', 'b'], ['a', 'c'], constants.COMPARISON_OVERLAP) 338 | assert result is True 339 | 340 | result = utils.is_match(['a', 'b'], ['c', 'd'], constants.COMPARISON_OVERLAP) 341 | assert result is False 342 | 343 | def test_is_match_overlap_int(self): 344 | result = utils.is_match([1, 2], [1, 3], constants.COMPARISON_OVERLAP) 345 | assert result is True 346 | 347 | result = utils.is_match([1, 2], [3, 4], constants.COMPARISON_OVERLAP) 348 | assert result is False 349 | 350 | def test_matches_with_range(self): 351 | source = [ 352 | MagicMock(foo=1), 353 | MagicMock(foo=3), 354 | ] 355 | 356 | results = utils.matches(*source, foo__range=(1, 2)) 357 | assert source[0] in results 358 | assert source[1] not in results 359 | 360 | def test_is_match_with_overlap_strings(self): 361 | source = [ 362 | MagicMock(foo=['abc', 'def', 'ghi']), 363 | MagicMock(foo=['jkl', 'mno', 'pqr']), 364 | ] 365 | 366 | results = utils.matches(*source, foo__overlap=["abc", "xyz"]) 367 | assert source[0] in results 368 | assert source[1] not in results 369 | 370 | def test_is_like_date_or_datetime_for_datetime_obj(self): 371 | result = utils.is_like_date_or_datetime(date(2019, 1, 1)) 372 | assert result is True 373 | 374 | def test_is_like_date_or_datetime_for_date_obj(self): 375 | result = utils.is_like_date_or_datetime(datetime(2019, 1, 1)) 376 | assert result is True 377 | 378 | def test_is_like_date_or_datetime_for_non_date_or_datetime_obj(self): 379 | result = utils.is_like_date_or_datetime('non_datetime_obj') 380 | assert result is False 381 | 382 | def test_get_field_value_returns_dict_value(self): 383 | dict_obj = {'key': 45} 384 | result = utils.get_field_value(dict_obj, 'key') 385 | assert result == 45 386 | 387 | def test_get_field_value_returns_datetime_obj(self): 388 | datetime_obj = datetime(2019, 1, 2) 389 | result = utils.get_field_value(datetime_obj, 'date') 390 | assert result == datetime_obj 391 | 392 | def test_get_field_value_returns_date_obj(self): 393 | date_obj = date(2019, 1, 2) 394 | result = utils.get_field_value(date_obj, 'date') 395 | assert result == date_obj 396 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # See https://docs.djangoproject.com/en/5.0/faq/install/#what-python-version-can-i-use-with-django 3 | # EOL: https://endoflife.date/django 4 | envlist = 5 | py{38,39}-dj22-drf{311,313} 6 | py{38,39,310}-dj32-drf{314} 7 | py{38,39,310,311,312}-dj42-drf{314} 8 | py{310,311,312}-dj50-drf{314} 9 | 10 | [pytest] 11 | norecursedirs = examples 12 | 13 | [flake8] 14 | ignore = F403 F405 E731 15 | max-line-length = 120 16 | 17 | [testenv] 18 | download = true 19 | constrain_package_deps = true 20 | use_frozen_constraints = false 21 | deps = 22 | -rrequirements/dev.txt 23 | dj22: Django~=2.2.1 24 | dj22: pytest-django~=4.5.2 25 | dj32: Django~=3.2.0 26 | dj32: pytest-django~=4.5.2 27 | dj42: Django~=4.2.9 28 | dj42: pytest-django~=4.5.2 29 | dj50: Django~=5.0.1 30 | dj50: pytest-django~=4.5.2 31 | drf311: djangorestframework~=3.11.2 32 | drf313: djangorestframework~=3.13.1 33 | drf314: djangorestframework~=3.14.0 34 | 35 | commands = 36 | pytest django_mock_queries/ tests/ --cov-report term-missing --cov=django_mock_queries 37 | python -c "import subprocess; subprocess.check_call(['./manage.py', 'test', '--settings=users.settings_mocked'], cwd='examples/users')" 38 | python -c "import subprocess; subprocess.check_call(['./manage.py', 'test'], cwd='examples/users')" 39 | python -c "import subprocess; subprocess.check_call(['pytest', '--ds=users.settings_mocked'], cwd='examples/users')" 40 | python -c "import subprocess; subprocess.check_call(['pytest'], cwd='examples/users')" 41 | flake8 django_mock_queries/ tests/ 42 | --------------------------------------------------------------------------------