From 934e8174abc8bbba244bf7d4c6f28c98581d12ca Mon Sep 17 00:00:00 2001 From: CoprDistGit Date: Fri, 5 May 2023 12:25:40 +0000 Subject: automatic import of python-pytest-drf --- .gitignore | 1 + python-pytest-drf.spec | 979 +++++++++++++++++++++++++++++++++++++++++++++++++ sources | 1 + 3 files changed, 981 insertions(+) create mode 100644 python-pytest-drf.spec create mode 100644 sources diff --git a/.gitignore b/.gitignore index e69de29..c7de260 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +/pytest-drf-1.1.3.tar.gz 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 +
+BDD-esque KeyValueViewSet test +```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 +``` +
+ +%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 +
+BDD-esque KeyValueViewSet test +```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 +``` +
+ +%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 +
+BDD-esque KeyValueViewSet test +```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 +``` +
+ +%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 - 1.1.3-1 +- Package Spec generated diff --git a/sources b/sources new file mode 100644 index 0000000..cf9a9a9 --- /dev/null +++ b/sources @@ -0,0 +1 @@ +355524b0aaaae3a927c670d41451e833 pytest-drf-1.1.3.tar.gz -- cgit v1.2.3