Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client and testcases classes #169

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ build/
dist/
.cache/
.eggs/
coverage.xml
10 changes: 10 additions & 0 deletions django_hosts/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .client import AsyncHostsClient, HostsClient
from .testcases import SimpleTestCase, TestCase, TransactionTestCase

__all__ = [
'AsyncHostsClient',
'HostsClient',
'SimpleTestCase',
'TestCase',
'TransactionTestCase',
]
26 changes: 26 additions & 0 deletions django_hosts/test/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from urllib.parse import urlparse

from django.test import AsyncClient, Client


class HostClientMixin:
"""Test client to help work with django-hosts in tests."""

def generic(self, method, path, *args, **extra):
scheme, netloc, *_others = urlparse(path)
if scheme:
extra["wsgi.url_scheme"] = scheme
if netloc:
# Populate the host header from the URL host
extra["headers"] = extra["headers"] or {}
if extra["headers"].get("host") is None:
extra["headers"]["host"] = netloc
return super().generic(method, path, *args, **extra)


class HostsClient(HostClientMixin, Client):
pass


class AsyncHostsClient(HostClientMixin, AsyncClient):
pass
24 changes: 24 additions & 0 deletions django_hosts/test/testcases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.test import (
SimpleTestCase as DjangoSimpleTestCase,
TestCase as DjangoTestCase,
TransactionTestCase as DjangoTransactionTestCase
)

from .client import HostsClient, AsyncHostsClient


class HostsTestCaseMixin:
client_class = HostsClient
async_client_class = AsyncHostsClient


class SimpleTestCase(HostsTestCaseMixin, DjangoSimpleTestCase):
pass


class TransactionTestCase(HostsTestCaseMixin, DjangoTransactionTestCase):
pass


class TestCase(HostsTestCaseMixin, DjangoTestCase):
pass
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ More docs
templatetags
reference
callbacks
testing
faq
changelog

Expand Down
74 changes: 74 additions & 0 deletions docs/testing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
Testing
=======

The problem
-----------

Testing with django-hosts can feel cumbersome if using the vanilla Django
testing toolbox. Here is how a typical test would look like:

.. code-block:: python

from django.test import TestCase
from django_hosts import reverse


class UserViewTestCase(TestCase):
def test_user_list(self):
# Assuming the url looks like http://api.example.com/v1/users/
url = reverse("list-user", host="api")

# Pass the server name
response = self.client.get(url, SERVER_NAME="api.example.com")
assert response.status_code == 200

As you can see, you need to remember a few things:

1. Use the ``reverse`` function from django-hosts, as opposed to the one from Django
2. Pass the extra host when reversing
3. Pass the server name (or host header) as part of the request

As your codebase grows, you'll (hopefully) be adding more tests cases and you
may get tired of repeating this.

The solution
------------

Luckily django-hosts provides some testing tool to help you write tests with
less boilerplate, mainly using custom test cases ``SimpleTestCase``,
``TestCase``, ``TransactionTestCase``, all coming from ``django_hosts.test``,
and subclasses of their counterpart from Django.

For example the above test would be written as:

.. code-block:: python

from django_hosts import reverse
from django_hosts.test import TestCase


@override_settings(DEFAULT_HOST="api")
class UserViewTestCase(TestCase):
def test_user_list(self):
url = reverse("list-user")
response = self.client.get(url)
assert response.status_code == 200

Specifically:

- We swap the Django's ``TestCase`` for the django hosts equivalent by changing
the import statement. It's using a custom test client to set the host header
automatically on the request based on the absolute URL. As a result, the
``self.client.get(...)`` call no longer need the server name/host header.

- We set the default host at the class level, using ``override_settings``
(`from Django <https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.override_settings>`_)
instead of repeating it in each ``reverse`` call. When a test class has lots
of methods, this can save a lot of typing and make your tests more readable.

Going further
-------------

This will hopefully cover the main cases, but should you want to reuse it and
combine it with your own client/test case classes, the base functionality is
provided as mixins: ``HostClientMixin`` and ``HostsTestCaseMixin``.
43 changes: 43 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from django.test import TestCase

from django_hosts.test import AsyncHostsClient, HostsClient


class TestHostsClient(TestCase):
client_class = HostsClient

def test_host_header_set_from_full_url(self):
response = self.client.get('https://testserver.local/api/v1/users/')
assert response.request['HTTP_HOST'] == 'testserver.local'

def test_host_header_set_from_url_without_scheme(self):
response = self.client.get('//testserver.local/api/v1/users/')
assert response.request['HTTP_HOST'] == 'testserver.local'

def test_host_header_from_user_is_not_overridden(self):
response = self.client.get(
'https://testserver.local/api/v1/users/', headers={'host': 'api.example.com'}
)
assert response.request['HTTP_HOST'] == 'api.example.com'

def test_host_header_not_set_from_relative_url(self):
response = self.client.get('/api/v1/users/')
assert 'HTTP_HOST' not in response.request


class TestAsyncHostsClient(TestCase):
async_client_class = AsyncHostsClient

async def test_host_header_set_from_url(self):
response = await self.async_client.get('https://testserver.local/api/v1/users/')
assert (b'host', b'testserver.local') in response.request['headers']

async def test_host_header_from_user_is_not_overridden(self):
response = await self.async_client.get(
'https://testserver.local/api/v1/users/', headers={'host': 'api.example.com'}
)
assert (b'host', b'api.example.com') in response.request['headers']

async def test_host_header_not_set_from_relative_url(self):
response = await self.async_client.get('/api/v1/users/')
assert (b'host', b'testserver') in response.request['headers']
19 changes: 19 additions & 0 deletions tests/test_testcases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django_hosts.test import TestCase, HostsClient, AsyncHostsClient, SimpleTestCase, TransactionTestCase


class TestHostsTestCase(TestCase):
def test_client_class_instances(self):
assert isinstance(self.client, HostsClient)
assert isinstance(self.async_client, AsyncHostsClient)


class TestSimpleHostsTestCase(SimpleTestCase):
def test_client_class_instances(self):
assert isinstance(self.client, HostsClient)
assert isinstance(self.async_client, AsyncHostsClient)


class TestTransactionHostsTestCase(TransactionTestCase):
def test_client_class_instances(self):
assert isinstance(self.client, HostsClient)
assert isinstance(self.async_client, AsyncHostsClient)
Loading