%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