Django channels are great for asynchronous views, but they can be a headache to test. This is due to the incompatibility with database transactions and all the side effects that the incompatibility produces. For example, the common pattern of rolling back the database after tests does not work, the data remains there which makes other tests fail as test outputs become flaky. However, using synchronous tests causes the request object to have different properties than in asynchronous tests. The request object is a WSGIRequest
object in synchronous views and an ASGIRequest
object in asynchronous views and the ASGIRequest
has specifically the scope
attribute which differs from the WSGIRequest
. However, due to the database incompatibilities, we'd still prefer to use synchronous tests if possible. This blog post will show you how to make the request object in synchronous views have the same properties as in asynchronous views by adding a middleware that injects the scope
.
The scope is a dictionary that contains information about the request. It is used in asynchronous views to pass information about the request to the view. It is commonly used in for example Django's GraphQL views.
This blog post tries to circumvent the following issues:
If they are fixed in the future, this blog post will be obsolete.
Creating a Scope Test Middleware
First, we'll create a middleware that injects the scope
into the request object. This middleware will be used in synchronous tests to make the request object have the same properties as in asynchronous tests. The scope will be based on the HttpRequest
object. You might want to customize the details based on your specific needs.
If we take a look at AsyncRequestFactory we can see how the default scope is created in Django. We'll use this as a reference. The middleware will create the dict as in the below example:
class EnsureScopeMiddleware:
"""
This middleware ensures that the request object has a 'scope' attribute.
Used in WSGI tests.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Check if the request object has a 'scope' attribute
if not hasattr(request, 'scope'):
request.scope = self.initialize_default_scope(request)
response = self.get_response(request)
return response
def initialize_default_scope(self, request):
# Here you create a default scope based on the HttpRequest
# You might want to customize the details based on your specific needs
return {
'type': 'http',
'path': request.path,
'headers': dict(request.headers.items()),
'method': request.method,
'query_string': request.META['QUERY_STRING'].encode('utf-8'),
'client': (request.META['REMOTE_ADDR'], request.META.get('REMOTE_PORT', 0)),
'server': (request.META['SERVER_NAME'], request.META['SERVER_PORT']),
'asgi': {'version': '3.0'},
}
We'll add this middleware to the MIDDLEWARE
setting in the test settings file e.g. test.py
file.
test.py
# MIDDLEWARE
# ------------------------------------------------------------------------------
MIDDLEWARE += ["myproject.middleware.EnsureScopeMiddleware"] # noqa F405
The middleware will now ensure that the request object has a scope
attribute. The difference between the server scope
and the test scope
can be seen in the below example.
Test middleware:
{'type': 'http', 'path': '/gql/', 'headers': {'Cookie': 'sessionid=b22it9p7pk3jcth2eaymk0rcf9798mzv', 'Content-Length': '487',
'Content-Type': 'application/json'}, 'method': 'POST', 'query_string': b'', 'client': ('127.0.0.1', 0), 'server':
('testserver', '80'), 'asgi': {'version': '3.0'}}
Django ASGI
server:
{'asgi': {'version': '3.0'}, 'type': 'http', 'http_version': '1.1', 'client': ['127.0.0.1', 0], 'server': ('127.0.0.1', '80'), 'scheme': 'http', 'method': 'POST', 'headers': [(b'host', b'testserver'), (b'content-length', b'460'), (b'content-type',
b'application/json'), (b'cookie', b'sessionid=5rlfj3qwh1qj4rwef05uekt29i6013po')], 'path': '/gql/', 'query_string': ''}
Any functionality that interacts with the ASGI scope can be tested now in synchronous WSGI tests.
Conclusion
We were using asynchronous views and testing them asynchronously as well, however the data inconsistency due to database issues were too many which was causing extremely flaky tests. Therefore, we decided to just use synchronous tests whenever possible and use the EnsureScopeMiddleware
to make the request object have the same properties as in asynchronous views. This way we can use synchronous tests and still have the same properties in the request object as in asynchronous views. This makes testing much easier and less flaky.