summaryrefslogtreecommitdiff
path: root/python-pytest-drf.spec
diff options
context:
space:
mode:
Diffstat (limited to 'python-pytest-drf.spec')
-rw-r--r--python-pytest-drf.spec979
1 files changed, 979 insertions, 0 deletions
diff --git a/python-pytest-drf.spec b/python-pytest-drf.spec
new file mode 100644
index 0000000..e9a12a8
--- /dev/null
+++ b/python-pytest-drf.spec
@@ -0,0 +1,979 @@
+%global _empty_manifest_terminate_build 0
+Name: python-pytest-drf
+Version: 1.1.3
+Release: 1
+Summary: A Django REST framework plugin for pytest.
+License: MIT
+URL: https://github.com/theY4Kman/pytest-drf
+Source0: https://mirrors.nju.edu.cn/pypi/web/packages/74/d5/af02389a21bbe799f716da6529c5714c4a000fb2c5ae6e0654ebebc94dca/pytest-drf-1.1.3.tar.gz
+BuildArch: noarch
+
+Requires: python3-djangorestframework
+Requires: python3-inflection
+Requires: python3-pytest
+Requires: python3-pytest-assert-utils
+Requires: python3-pytest-common-subject
+Requires: python3-pytest-lambda
+Requires: python3-typing_extensions
+
+%description
+```
+Yaaaaay!
+## Putting it all together
+```python
+# tests/test_kv.py
+from typing import Any, Dict
+from pytest_common_subject import precondition_fixture
+from pytest_drf import (
+ ViewSetTest,
+ Returns200,
+ Returns201,
+ Returns204,
+ UsesGetMethod,
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ UsesListEndpoint,
+ UsesPatchMethod,
+ UsesPostMethod,
+)
+from pytest_drf.util import pluralized, url_for
+from pytest_lambda import lambda_fixture, static_fixture
+from pytest_assert_utils import assert_model_attrs
+def express_key_value(kv: KeyValue) -> Dict[str, Any]:
+ return {
+ 'id': kv.id,
+ 'key': kv.key,
+ 'value': kv.value,
+ }
+express_key_values = pluralized(express_key_value)
+class TestKeyValueViewSet(ViewSetTest):
+ list_url = lambda_fixture(
+ lambda:
+ url_for('key-values-list'))
+ detail_url = lambda_fixture(
+ lambda key_value:
+ url_for('key-values-detail', key_value.pk))
+ class TestList(
+ UsesGetMethod,
+ UsesListEndpoint,
+ Returns200,
+ ):
+ key_values = lambda_fixture(
+ lambda: [
+ KeyValue.objects.create(key=key, value=value)
+ for key, value in {
+ 'quay': 'worth',
+ 'chi': 'revenue',
+ 'umma': 'gumma',
+ }.items()
+ ],
+ autouse=True,
+ )
+ def test_it_returns_key_values(self, key_values, results):
+ expected = express_key_values(sorted(key_values, key=lambda kv: kv.id))
+ actual = results
+ assert expected == actual
+ class TestCreate(
+ UsesPostMethod,
+ UsesListEndpoint,
+ Returns201,
+ ):
+ data = static_fixture({
+ 'key': 'snakes',
+ 'value': 'hissssssss',
+ })
+ initial_key_value_ids = precondition_fixture(
+ lambda:
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def test_it_creates_new_key_value(self, initial_key_value_ids, json):
+ expected = initial_key_value_ids | {json['id']}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+ def test_it_sets_expected_attrs(self, data, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def test_it_returns_key_value(self, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestRetrieve(
+ UsesGetMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='monty',
+ value='jython',
+ ))
+ def test_it_returns_key_value(self, key_value, json):
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestUpdate(
+ UsesPatchMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='pipenv',
+ value='was a huge leap forward',
+ ))
+ data = static_fixture({
+ 'key': 'buuut poetry',
+ 'value': 'locks quicker and i like that',
+ })
+ def test_it_sets_expected_attrs(self, data, key_value):
+ # We must tell Django to grab fresh data from the database, or we'll
+ # see our stale initial data and think our endpoint is broken!
+ key_value.refresh_from_db()
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def test_it_returns_key_value(self, key_value, json):
+ key_value.refresh_from_db()
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestDestroy(
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ Returns204,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='i love',
+ value='YOU',
+ ))
+ initial_key_value_ids = precondition_fixture(
+ lambda key_value: # ensure our to-be-deleted KeyValue exists in our set
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def test_it_deletes_key_value(self, initial_key_value_ids, key_value):
+ expected = initial_key_value_ids - {key_value.id}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+```
+It's quite a feat!
+Now, we tested an already-existing endpoint here, just for demonstration purposes. But there's a bigger advantage to performing one request per test, and having a single responsibility for each test: we can write the tests first and incrementally build the ViewSet. We run the tests on changes, and when they're all green, we know the endpoint is done.
+The beauty of the tests-first methodology is that it frees us up to be creative. Because we have a definite end condition, we can experiment with better implementations — more maintainable, easier to read, using best practices, perhaps leaning on a third-party package for heavy lifting.
+Well, congratulations if you've made it this far. I hope you may find some value in this library, or even from some conventions in these example tests. Good luck out there, and remember: readability counts — in tests, doubly so.
+## Bonus: BDD syntax
+Personally, I like to use `DescribeKeyValueViewSet`, and `DescribeList`, `DescribeCreate`, etc for my test classes. If I'm testing `DescribeCreate` as a particular user, I like to use, e.g., `ContextAsAdmin`. Sometimes `CaseUnauthenticated` hits the spot.
+And for test methods, I love to omit the `test` in `test_it_does_xyz`, and simply put `it_does_xyz`.
+To appease my leanings toward BDD namings, I use a `pytest.ini` with these options:
+```ini
+[pytest]
+# Only search for tests within files matching these patterns
+python_files = tests.py test_*.py
+# Discover tests within classes matching these patterns
+# NOTE: the dash represents a word boundary (functionality provided by pytest-camel-collect)
+python_classes = Test-* Describe-* Context-* With-* Without-* For-* When-* If-* Case-*
+# Only methods matching these patterns are considered tests
+python_functions = test_* it_* its_*
+```
+About the dashes in `python_classes`: sometimes I'll name a test class `ForAdminUsers`. If I had the pattern `For*`, it would also match a pytest-drf mixin named `ForbidsAnonymousUsers`. [pytest-camel-collect](https://github.com/theY4Kman/pytest-camel-collect) is a little plugin that interprets dashes in `python_classes` as CamelCase word boundaries. However, similar behavior can be had on stock pytest using a pattern like `For[A-Z0-9]*`.
+Here's what our example `KeyValueViewSet` test would look like with this BDD naming scheme
+<details>
+<summary>BDD-esque KeyValueViewSet test</summary>
+```python
+from typing import Any, Dict
+from pytest_common_subject import precondition_fixture
+from pytest_drf import (
+ ViewSetTest,
+ Returns200,
+ Returns201,
+ Returns204,
+ UsesGetMethod,
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ UsesListEndpoint,
+ UsesPatchMethod,
+ UsesPostMethod,
+)
+from pytest_drf.util import pluralized, url_for
+from pytest_lambda import lambda_fixture, static_fixture
+from pytest_assert_utils import assert_model_attrs
+def express_key_value(kv: KeyValue) -> Dict[str, Any]:
+ return {
+ 'id': kv.id,
+ 'key': kv.key,
+ 'value': kv.value,
+ }
+express_key_values = pluralized(express_key_value)
+class DescribeKeyValueViewSet(ViewSetTest):
+ list_url = lambda_fixture(
+ lambda:
+ url_for('key-values-list'))
+ detail_url = lambda_fixture(
+ lambda key_value:
+ url_for('key-values-detail', key_value.pk))
+ class DescribeList(
+ UsesGetMethod,
+ UsesListEndpoint,
+ Returns200,
+ ):
+ key_values = lambda_fixture(
+ lambda: [
+ KeyValue.objects.create(key=key, value=value)
+ for key, value in {
+ 'quay': 'worth',
+ 'chi': 'revenue',
+ 'umma': 'gumma',
+ }.items()
+ ],
+ autouse=True,
+ )
+ def it_returns_key_values(self, key_values, results):
+ expected = express_key_values(sorted(key_values, key=lambda kv: kv.id))
+ actual = results
+ assert expected == actual
+ class DescribeCreate(
+ UsesPostMethod,
+ UsesListEndpoint,
+ Returns201,
+ ):
+ data = static_fixture({
+ 'key': 'snakes',
+ 'value': 'hissssssss',
+ })
+ initial_key_value_ids = precondition_fixture(
+ lambda:
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def it_creates_new_key_value(self, initial_key_value_ids, json):
+ expected = initial_key_value_ids | {json['id']}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+ def it_sets_expected_attrs(self, data, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def it_returns_key_value(self, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeRetrieve(
+ UsesGetMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='monty',
+ value='jython',
+ ))
+ def it_returns_key_value(self, key_value, json):
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeUpdate(
+ UsesPatchMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='pipenv',
+ value='was a huge leap forward',
+ ))
+ data = static_fixture({
+ 'key': 'buuut poetry',
+ 'value': 'locks quicker and i like that',
+ })
+ def it_sets_expected_attrs(self, data, key_value):
+ # We must tell Django to grab fresh data from the database, or we'll
+ # see our stale initial data and think our endpoint is broken!
+ key_value.refresh_from_db()
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def it_returns_key_value(self, key_value, json):
+ key_value.refresh_from_db()
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeDestroy(
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ Returns204,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='i love',
+ value='YOU',
+ ))
+ initial_key_value_ids = precondition_fixture(
+ lambda key_value: # ensure our to-be-deleted KeyValue exists in our set
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def it_deletes_key_value(self, initial_key_value_ids, key_value):
+ expected = initial_key_value_ids - {key_value.id}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+```
+</details>
+
+%package -n python3-pytest-drf
+Summary: A Django REST framework plugin for pytest.
+Provides: python-pytest-drf
+BuildRequires: python3-devel
+BuildRequires: python3-setuptools
+BuildRequires: python3-pip
+%description -n python3-pytest-drf
+```
+Yaaaaay!
+## Putting it all together
+```python
+# tests/test_kv.py
+from typing import Any, Dict
+from pytest_common_subject import precondition_fixture
+from pytest_drf import (
+ ViewSetTest,
+ Returns200,
+ Returns201,
+ Returns204,
+ UsesGetMethod,
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ UsesListEndpoint,
+ UsesPatchMethod,
+ UsesPostMethod,
+)
+from pytest_drf.util import pluralized, url_for
+from pytest_lambda import lambda_fixture, static_fixture
+from pytest_assert_utils import assert_model_attrs
+def express_key_value(kv: KeyValue) -> Dict[str, Any]:
+ return {
+ 'id': kv.id,
+ 'key': kv.key,
+ 'value': kv.value,
+ }
+express_key_values = pluralized(express_key_value)
+class TestKeyValueViewSet(ViewSetTest):
+ list_url = lambda_fixture(
+ lambda:
+ url_for('key-values-list'))
+ detail_url = lambda_fixture(
+ lambda key_value:
+ url_for('key-values-detail', key_value.pk))
+ class TestList(
+ UsesGetMethod,
+ UsesListEndpoint,
+ Returns200,
+ ):
+ key_values = lambda_fixture(
+ lambda: [
+ KeyValue.objects.create(key=key, value=value)
+ for key, value in {
+ 'quay': 'worth',
+ 'chi': 'revenue',
+ 'umma': 'gumma',
+ }.items()
+ ],
+ autouse=True,
+ )
+ def test_it_returns_key_values(self, key_values, results):
+ expected = express_key_values(sorted(key_values, key=lambda kv: kv.id))
+ actual = results
+ assert expected == actual
+ class TestCreate(
+ UsesPostMethod,
+ UsesListEndpoint,
+ Returns201,
+ ):
+ data = static_fixture({
+ 'key': 'snakes',
+ 'value': 'hissssssss',
+ })
+ initial_key_value_ids = precondition_fixture(
+ lambda:
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def test_it_creates_new_key_value(self, initial_key_value_ids, json):
+ expected = initial_key_value_ids | {json['id']}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+ def test_it_sets_expected_attrs(self, data, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def test_it_returns_key_value(self, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestRetrieve(
+ UsesGetMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='monty',
+ value='jython',
+ ))
+ def test_it_returns_key_value(self, key_value, json):
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestUpdate(
+ UsesPatchMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='pipenv',
+ value='was a huge leap forward',
+ ))
+ data = static_fixture({
+ 'key': 'buuut poetry',
+ 'value': 'locks quicker and i like that',
+ })
+ def test_it_sets_expected_attrs(self, data, key_value):
+ # We must tell Django to grab fresh data from the database, or we'll
+ # see our stale initial data and think our endpoint is broken!
+ key_value.refresh_from_db()
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def test_it_returns_key_value(self, key_value, json):
+ key_value.refresh_from_db()
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestDestroy(
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ Returns204,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='i love',
+ value='YOU',
+ ))
+ initial_key_value_ids = precondition_fixture(
+ lambda key_value: # ensure our to-be-deleted KeyValue exists in our set
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def test_it_deletes_key_value(self, initial_key_value_ids, key_value):
+ expected = initial_key_value_ids - {key_value.id}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+```
+It's quite a feat!
+Now, we tested an already-existing endpoint here, just for demonstration purposes. But there's a bigger advantage to performing one request per test, and having a single responsibility for each test: we can write the tests first and incrementally build the ViewSet. We run the tests on changes, and when they're all green, we know the endpoint is done.
+The beauty of the tests-first methodology is that it frees us up to be creative. Because we have a definite end condition, we can experiment with better implementations — more maintainable, easier to read, using best practices, perhaps leaning on a third-party package for heavy lifting.
+Well, congratulations if you've made it this far. I hope you may find some value in this library, or even from some conventions in these example tests. Good luck out there, and remember: readability counts — in tests, doubly so.
+## Bonus: BDD syntax
+Personally, I like to use `DescribeKeyValueViewSet`, and `DescribeList`, `DescribeCreate`, etc for my test classes. If I'm testing `DescribeCreate` as a particular user, I like to use, e.g., `ContextAsAdmin`. Sometimes `CaseUnauthenticated` hits the spot.
+And for test methods, I love to omit the `test` in `test_it_does_xyz`, and simply put `it_does_xyz`.
+To appease my leanings toward BDD namings, I use a `pytest.ini` with these options:
+```ini
+[pytest]
+# Only search for tests within files matching these patterns
+python_files = tests.py test_*.py
+# Discover tests within classes matching these patterns
+# NOTE: the dash represents a word boundary (functionality provided by pytest-camel-collect)
+python_classes = Test-* Describe-* Context-* With-* Without-* For-* When-* If-* Case-*
+# Only methods matching these patterns are considered tests
+python_functions = test_* it_* its_*
+```
+About the dashes in `python_classes`: sometimes I'll name a test class `ForAdminUsers`. If I had the pattern `For*`, it would also match a pytest-drf mixin named `ForbidsAnonymousUsers`. [pytest-camel-collect](https://github.com/theY4Kman/pytest-camel-collect) is a little plugin that interprets dashes in `python_classes` as CamelCase word boundaries. However, similar behavior can be had on stock pytest using a pattern like `For[A-Z0-9]*`.
+Here's what our example `KeyValueViewSet` test would look like with this BDD naming scheme
+<details>
+<summary>BDD-esque KeyValueViewSet test</summary>
+```python
+from typing import Any, Dict
+from pytest_common_subject import precondition_fixture
+from pytest_drf import (
+ ViewSetTest,
+ Returns200,
+ Returns201,
+ Returns204,
+ UsesGetMethod,
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ UsesListEndpoint,
+ UsesPatchMethod,
+ UsesPostMethod,
+)
+from pytest_drf.util import pluralized, url_for
+from pytest_lambda import lambda_fixture, static_fixture
+from pytest_assert_utils import assert_model_attrs
+def express_key_value(kv: KeyValue) -> Dict[str, Any]:
+ return {
+ 'id': kv.id,
+ 'key': kv.key,
+ 'value': kv.value,
+ }
+express_key_values = pluralized(express_key_value)
+class DescribeKeyValueViewSet(ViewSetTest):
+ list_url = lambda_fixture(
+ lambda:
+ url_for('key-values-list'))
+ detail_url = lambda_fixture(
+ lambda key_value:
+ url_for('key-values-detail', key_value.pk))
+ class DescribeList(
+ UsesGetMethod,
+ UsesListEndpoint,
+ Returns200,
+ ):
+ key_values = lambda_fixture(
+ lambda: [
+ KeyValue.objects.create(key=key, value=value)
+ for key, value in {
+ 'quay': 'worth',
+ 'chi': 'revenue',
+ 'umma': 'gumma',
+ }.items()
+ ],
+ autouse=True,
+ )
+ def it_returns_key_values(self, key_values, results):
+ expected = express_key_values(sorted(key_values, key=lambda kv: kv.id))
+ actual = results
+ assert expected == actual
+ class DescribeCreate(
+ UsesPostMethod,
+ UsesListEndpoint,
+ Returns201,
+ ):
+ data = static_fixture({
+ 'key': 'snakes',
+ 'value': 'hissssssss',
+ })
+ initial_key_value_ids = precondition_fixture(
+ lambda:
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def it_creates_new_key_value(self, initial_key_value_ids, json):
+ expected = initial_key_value_ids | {json['id']}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+ def it_sets_expected_attrs(self, data, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def it_returns_key_value(self, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeRetrieve(
+ UsesGetMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='monty',
+ value='jython',
+ ))
+ def it_returns_key_value(self, key_value, json):
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeUpdate(
+ UsesPatchMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='pipenv',
+ value='was a huge leap forward',
+ ))
+ data = static_fixture({
+ 'key': 'buuut poetry',
+ 'value': 'locks quicker and i like that',
+ })
+ def it_sets_expected_attrs(self, data, key_value):
+ # We must tell Django to grab fresh data from the database, or we'll
+ # see our stale initial data and think our endpoint is broken!
+ key_value.refresh_from_db()
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def it_returns_key_value(self, key_value, json):
+ key_value.refresh_from_db()
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeDestroy(
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ Returns204,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='i love',
+ value='YOU',
+ ))
+ initial_key_value_ids = precondition_fixture(
+ lambda key_value: # ensure our to-be-deleted KeyValue exists in our set
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def it_deletes_key_value(self, initial_key_value_ids, key_value):
+ expected = initial_key_value_ids - {key_value.id}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+```
+</details>
+
+%package help
+Summary: Development documents and examples for pytest-drf
+Provides: python3-pytest-drf-doc
+%description help
+```
+Yaaaaay!
+## Putting it all together
+```python
+# tests/test_kv.py
+from typing import Any, Dict
+from pytest_common_subject import precondition_fixture
+from pytest_drf import (
+ ViewSetTest,
+ Returns200,
+ Returns201,
+ Returns204,
+ UsesGetMethod,
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ UsesListEndpoint,
+ UsesPatchMethod,
+ UsesPostMethod,
+)
+from pytest_drf.util import pluralized, url_for
+from pytest_lambda import lambda_fixture, static_fixture
+from pytest_assert_utils import assert_model_attrs
+def express_key_value(kv: KeyValue) -> Dict[str, Any]:
+ return {
+ 'id': kv.id,
+ 'key': kv.key,
+ 'value': kv.value,
+ }
+express_key_values = pluralized(express_key_value)
+class TestKeyValueViewSet(ViewSetTest):
+ list_url = lambda_fixture(
+ lambda:
+ url_for('key-values-list'))
+ detail_url = lambda_fixture(
+ lambda key_value:
+ url_for('key-values-detail', key_value.pk))
+ class TestList(
+ UsesGetMethod,
+ UsesListEndpoint,
+ Returns200,
+ ):
+ key_values = lambda_fixture(
+ lambda: [
+ KeyValue.objects.create(key=key, value=value)
+ for key, value in {
+ 'quay': 'worth',
+ 'chi': 'revenue',
+ 'umma': 'gumma',
+ }.items()
+ ],
+ autouse=True,
+ )
+ def test_it_returns_key_values(self, key_values, results):
+ expected = express_key_values(sorted(key_values, key=lambda kv: kv.id))
+ actual = results
+ assert expected == actual
+ class TestCreate(
+ UsesPostMethod,
+ UsesListEndpoint,
+ Returns201,
+ ):
+ data = static_fixture({
+ 'key': 'snakes',
+ 'value': 'hissssssss',
+ })
+ initial_key_value_ids = precondition_fixture(
+ lambda:
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def test_it_creates_new_key_value(self, initial_key_value_ids, json):
+ expected = initial_key_value_ids | {json['id']}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+ def test_it_sets_expected_attrs(self, data, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def test_it_returns_key_value(self, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestRetrieve(
+ UsesGetMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='monty',
+ value='jython',
+ ))
+ def test_it_returns_key_value(self, key_value, json):
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestUpdate(
+ UsesPatchMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='pipenv',
+ value='was a huge leap forward',
+ ))
+ data = static_fixture({
+ 'key': 'buuut poetry',
+ 'value': 'locks quicker and i like that',
+ })
+ def test_it_sets_expected_attrs(self, data, key_value):
+ # We must tell Django to grab fresh data from the database, or we'll
+ # see our stale initial data and think our endpoint is broken!
+ key_value.refresh_from_db()
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def test_it_returns_key_value(self, key_value, json):
+ key_value.refresh_from_db()
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class TestDestroy(
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ Returns204,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='i love',
+ value='YOU',
+ ))
+ initial_key_value_ids = precondition_fixture(
+ lambda key_value: # ensure our to-be-deleted KeyValue exists in our set
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def test_it_deletes_key_value(self, initial_key_value_ids, key_value):
+ expected = initial_key_value_ids - {key_value.id}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+```
+It's quite a feat!
+Now, we tested an already-existing endpoint here, just for demonstration purposes. But there's a bigger advantage to performing one request per test, and having a single responsibility for each test: we can write the tests first and incrementally build the ViewSet. We run the tests on changes, and when they're all green, we know the endpoint is done.
+The beauty of the tests-first methodology is that it frees us up to be creative. Because we have a definite end condition, we can experiment with better implementations — more maintainable, easier to read, using best practices, perhaps leaning on a third-party package for heavy lifting.
+Well, congratulations if you've made it this far. I hope you may find some value in this library, or even from some conventions in these example tests. Good luck out there, and remember: readability counts — in tests, doubly so.
+## Bonus: BDD syntax
+Personally, I like to use `DescribeKeyValueViewSet`, and `DescribeList`, `DescribeCreate`, etc for my test classes. If I'm testing `DescribeCreate` as a particular user, I like to use, e.g., `ContextAsAdmin`. Sometimes `CaseUnauthenticated` hits the spot.
+And for test methods, I love to omit the `test` in `test_it_does_xyz`, and simply put `it_does_xyz`.
+To appease my leanings toward BDD namings, I use a `pytest.ini` with these options:
+```ini
+[pytest]
+# Only search for tests within files matching these patterns
+python_files = tests.py test_*.py
+# Discover tests within classes matching these patterns
+# NOTE: the dash represents a word boundary (functionality provided by pytest-camel-collect)
+python_classes = Test-* Describe-* Context-* With-* Without-* For-* When-* If-* Case-*
+# Only methods matching these patterns are considered tests
+python_functions = test_* it_* its_*
+```
+About the dashes in `python_classes`: sometimes I'll name a test class `ForAdminUsers`. If I had the pattern `For*`, it would also match a pytest-drf mixin named `ForbidsAnonymousUsers`. [pytest-camel-collect](https://github.com/theY4Kman/pytest-camel-collect) is a little plugin that interprets dashes in `python_classes` as CamelCase word boundaries. However, similar behavior can be had on stock pytest using a pattern like `For[A-Z0-9]*`.
+Here's what our example `KeyValueViewSet` test would look like with this BDD naming scheme
+<details>
+<summary>BDD-esque KeyValueViewSet test</summary>
+```python
+from typing import Any, Dict
+from pytest_common_subject import precondition_fixture
+from pytest_drf import (
+ ViewSetTest,
+ Returns200,
+ Returns201,
+ Returns204,
+ UsesGetMethod,
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ UsesListEndpoint,
+ UsesPatchMethod,
+ UsesPostMethod,
+)
+from pytest_drf.util import pluralized, url_for
+from pytest_lambda import lambda_fixture, static_fixture
+from pytest_assert_utils import assert_model_attrs
+def express_key_value(kv: KeyValue) -> Dict[str, Any]:
+ return {
+ 'id': kv.id,
+ 'key': kv.key,
+ 'value': kv.value,
+ }
+express_key_values = pluralized(express_key_value)
+class DescribeKeyValueViewSet(ViewSetTest):
+ list_url = lambda_fixture(
+ lambda:
+ url_for('key-values-list'))
+ detail_url = lambda_fixture(
+ lambda key_value:
+ url_for('key-values-detail', key_value.pk))
+ class DescribeList(
+ UsesGetMethod,
+ UsesListEndpoint,
+ Returns200,
+ ):
+ key_values = lambda_fixture(
+ lambda: [
+ KeyValue.objects.create(key=key, value=value)
+ for key, value in {
+ 'quay': 'worth',
+ 'chi': 'revenue',
+ 'umma': 'gumma',
+ }.items()
+ ],
+ autouse=True,
+ )
+ def it_returns_key_values(self, key_values, results):
+ expected = express_key_values(sorted(key_values, key=lambda kv: kv.id))
+ actual = results
+ assert expected == actual
+ class DescribeCreate(
+ UsesPostMethod,
+ UsesListEndpoint,
+ Returns201,
+ ):
+ data = static_fixture({
+ 'key': 'snakes',
+ 'value': 'hissssssss',
+ })
+ initial_key_value_ids = precondition_fixture(
+ lambda:
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def it_creates_new_key_value(self, initial_key_value_ids, json):
+ expected = initial_key_value_ids | {json['id']}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+ def it_sets_expected_attrs(self, data, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def it_returns_key_value(self, json):
+ key_value = KeyValue.objects.get(pk=json['id'])
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeRetrieve(
+ UsesGetMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='monty',
+ value='jython',
+ ))
+ def it_returns_key_value(self, key_value, json):
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeUpdate(
+ UsesPatchMethod,
+ UsesDetailEndpoint,
+ Returns200,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='pipenv',
+ value='was a huge leap forward',
+ ))
+ data = static_fixture({
+ 'key': 'buuut poetry',
+ 'value': 'locks quicker and i like that',
+ })
+ def it_sets_expected_attrs(self, data, key_value):
+ # We must tell Django to grab fresh data from the database, or we'll
+ # see our stale initial data and think our endpoint is broken!
+ key_value.refresh_from_db()
+ expected = data
+ assert_model_attrs(key_value, expected)
+ def it_returns_key_value(self, key_value, json):
+ key_value.refresh_from_db()
+ expected = express_key_value(key_value)
+ actual = json
+ assert expected == actual
+ class DescribeDestroy(
+ UsesDeleteMethod,
+ UsesDetailEndpoint,
+ Returns204,
+ ):
+ key_value = lambda_fixture(
+ lambda:
+ KeyValue.objects.create(
+ key='i love',
+ value='YOU',
+ ))
+ initial_key_value_ids = precondition_fixture(
+ lambda key_value: # ensure our to-be-deleted KeyValue exists in our set
+ set(KeyValue.objects.values_list('id', flat=True)))
+ def it_deletes_key_value(self, initial_key_value_ids, key_value):
+ expected = initial_key_value_ids - {key_value.id}
+ actual = set(KeyValue.objects.values_list('id', flat=True))
+ assert expected == actual
+```
+</details>
+
+%prep
+%autosetup -n pytest-drf-1.1.3
+
+%build
+%py3_build
+
+%install
+%py3_install
+install -d -m755 %{buildroot}/%{_pkgdocdir}
+if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi
+if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi
+if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi
+if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi
+pushd %{buildroot}
+if [ -d usr/lib ]; then
+ find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst
+fi
+if [ -d usr/lib64 ]; then
+ find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst
+fi
+if [ -d usr/bin ]; then
+ find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst
+fi
+if [ -d usr/sbin ]; then
+ find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst
+fi
+touch doclist.lst
+if [ -d usr/share/man ]; then
+ find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst
+fi
+popd
+mv %{buildroot}/filelist.lst .
+mv %{buildroot}/doclist.lst .
+
+%files -n python3-pytest-drf -f filelist.lst
+%dir %{python3_sitelib}/*
+
+%files help -f doclist.lst
+%{_docdir}/*
+
+%changelog
+* Fri May 05 2023 Python_Bot <Python_Bot@openeuler.org> - 1.1.3-1
+- Package Spec generated