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
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 HostsTestCase, SimpleHostsTestCase, TransactionHostsTestCase

__all__ = [
'AsyncHostsClient',
'HostsClient',
'HostsTestCase',
'SimpleHostsTestCase',
'TransactionHostsTestCase',
]
25 changes: 25 additions & 0 deletions django_hosts/test/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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):
if path.startswith('http'):
browniebroke marked this conversation as resolved.
Show resolved Hide resolved
# Populate the host header from the URL host
_scheme, host, *_others = urlparse(path)
if extra.get('headers') is None:
extra['headers'] = {}
if extra['headers'].get('host') is None:
extra['headers']['host'] = host
return super().generic(method, path, *args, **extra)


class HostsClient(HostClientMixin, Client):
pass


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

from .client import HostsClient, AsyncHostsClient


class HostsTestCaseMixin:
client_class = HostsClient
async_client_class = AsyncHostsClient


class SimpleHostsTestCase(HostsTestCaseMixin, SimpleTestCase):
pass


class TransactionHostsTestCase(HostsTestCaseMixin, TransactionTestCase):
pass


class HostsTestCase(HostsTestCaseMixin, TestCase):
browniebroke marked this conversation as resolved.
Show resolved Hide resolved
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 ``SimpleHostsTestCase``,
``HostsTestCase``, ``TransactionHostsTestCase``, 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 HostsTestCase


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

Specifically:

- We swap the Django ``TestCase`` for the django hosts equivalent. 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``.
39 changes: 39 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.test import TestCase

from django_hosts.test import AsyncHostsClient, HostsClient


class TestHostsClient(TestCase):
client_class = HostsClient

def test_host_header_set_from_url(self):
response = self.client.get('https://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']
Loading