%global _empty_manifest_terminate_build 0 Name: python-django-test-plus Version: 2.2.1 Release: 1 Summary: "django-test-plus provides useful additions to Django's default TestCase" License: BSD License URL: https://github.com/revsys/django-test-plus/ Source0: https://mirrors.nju.edu.cn/pypi/web/packages/fc/66/435ae537414a2dfad119f8162a4e2f7df5f42ec05f8cbdf3f373ea1239c9/django-test-plus-2.2.1.tar.gz BuildArch: noarch Requires: python3-factory-boy Requires: python3-flake8 Requires: python3-pyflakes Requires: python3-pytest Requires: python3-pytest-cov Requires: python3-pytest-django Requires: python3-pytest-pythonpath %description # django-test-plus Useful additions to Django's default TestCase from [REVSYS](https://www.revsys.com/) [![pypi](https://img.shields.io/pypi/v/django-test-plus.svg)](https://pypi.org/project/django-test-plus/) [![build matrix demo](https://github.com/revsys/django-test-plus/actions/workflows/actions.yml/badge.svg)](https://github.com/revsys/django-test-plus/actions/workflows/actions.yml) ## Rationale Let's face it, writing tests isn't always fun. Part of the reason for that is all of the boilerplate you end up writing. django-test-plus is an attempt to cut down on some of that when writing Django tests. We guarantee it will increase the time before you get carpal tunnel by at least 3 weeks! ## Support Supports: Python 3.6, 3.7, 3.8, 3.9, and 3.10. Supports Django Versions: 2.0, 2.1, 2.2, 3.0, 3.1. and 3.2. ## Documentation Full documentation is available at http://django-test-plus.readthedocs.org ## Installation ```shell $ pip install django-test-plus ``` ## Usage To use django-test-plus, have your tests inherit from test_plus.test.TestCase rather than the normal django.test.TestCase:: ```python from test_plus.test import TestCase class MyViewTests(TestCase): ... ``` This is sufficient to get things rolling, but you are encouraged to create *your own* sub-classes for your projects. This will allow you to add your own project-specific helper methods. For example, if you have a django project named 'myproject', you might create the following in `myproject/test.py`: ```python from test_plus.test import TestCase as PlusTestCase class TestCase(PlusTestCase): pass ``` And then in your tests use: ```python from myproject.test import TestCase class MyViewTests(TestCase): ... ``` This import, which is similar to the way you would import Django's TestCase, is also valid: ```python from test_plus import TestCase ``` ## pytest Usage You can get a TestCase like object as a pytest fixture now by asking for `tp`. All of the methods below would then work in pytest functions. For example: ```python def test_url_reverse(tp): expected_url = '/api/' reversed_url = tp.reverse('api') assert expected_url == reversed_url ``` The `tp_api` fixture will provide a `TestCase` that uses django-rest-framework's `APIClient()`: ```python def test_url_reverse(tp_api): response = tp_api.client.post("myapi", format="json") assert response.status_code == 200 ``` ## Methods ### reverse(url_name, *args, **kwargs) When testing views you often find yourself needing to reverse the URL's name. With django-test-plus there is no need for the `from django.core.urlresolvers import reverse` boilerplate. Instead, use: ```python def test_something(self): url = self.reverse('my-url-name') slug_url = self.reverse('name-takes-a-slug', slug='my-slug') pk_url = self.reverse('name-takes-a-pk', pk=12) ``` As you can see our reverse also passes along any args or kwargs you need to pass in. ## get(url_name, follow=True, *args, **kwargs) Another thing you do often is HTTP get urls. Our `get()` method assumes you are passing in a named URL with any args or kwargs necessary to reverse the url_name. If needed, place kwargs for `TestClient.get()` in an 'extra' dictionary.: ```python def test_get_named_url(self): response = self.get('my-url-name') # Get XML data via AJAX request xml_response = self.get( 'my-url-name', extra={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) ``` When using this get method two other things happen for you: we store the last response in `self.last_response` and the response's Context in `self.context`. So instead of: ```python def test_default_django(self): response = self.client.get(reverse('my-url-name')) self.assertTrue('foo' in response.context) self.assertEqual(response.context['foo'], 12) ``` You can write: ```python def test_testplus_get(self): self.get('my-url-name') self.assertInContext('foo') self.assertEqual(self.context['foo'], 12) ``` It's also smart about already reversed URLs, so you can be lazy and do: ```python def test_testplus_get(self): url = self.reverse('my-url-name') self.get(url) self.response_200() ``` If you need to pass query string parameters to your url name, you can do so like this. Assuming the name 'search' maps to '/search/' then: ```python def test_testplus_get_query(self): self.get('search', data={'query': 'testing'}) ``` Would GET `/search/?query=testing`. ## post(url_name, data, follow=True, *args, **kwargs) Our `post()` method takes a named URL, an optional dictionary of data you wish to post and any args or kwargs necessary to reverse the url_name. If needed, place kwargs for `TestClient.post()` in an 'extra' dictionary.: ```python def test_post_named_url(self): response = self.post('my-url-name', data={'coolness-factor': 11.0}, extra={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) ``` *NOTE* Along with the frequently used get and post, we support all of the HTTP verbs such as put, patch, head, trace, options, and delete in the same fashion. ## get_context(key) Often you need to get things out of the template context: ```python def test_context_data(self): self.get('my-view-with-some-context') slug = self.get_context('slug') ``` ## assertInContext(key) You can ensure a specific key exists in the last response's context by using: ```python def test_in_context(self): self.get('my-view-with-some-context') self.assertInContext('some-key') ``` ## assertContext(key, value) We can get context values and ensure they exist, but we can also test equality while we're at it. This asserts that key == value: ```python def test_in_context(self): self.get('my-view-with-some-context') self.assertContext('some-key', 'expected value') ``` ## assert_http_###_(response, msg=None) - status code checking Another test you often need to do is check that a response has a certain HTTP status code. With Django's default TestCase you would write: ```python from django.core.urlresolvers import reverse def test_status(self): response = self.client.get(reverse('my-url-name')) self.assertEqual(response.status_code, 200) ``` With django-test-plus you can shorten that to be: ```python def test_better_status(self): response = self.get('my-url-name') self.assert_http_200_ok(response) ``` Django-test-plus provides a majority of the status codes assertions for you. The status assertions can be found in their own [mixin](https://github.com/revsys/django-test-plus/blob/main/test_plus/status_codes.py) and should be searchable if you're using an IDE like pycharm. It should be noted that in previous versions, django-test-plus had assertion methods in the pattern of `response_###()`, which are still available but have since been deprecated. See below for a list of those methods. Each of the assertion methods takes an optional Django test client `response` and a string `msg` argument that, if specified, is used as the error message when a failure occurs. The methods, `assert_http_301_moved_permanently` and `assert_http_302_found` also take an optional `url` argument that if passed, will check to make sure the `response.url` matches. If it's available, the `assert_http_###_` methods will use the last response. So you can do: ```python def test_status(self): self.get('my-url-name') self.assert_http_200_ok() ``` Which is a bit shorter. The `response_###()` methods that are deprecated, but still available for use, include: - `response_200()` - `response_201()` - `response_204()` - `response_301()` - `response_302()` - `response_400()` - `response_401()` - `response_403()` - `response_404()` - `response_405()` - `response_409()` - `response_410()` All of which take an optional Django test client response and a str msg argument that, if specified, is used as the error message when a failure occurs. Just like the `assert_http_###_()` methods, these methods will use the last response if it's available. ## get_check_200(url_name, *args, **kwargs) GETing and checking views return status 200 is a common test. This method makes it more convenient:: ```python def test_even_better_status(self): response = self.get_check_200('my-url-name') ``` ## make_user(username='testuser', password='password', perms=None) When testing out views you often need to create various users to ensure all of your logic is safe and sound. To make this process easier, this method will create a user for you: ```python def test_user_stuff(self) user1 = self.make_user('u1') user2 = self.make_user('u2') ``` If creating a User in your project is more complicated, say for example you removed the `username` field from the default Django Auth model, you can provide a [Factory Boy](https://factoryboy.readthedocs.org/en/latest/) factory to create it or override this method on your own sub-class. To use a Factory Boy factory, create your class like this:: ```python from test_plus.test import TestCase from .factories import UserFactory class MySpecialTest(TestCase): user_factory = UserFactory def test_special_creation(self): user1 = self.make_user('u1') ``` **NOTE:** Users created by this method will have their password set to the string 'password' by default, in order to ease testing. If you need a specific password, override the `password` parameter. You can also pass in user permissions by passing in a string of '`.`' or '`.*`'. For example: ```python user2 = self.make_user(perms=['myapp.create_widget', 'otherapp.*']) ``` ## print_form_errors(response_or_form=None) When debugging a failing test for a view with a form, this method helps you quickly look at any form errors. Example usage: ```python class MyFormTest(TestCase): self.post('my-url-name', data={}) self.print_form_errors() # or resp = self.post('my-url-name', data={}) self.print_form_errors(resp) # or form = MyForm(data={}) self.print_form_errors(form) ``` ## Authentication Helpers ### assertLoginRequired(url_name, *args, **kwargs) This method helps you test that a given named URL requires authorization: ```python def test_auth(self): self.assertLoginRequired('my-restricted-url') self.assertLoginRequired('my-restricted-object', pk=12) self.assertLoginRequired('my-restricted-object', slug='something') ``` ### login context Along with ensuring a view requires login and creating users, the next thing you end up doing is logging in as various users to test your restriction logic: ```python def test_restrictions(self): user1 = self.make_user('u1') user2 = self.make_user('u2') self.assertLoginRequired('my-protected-view') with self.login(username=user1.username, password='password'): response = self.get('my-protected-view') # Test user1 sees what they should be seeing with self.login(username=user2.username, password='password'): response = self.get('my-protected-view') # Test user2 see what they should be seeing ``` Since we're likely creating our users using `make_user()` from above, the login context assumes the password is 'password' unless specified otherwise. Therefore you you can do: ```python def test_restrictions(self): user1 = self.make_user('u1') with self.login(username=user1.username): response = self.get('my-protected-view') ``` We can also derive the username if we're using `make_user()` so we can shorten that up even further like this: ```python def test_restrictions(self): user1 = self.make_user('u1') with self.login(user1): response = self.get('my-protected-view') ``` ## Ensuring low query counts ### assertNumQueriesLessThan(number) - context Django provides [assertNumQueries](https://docs.djangoproject.com/en/1.8/topics/testing/tools/#django.test.TransactionTestCase.assertNumQueries) which is great when your code generates a specific number of queries. However, if this number varies due to the nature of your data, with this method you can still test to ensure the code doesn't start producing a ton more queries than you expect: ```python def test_something_out(self): with self.assertNumQueriesLessThan(7): self.get('some-view-with-6-queries') ``` ### assertGoodView(url_name, *args, **kwargs) This method does a few things for you. It: - Retrieves the name URL - Ensures the view does not generate more than 50 queries - Ensures the response has status code 200 - Returns the response Often a wide, sweeping test like this is better than no test at all. You can use it like this: ```python def test_better_than_nothing(self): response = self.assertGoodView('my-url-name') ``` ## Testing DRF views To take advantage of the convenience of DRF's test client, you can create a subclass of `TestCase` and set the `client_class` property: ```python from test_plus import TestCase from rest_framework.test import APIClient class APITestCase(TestCase): client_class = APIClient ``` For convenience, `test_plus` ships with `APITestCase`, which does just that: ```python from test_plus import APITestCase class MyAPITestCase(APITestCase): def test_post(self): data = {'testing': {'prop': 'value'}} self.post('view-json', data=data, extra={'format': 'json'}) self.assert_http_200_ok() ``` Note that using `APITestCase` requires Django >= 1.8 and having installed `django-rest-framework`. ## Testing class-based "generic" views The TestCase methods `get()` and `post()` work for both function-based and class-based views. However, in doing so they invoke Django's URL resolution, middleware, template processing, and decorator systems. For integration testing this is desirable, as you want to ensure your URLs resolve properly, view permissions are enforced, etc. For unit testing this is costly because all these Django request/response systems are invoked in addition to your method, and they typically do not affect the end result. Class-based views (derived from Django's `generic.models.View` class) contain methods and mixins which makes granular unit testing (more) feasible. Quite often your usage of a generic view class comprises an override of an existing method. Invoking the entire view and the Django request/response stack is a waste of time when you really want to call the overridden method directly and test the result. CBVTestCase to the rescue! As with TestCase above, have your tests inherit from test_plus.test.CBVTestCase rather than TestCase like so: ```python from test_plus.test import CBVTestCase class MyViewTests(CBVTestCase): ``` ## Methods ### get_instance(cls, initkwargs=None, request=None, *args, **kwargs) This core method simplifies the instantiation of your class, giving you a way to invoke class methods directly. Returns an instance of `cls`, initialized with `initkwargs`. Sets `request`, `args`, and `kwargs` attributes on the class instance. `args` and `kwargs` are the same values you would pass to `reverse()`. Sample usage: ```python from django.views import generic from test_plus.test import CBVTestCase class MyClass(generic.DetailView) def get_context_data(self, **kwargs): kwargs['answer'] = 42 return kwargs class MyTests(CBVTestCase): def test_context_data(self): my_view = self.get_instance(MyClass, {'object': some_object}) context = my_view.get_context_data() self.assertEqual(context['answer'], 42) ``` ### get(cls, initkwargs=None, *args, **kwargs) Invokes `cls.get()` and returns the response, rendering template if possible. Builds on the `CBVTestCase.get_instance()` foundation. All test_plus.test.TestCase methods are valid, so the following works: ```python response = self.get(MyClass) self.assertContext('my_key', expected_value) ``` All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.get()`. **NOTE:** This method bypasses Django's middleware, and therefore context variables created by middleware are not available. If this affects your template/context testing, you should use TestCase instead of CBVTestCase. ### post(cls, data=None, initkwargs=None, *args, **kwargs) Invokes `cls.post()` and returns the response, rendering template if possible. Builds on the `CBVTestCase.get_instance()` foundation. Example: ```python response = self.post(MyClass, data={'search_term': 'revsys'}) self.response_200(response) self.assertContext('company_name', 'RevSys') ``` All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. **NOTE:** This method bypasses Django's middleware, and therefore context variables created by middleware are not available. If this affects your template/context testing you should use TestCase instead of CBVTestCase. ### get_check_200(cls, initkwargs=None, *args, **kwargs) Works just like `TestCase.get_check_200()`. Caller must provide a view class instead of a URL name or path parameter. All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. ### assertGoodView(cls, initkwargs=None, *args, **kwargs) Works just like `TestCase.assertGoodView()`. Caller must provide a view class instead of a URL name or path parameter. All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. ## Development To work on django-test-plus itself, clone this repository and run the following commands: ```shell $ pip install -r requirements.txt $ pip install -e . ``` **NOTE**: You will also need to ensure that the `test_project` directory, located at the root of this repo, is in your virtualenv's path. ## Keep in touch! If you have a question about this project, please open a GitHub issue. If you love us and want to keep track of our goings-on, here's where you can find us online: %package -n python3-django-test-plus Summary: "django-test-plus provides useful additions to Django's default TestCase" Provides: python-django-test-plus BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-pip %description -n python3-django-test-plus # django-test-plus Useful additions to Django's default TestCase from [REVSYS](https://www.revsys.com/) [![pypi](https://img.shields.io/pypi/v/django-test-plus.svg)](https://pypi.org/project/django-test-plus/) [![build matrix demo](https://github.com/revsys/django-test-plus/actions/workflows/actions.yml/badge.svg)](https://github.com/revsys/django-test-plus/actions/workflows/actions.yml) ## Rationale Let's face it, writing tests isn't always fun. Part of the reason for that is all of the boilerplate you end up writing. django-test-plus is an attempt to cut down on some of that when writing Django tests. We guarantee it will increase the time before you get carpal tunnel by at least 3 weeks! ## Support Supports: Python 3.6, 3.7, 3.8, 3.9, and 3.10. Supports Django Versions: 2.0, 2.1, 2.2, 3.0, 3.1. and 3.2. ## Documentation Full documentation is available at http://django-test-plus.readthedocs.org ## Installation ```shell $ pip install django-test-plus ``` ## Usage To use django-test-plus, have your tests inherit from test_plus.test.TestCase rather than the normal django.test.TestCase:: ```python from test_plus.test import TestCase class MyViewTests(TestCase): ... ``` This is sufficient to get things rolling, but you are encouraged to create *your own* sub-classes for your projects. This will allow you to add your own project-specific helper methods. For example, if you have a django project named 'myproject', you might create the following in `myproject/test.py`: ```python from test_plus.test import TestCase as PlusTestCase class TestCase(PlusTestCase): pass ``` And then in your tests use: ```python from myproject.test import TestCase class MyViewTests(TestCase): ... ``` This import, which is similar to the way you would import Django's TestCase, is also valid: ```python from test_plus import TestCase ``` ## pytest Usage You can get a TestCase like object as a pytest fixture now by asking for `tp`. All of the methods below would then work in pytest functions. For example: ```python def test_url_reverse(tp): expected_url = '/api/' reversed_url = tp.reverse('api') assert expected_url == reversed_url ``` The `tp_api` fixture will provide a `TestCase` that uses django-rest-framework's `APIClient()`: ```python def test_url_reverse(tp_api): response = tp_api.client.post("myapi", format="json") assert response.status_code == 200 ``` ## Methods ### reverse(url_name, *args, **kwargs) When testing views you often find yourself needing to reverse the URL's name. With django-test-plus there is no need for the `from django.core.urlresolvers import reverse` boilerplate. Instead, use: ```python def test_something(self): url = self.reverse('my-url-name') slug_url = self.reverse('name-takes-a-slug', slug='my-slug') pk_url = self.reverse('name-takes-a-pk', pk=12) ``` As you can see our reverse also passes along any args or kwargs you need to pass in. ## get(url_name, follow=True, *args, **kwargs) Another thing you do often is HTTP get urls. Our `get()` method assumes you are passing in a named URL with any args or kwargs necessary to reverse the url_name. If needed, place kwargs for `TestClient.get()` in an 'extra' dictionary.: ```python def test_get_named_url(self): response = self.get('my-url-name') # Get XML data via AJAX request xml_response = self.get( 'my-url-name', extra={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) ``` When using this get method two other things happen for you: we store the last response in `self.last_response` and the response's Context in `self.context`. So instead of: ```python def test_default_django(self): response = self.client.get(reverse('my-url-name')) self.assertTrue('foo' in response.context) self.assertEqual(response.context['foo'], 12) ``` You can write: ```python def test_testplus_get(self): self.get('my-url-name') self.assertInContext('foo') self.assertEqual(self.context['foo'], 12) ``` It's also smart about already reversed URLs, so you can be lazy and do: ```python def test_testplus_get(self): url = self.reverse('my-url-name') self.get(url) self.response_200() ``` If you need to pass query string parameters to your url name, you can do so like this. Assuming the name 'search' maps to '/search/' then: ```python def test_testplus_get_query(self): self.get('search', data={'query': 'testing'}) ``` Would GET `/search/?query=testing`. ## post(url_name, data, follow=True, *args, **kwargs) Our `post()` method takes a named URL, an optional dictionary of data you wish to post and any args or kwargs necessary to reverse the url_name. If needed, place kwargs for `TestClient.post()` in an 'extra' dictionary.: ```python def test_post_named_url(self): response = self.post('my-url-name', data={'coolness-factor': 11.0}, extra={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) ``` *NOTE* Along with the frequently used get and post, we support all of the HTTP verbs such as put, patch, head, trace, options, and delete in the same fashion. ## get_context(key) Often you need to get things out of the template context: ```python def test_context_data(self): self.get('my-view-with-some-context') slug = self.get_context('slug') ``` ## assertInContext(key) You can ensure a specific key exists in the last response's context by using: ```python def test_in_context(self): self.get('my-view-with-some-context') self.assertInContext('some-key') ``` ## assertContext(key, value) We can get context values and ensure they exist, but we can also test equality while we're at it. This asserts that key == value: ```python def test_in_context(self): self.get('my-view-with-some-context') self.assertContext('some-key', 'expected value') ``` ## assert_http_###_(response, msg=None) - status code checking Another test you often need to do is check that a response has a certain HTTP status code. With Django's default TestCase you would write: ```python from django.core.urlresolvers import reverse def test_status(self): response = self.client.get(reverse('my-url-name')) self.assertEqual(response.status_code, 200) ``` With django-test-plus you can shorten that to be: ```python def test_better_status(self): response = self.get('my-url-name') self.assert_http_200_ok(response) ``` Django-test-plus provides a majority of the status codes assertions for you. The status assertions can be found in their own [mixin](https://github.com/revsys/django-test-plus/blob/main/test_plus/status_codes.py) and should be searchable if you're using an IDE like pycharm. It should be noted that in previous versions, django-test-plus had assertion methods in the pattern of `response_###()`, which are still available but have since been deprecated. See below for a list of those methods. Each of the assertion methods takes an optional Django test client `response` and a string `msg` argument that, if specified, is used as the error message when a failure occurs. The methods, `assert_http_301_moved_permanently` and `assert_http_302_found` also take an optional `url` argument that if passed, will check to make sure the `response.url` matches. If it's available, the `assert_http_###_` methods will use the last response. So you can do: ```python def test_status(self): self.get('my-url-name') self.assert_http_200_ok() ``` Which is a bit shorter. The `response_###()` methods that are deprecated, but still available for use, include: - `response_200()` - `response_201()` - `response_204()` - `response_301()` - `response_302()` - `response_400()` - `response_401()` - `response_403()` - `response_404()` - `response_405()` - `response_409()` - `response_410()` All of which take an optional Django test client response and a str msg argument that, if specified, is used as the error message when a failure occurs. Just like the `assert_http_###_()` methods, these methods will use the last response if it's available. ## get_check_200(url_name, *args, **kwargs) GETing and checking views return status 200 is a common test. This method makes it more convenient:: ```python def test_even_better_status(self): response = self.get_check_200('my-url-name') ``` ## make_user(username='testuser', password='password', perms=None) When testing out views you often need to create various users to ensure all of your logic is safe and sound. To make this process easier, this method will create a user for you: ```python def test_user_stuff(self) user1 = self.make_user('u1') user2 = self.make_user('u2') ``` If creating a User in your project is more complicated, say for example you removed the `username` field from the default Django Auth model, you can provide a [Factory Boy](https://factoryboy.readthedocs.org/en/latest/) factory to create it or override this method on your own sub-class. To use a Factory Boy factory, create your class like this:: ```python from test_plus.test import TestCase from .factories import UserFactory class MySpecialTest(TestCase): user_factory = UserFactory def test_special_creation(self): user1 = self.make_user('u1') ``` **NOTE:** Users created by this method will have their password set to the string 'password' by default, in order to ease testing. If you need a specific password, override the `password` parameter. You can also pass in user permissions by passing in a string of '`.`' or '`.*`'. For example: ```python user2 = self.make_user(perms=['myapp.create_widget', 'otherapp.*']) ``` ## print_form_errors(response_or_form=None) When debugging a failing test for a view with a form, this method helps you quickly look at any form errors. Example usage: ```python class MyFormTest(TestCase): self.post('my-url-name', data={}) self.print_form_errors() # or resp = self.post('my-url-name', data={}) self.print_form_errors(resp) # or form = MyForm(data={}) self.print_form_errors(form) ``` ## Authentication Helpers ### assertLoginRequired(url_name, *args, **kwargs) This method helps you test that a given named URL requires authorization: ```python def test_auth(self): self.assertLoginRequired('my-restricted-url') self.assertLoginRequired('my-restricted-object', pk=12) self.assertLoginRequired('my-restricted-object', slug='something') ``` ### login context Along with ensuring a view requires login and creating users, the next thing you end up doing is logging in as various users to test your restriction logic: ```python def test_restrictions(self): user1 = self.make_user('u1') user2 = self.make_user('u2') self.assertLoginRequired('my-protected-view') with self.login(username=user1.username, password='password'): response = self.get('my-protected-view') # Test user1 sees what they should be seeing with self.login(username=user2.username, password='password'): response = self.get('my-protected-view') # Test user2 see what they should be seeing ``` Since we're likely creating our users using `make_user()` from above, the login context assumes the password is 'password' unless specified otherwise. Therefore you you can do: ```python def test_restrictions(self): user1 = self.make_user('u1') with self.login(username=user1.username): response = self.get('my-protected-view') ``` We can also derive the username if we're using `make_user()` so we can shorten that up even further like this: ```python def test_restrictions(self): user1 = self.make_user('u1') with self.login(user1): response = self.get('my-protected-view') ``` ## Ensuring low query counts ### assertNumQueriesLessThan(number) - context Django provides [assertNumQueries](https://docs.djangoproject.com/en/1.8/topics/testing/tools/#django.test.TransactionTestCase.assertNumQueries) which is great when your code generates a specific number of queries. However, if this number varies due to the nature of your data, with this method you can still test to ensure the code doesn't start producing a ton more queries than you expect: ```python def test_something_out(self): with self.assertNumQueriesLessThan(7): self.get('some-view-with-6-queries') ``` ### assertGoodView(url_name, *args, **kwargs) This method does a few things for you. It: - Retrieves the name URL - Ensures the view does not generate more than 50 queries - Ensures the response has status code 200 - Returns the response Often a wide, sweeping test like this is better than no test at all. You can use it like this: ```python def test_better_than_nothing(self): response = self.assertGoodView('my-url-name') ``` ## Testing DRF views To take advantage of the convenience of DRF's test client, you can create a subclass of `TestCase` and set the `client_class` property: ```python from test_plus import TestCase from rest_framework.test import APIClient class APITestCase(TestCase): client_class = APIClient ``` For convenience, `test_plus` ships with `APITestCase`, which does just that: ```python from test_plus import APITestCase class MyAPITestCase(APITestCase): def test_post(self): data = {'testing': {'prop': 'value'}} self.post('view-json', data=data, extra={'format': 'json'}) self.assert_http_200_ok() ``` Note that using `APITestCase` requires Django >= 1.8 and having installed `django-rest-framework`. ## Testing class-based "generic" views The TestCase methods `get()` and `post()` work for both function-based and class-based views. However, in doing so they invoke Django's URL resolution, middleware, template processing, and decorator systems. For integration testing this is desirable, as you want to ensure your URLs resolve properly, view permissions are enforced, etc. For unit testing this is costly because all these Django request/response systems are invoked in addition to your method, and they typically do not affect the end result. Class-based views (derived from Django's `generic.models.View` class) contain methods and mixins which makes granular unit testing (more) feasible. Quite often your usage of a generic view class comprises an override of an existing method. Invoking the entire view and the Django request/response stack is a waste of time when you really want to call the overridden method directly and test the result. CBVTestCase to the rescue! As with TestCase above, have your tests inherit from test_plus.test.CBVTestCase rather than TestCase like so: ```python from test_plus.test import CBVTestCase class MyViewTests(CBVTestCase): ``` ## Methods ### get_instance(cls, initkwargs=None, request=None, *args, **kwargs) This core method simplifies the instantiation of your class, giving you a way to invoke class methods directly. Returns an instance of `cls`, initialized with `initkwargs`. Sets `request`, `args`, and `kwargs` attributes on the class instance. `args` and `kwargs` are the same values you would pass to `reverse()`. Sample usage: ```python from django.views import generic from test_plus.test import CBVTestCase class MyClass(generic.DetailView) def get_context_data(self, **kwargs): kwargs['answer'] = 42 return kwargs class MyTests(CBVTestCase): def test_context_data(self): my_view = self.get_instance(MyClass, {'object': some_object}) context = my_view.get_context_data() self.assertEqual(context['answer'], 42) ``` ### get(cls, initkwargs=None, *args, **kwargs) Invokes `cls.get()` and returns the response, rendering template if possible. Builds on the `CBVTestCase.get_instance()` foundation. All test_plus.test.TestCase methods are valid, so the following works: ```python response = self.get(MyClass) self.assertContext('my_key', expected_value) ``` All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.get()`. **NOTE:** This method bypasses Django's middleware, and therefore context variables created by middleware are not available. If this affects your template/context testing, you should use TestCase instead of CBVTestCase. ### post(cls, data=None, initkwargs=None, *args, **kwargs) Invokes `cls.post()` and returns the response, rendering template if possible. Builds on the `CBVTestCase.get_instance()` foundation. Example: ```python response = self.post(MyClass, data={'search_term': 'revsys'}) self.response_200(response) self.assertContext('company_name', 'RevSys') ``` All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. **NOTE:** This method bypasses Django's middleware, and therefore context variables created by middleware are not available. If this affects your template/context testing you should use TestCase instead of CBVTestCase. ### get_check_200(cls, initkwargs=None, *args, **kwargs) Works just like `TestCase.get_check_200()`. Caller must provide a view class instead of a URL name or path parameter. All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. ### assertGoodView(cls, initkwargs=None, *args, **kwargs) Works just like `TestCase.assertGoodView()`. Caller must provide a view class instead of a URL name or path parameter. All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. ## Development To work on django-test-plus itself, clone this repository and run the following commands: ```shell $ pip install -r requirements.txt $ pip install -e . ``` **NOTE**: You will also need to ensure that the `test_project` directory, located at the root of this repo, is in your virtualenv's path. ## Keep in touch! If you have a question about this project, please open a GitHub issue. If you love us and want to keep track of our goings-on, here's where you can find us online: %package help Summary: Development documents and examples for django-test-plus Provides: python3-django-test-plus-doc %description help # django-test-plus Useful additions to Django's default TestCase from [REVSYS](https://www.revsys.com/) [![pypi](https://img.shields.io/pypi/v/django-test-plus.svg)](https://pypi.org/project/django-test-plus/) [![build matrix demo](https://github.com/revsys/django-test-plus/actions/workflows/actions.yml/badge.svg)](https://github.com/revsys/django-test-plus/actions/workflows/actions.yml) ## Rationale Let's face it, writing tests isn't always fun. Part of the reason for that is all of the boilerplate you end up writing. django-test-plus is an attempt to cut down on some of that when writing Django tests. We guarantee it will increase the time before you get carpal tunnel by at least 3 weeks! ## Support Supports: Python 3.6, 3.7, 3.8, 3.9, and 3.10. Supports Django Versions: 2.0, 2.1, 2.2, 3.0, 3.1. and 3.2. ## Documentation Full documentation is available at http://django-test-plus.readthedocs.org ## Installation ```shell $ pip install django-test-plus ``` ## Usage To use django-test-plus, have your tests inherit from test_plus.test.TestCase rather than the normal django.test.TestCase:: ```python from test_plus.test import TestCase class MyViewTests(TestCase): ... ``` This is sufficient to get things rolling, but you are encouraged to create *your own* sub-classes for your projects. This will allow you to add your own project-specific helper methods. For example, if you have a django project named 'myproject', you might create the following in `myproject/test.py`: ```python from test_plus.test import TestCase as PlusTestCase class TestCase(PlusTestCase): pass ``` And then in your tests use: ```python from myproject.test import TestCase class MyViewTests(TestCase): ... ``` This import, which is similar to the way you would import Django's TestCase, is also valid: ```python from test_plus import TestCase ``` ## pytest Usage You can get a TestCase like object as a pytest fixture now by asking for `tp`. All of the methods below would then work in pytest functions. For example: ```python def test_url_reverse(tp): expected_url = '/api/' reversed_url = tp.reverse('api') assert expected_url == reversed_url ``` The `tp_api` fixture will provide a `TestCase` that uses django-rest-framework's `APIClient()`: ```python def test_url_reverse(tp_api): response = tp_api.client.post("myapi", format="json") assert response.status_code == 200 ``` ## Methods ### reverse(url_name, *args, **kwargs) When testing views you often find yourself needing to reverse the URL's name. With django-test-plus there is no need for the `from django.core.urlresolvers import reverse` boilerplate. Instead, use: ```python def test_something(self): url = self.reverse('my-url-name') slug_url = self.reverse('name-takes-a-slug', slug='my-slug') pk_url = self.reverse('name-takes-a-pk', pk=12) ``` As you can see our reverse also passes along any args or kwargs you need to pass in. ## get(url_name, follow=True, *args, **kwargs) Another thing you do often is HTTP get urls. Our `get()` method assumes you are passing in a named URL with any args or kwargs necessary to reverse the url_name. If needed, place kwargs for `TestClient.get()` in an 'extra' dictionary.: ```python def test_get_named_url(self): response = self.get('my-url-name') # Get XML data via AJAX request xml_response = self.get( 'my-url-name', extra={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) ``` When using this get method two other things happen for you: we store the last response in `self.last_response` and the response's Context in `self.context`. So instead of: ```python def test_default_django(self): response = self.client.get(reverse('my-url-name')) self.assertTrue('foo' in response.context) self.assertEqual(response.context['foo'], 12) ``` You can write: ```python def test_testplus_get(self): self.get('my-url-name') self.assertInContext('foo') self.assertEqual(self.context['foo'], 12) ``` It's also smart about already reversed URLs, so you can be lazy and do: ```python def test_testplus_get(self): url = self.reverse('my-url-name') self.get(url) self.response_200() ``` If you need to pass query string parameters to your url name, you can do so like this. Assuming the name 'search' maps to '/search/' then: ```python def test_testplus_get_query(self): self.get('search', data={'query': 'testing'}) ``` Would GET `/search/?query=testing`. ## post(url_name, data, follow=True, *args, **kwargs) Our `post()` method takes a named URL, an optional dictionary of data you wish to post and any args or kwargs necessary to reverse the url_name. If needed, place kwargs for `TestClient.post()` in an 'extra' dictionary.: ```python def test_post_named_url(self): response = self.post('my-url-name', data={'coolness-factor': 11.0}, extra={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) ``` *NOTE* Along with the frequently used get and post, we support all of the HTTP verbs such as put, patch, head, trace, options, and delete in the same fashion. ## get_context(key) Often you need to get things out of the template context: ```python def test_context_data(self): self.get('my-view-with-some-context') slug = self.get_context('slug') ``` ## assertInContext(key) You can ensure a specific key exists in the last response's context by using: ```python def test_in_context(self): self.get('my-view-with-some-context') self.assertInContext('some-key') ``` ## assertContext(key, value) We can get context values and ensure they exist, but we can also test equality while we're at it. This asserts that key == value: ```python def test_in_context(self): self.get('my-view-with-some-context') self.assertContext('some-key', 'expected value') ``` ## assert_http_###_(response, msg=None) - status code checking Another test you often need to do is check that a response has a certain HTTP status code. With Django's default TestCase you would write: ```python from django.core.urlresolvers import reverse def test_status(self): response = self.client.get(reverse('my-url-name')) self.assertEqual(response.status_code, 200) ``` With django-test-plus you can shorten that to be: ```python def test_better_status(self): response = self.get('my-url-name') self.assert_http_200_ok(response) ``` Django-test-plus provides a majority of the status codes assertions for you. The status assertions can be found in their own [mixin](https://github.com/revsys/django-test-plus/blob/main/test_plus/status_codes.py) and should be searchable if you're using an IDE like pycharm. It should be noted that in previous versions, django-test-plus had assertion methods in the pattern of `response_###()`, which are still available but have since been deprecated. See below for a list of those methods. Each of the assertion methods takes an optional Django test client `response` and a string `msg` argument that, if specified, is used as the error message when a failure occurs. The methods, `assert_http_301_moved_permanently` and `assert_http_302_found` also take an optional `url` argument that if passed, will check to make sure the `response.url` matches. If it's available, the `assert_http_###_` methods will use the last response. So you can do: ```python def test_status(self): self.get('my-url-name') self.assert_http_200_ok() ``` Which is a bit shorter. The `response_###()` methods that are deprecated, but still available for use, include: - `response_200()` - `response_201()` - `response_204()` - `response_301()` - `response_302()` - `response_400()` - `response_401()` - `response_403()` - `response_404()` - `response_405()` - `response_409()` - `response_410()` All of which take an optional Django test client response and a str msg argument that, if specified, is used as the error message when a failure occurs. Just like the `assert_http_###_()` methods, these methods will use the last response if it's available. ## get_check_200(url_name, *args, **kwargs) GETing and checking views return status 200 is a common test. This method makes it more convenient:: ```python def test_even_better_status(self): response = self.get_check_200('my-url-name') ``` ## make_user(username='testuser', password='password', perms=None) When testing out views you often need to create various users to ensure all of your logic is safe and sound. To make this process easier, this method will create a user for you: ```python def test_user_stuff(self) user1 = self.make_user('u1') user2 = self.make_user('u2') ``` If creating a User in your project is more complicated, say for example you removed the `username` field from the default Django Auth model, you can provide a [Factory Boy](https://factoryboy.readthedocs.org/en/latest/) factory to create it or override this method on your own sub-class. To use a Factory Boy factory, create your class like this:: ```python from test_plus.test import TestCase from .factories import UserFactory class MySpecialTest(TestCase): user_factory = UserFactory def test_special_creation(self): user1 = self.make_user('u1') ``` **NOTE:** Users created by this method will have their password set to the string 'password' by default, in order to ease testing. If you need a specific password, override the `password` parameter. You can also pass in user permissions by passing in a string of '`.`' or '`.*`'. For example: ```python user2 = self.make_user(perms=['myapp.create_widget', 'otherapp.*']) ``` ## print_form_errors(response_or_form=None) When debugging a failing test for a view with a form, this method helps you quickly look at any form errors. Example usage: ```python class MyFormTest(TestCase): self.post('my-url-name', data={}) self.print_form_errors() # or resp = self.post('my-url-name', data={}) self.print_form_errors(resp) # or form = MyForm(data={}) self.print_form_errors(form) ``` ## Authentication Helpers ### assertLoginRequired(url_name, *args, **kwargs) This method helps you test that a given named URL requires authorization: ```python def test_auth(self): self.assertLoginRequired('my-restricted-url') self.assertLoginRequired('my-restricted-object', pk=12) self.assertLoginRequired('my-restricted-object', slug='something') ``` ### login context Along with ensuring a view requires login and creating users, the next thing you end up doing is logging in as various users to test your restriction logic: ```python def test_restrictions(self): user1 = self.make_user('u1') user2 = self.make_user('u2') self.assertLoginRequired('my-protected-view') with self.login(username=user1.username, password='password'): response = self.get('my-protected-view') # Test user1 sees what they should be seeing with self.login(username=user2.username, password='password'): response = self.get('my-protected-view') # Test user2 see what they should be seeing ``` Since we're likely creating our users using `make_user()` from above, the login context assumes the password is 'password' unless specified otherwise. Therefore you you can do: ```python def test_restrictions(self): user1 = self.make_user('u1') with self.login(username=user1.username): response = self.get('my-protected-view') ``` We can also derive the username if we're using `make_user()` so we can shorten that up even further like this: ```python def test_restrictions(self): user1 = self.make_user('u1') with self.login(user1): response = self.get('my-protected-view') ``` ## Ensuring low query counts ### assertNumQueriesLessThan(number) - context Django provides [assertNumQueries](https://docs.djangoproject.com/en/1.8/topics/testing/tools/#django.test.TransactionTestCase.assertNumQueries) which is great when your code generates a specific number of queries. However, if this number varies due to the nature of your data, with this method you can still test to ensure the code doesn't start producing a ton more queries than you expect: ```python def test_something_out(self): with self.assertNumQueriesLessThan(7): self.get('some-view-with-6-queries') ``` ### assertGoodView(url_name, *args, **kwargs) This method does a few things for you. It: - Retrieves the name URL - Ensures the view does not generate more than 50 queries - Ensures the response has status code 200 - Returns the response Often a wide, sweeping test like this is better than no test at all. You can use it like this: ```python def test_better_than_nothing(self): response = self.assertGoodView('my-url-name') ``` ## Testing DRF views To take advantage of the convenience of DRF's test client, you can create a subclass of `TestCase` and set the `client_class` property: ```python from test_plus import TestCase from rest_framework.test import APIClient class APITestCase(TestCase): client_class = APIClient ``` For convenience, `test_plus` ships with `APITestCase`, which does just that: ```python from test_plus import APITestCase class MyAPITestCase(APITestCase): def test_post(self): data = {'testing': {'prop': 'value'}} self.post('view-json', data=data, extra={'format': 'json'}) self.assert_http_200_ok() ``` Note that using `APITestCase` requires Django >= 1.8 and having installed `django-rest-framework`. ## Testing class-based "generic" views The TestCase methods `get()` and `post()` work for both function-based and class-based views. However, in doing so they invoke Django's URL resolution, middleware, template processing, and decorator systems. For integration testing this is desirable, as you want to ensure your URLs resolve properly, view permissions are enforced, etc. For unit testing this is costly because all these Django request/response systems are invoked in addition to your method, and they typically do not affect the end result. Class-based views (derived from Django's `generic.models.View` class) contain methods and mixins which makes granular unit testing (more) feasible. Quite often your usage of a generic view class comprises an override of an existing method. Invoking the entire view and the Django request/response stack is a waste of time when you really want to call the overridden method directly and test the result. CBVTestCase to the rescue! As with TestCase above, have your tests inherit from test_plus.test.CBVTestCase rather than TestCase like so: ```python from test_plus.test import CBVTestCase class MyViewTests(CBVTestCase): ``` ## Methods ### get_instance(cls, initkwargs=None, request=None, *args, **kwargs) This core method simplifies the instantiation of your class, giving you a way to invoke class methods directly. Returns an instance of `cls`, initialized with `initkwargs`. Sets `request`, `args`, and `kwargs` attributes on the class instance. `args` and `kwargs` are the same values you would pass to `reverse()`. Sample usage: ```python from django.views import generic from test_plus.test import CBVTestCase class MyClass(generic.DetailView) def get_context_data(self, **kwargs): kwargs['answer'] = 42 return kwargs class MyTests(CBVTestCase): def test_context_data(self): my_view = self.get_instance(MyClass, {'object': some_object}) context = my_view.get_context_data() self.assertEqual(context['answer'], 42) ``` ### get(cls, initkwargs=None, *args, **kwargs) Invokes `cls.get()` and returns the response, rendering template if possible. Builds on the `CBVTestCase.get_instance()` foundation. All test_plus.test.TestCase methods are valid, so the following works: ```python response = self.get(MyClass) self.assertContext('my_key', expected_value) ``` All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.get()`. **NOTE:** This method bypasses Django's middleware, and therefore context variables created by middleware are not available. If this affects your template/context testing, you should use TestCase instead of CBVTestCase. ### post(cls, data=None, initkwargs=None, *args, **kwargs) Invokes `cls.post()` and returns the response, rendering template if possible. Builds on the `CBVTestCase.get_instance()` foundation. Example: ```python response = self.post(MyClass, data={'search_term': 'revsys'}) self.response_200(response) self.assertContext('company_name', 'RevSys') ``` All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. **NOTE:** This method bypasses Django's middleware, and therefore context variables created by middleware are not available. If this affects your template/context testing you should use TestCase instead of CBVTestCase. ### get_check_200(cls, initkwargs=None, *args, **kwargs) Works just like `TestCase.get_check_200()`. Caller must provide a view class instead of a URL name or path parameter. All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. ### assertGoodView(cls, initkwargs=None, *args, **kwargs) Works just like `TestCase.assertGoodView()`. Caller must provide a view class instead of a URL name or path parameter. All test_plus TestCase side-effects are honored and all test_plus TestCase assertion methods work with `CBVTestCase.post()`. ## Development To work on django-test-plus itself, clone this repository and run the following commands: ```shell $ pip install -r requirements.txt $ pip install -e . ``` **NOTE**: You will also need to ensure that the `test_project` directory, located at the root of this repo, is in your virtualenv's path. ## Keep in touch! If you have a question about this project, please open a GitHub issue. If you love us and want to keep track of our goings-on, here's where you can find us online: %prep %autosetup -n django-test-plus-2.2.1 %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-django-test-plus -f filelist.lst %dir %{python3_sitelib}/* %files help -f doclist.lst %{_docdir}/* %changelog * Sun Apr 23 2023 Python_Bot - 2.2.1-1 - Package Spec generated