Django频道2,在测试中访问数据库 [英] Django channels 2, accessing db in tests

查看:76
本文介绍了Django频道2,在测试中访问数据库的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我最近将项目更新为Django 2和Channel2。现在,我正在尝试重写聊天应用程序的测试。

I recently updated my project to Django 2 and channels 2. Right now I am trying to rewrite my tests for chat app.

我面临测试问题取决于pytest-django的django db标记。我试图在 WebsocketCommunicator 上使用async_to_sync在夹具,设置方法以及测试函数本身中创建对象。但是,这些都不起作用。

I am facing a problem with tests that depend on django db mark from pytest-django. I tried to create objects in fixtures, setup methods, in test function itself, using async_to_sync on WebsocketCommunicator. However, none of those worked.

如果我在灯具中创建用户并正确保存,则会获得一个ID。但是,在我的使用者中,Django在数据库中看不到该用户。并像对待匿名用户一样对待它。

If I create a user in a fixture and save it correctly gets an id. However, in my consumer Django does not see that User in the database. And treat it like an anonymous user.

我有一个临时令牌,用于在websocket.connect上对用户进行身份验证。

I have a temporary token which I use to authenticate a user on websocket.connect.

@pytest.fixture
def room():
    room = generate_room()
    room.save()
    return room


@pytest.fixture
def room_with_user(room, normal_user):
    room.users.add(normal_user)
    yield room
    room.users.remove(normal_user)

@pytest.fixture
def normal_user():
    user = generate_user()
    user.save()
    return user

@pytest.mark.django_db
class TestConnect:

    @pytest.mark.asyncio
    async def test_get_connected_client(self, path, room_with_user, temp_token):
        assert get_path(room_with_user.id) == path

        communicator = QSWebsocketCommunicator(application, path, query_string=get_query_string(temp_token))
        connected, subprotocol = await communicator.connect()
        assert connected
        await communicator.disconnect()

消费者:

class ChatConsumer(JsonWebsocketConsumer):
    def connect(self):
        # Called on connection. Either call

        self.user = self.scope['user']

        self.room_id = self.scope['url_route']['kwargs']['room_id']
        group = f'room_{self.room_id}'
        users = list(User.objects.all())  # no users here
        self.group_name = group

        if not (self.user is not None and self.user.is_authenticated):
            return self.close({'Error': 'Not authenticated user'})

        try:
            self.room = Room.objects.get(id=self.room_id, users__id=self.user.id)
        except ObjectDoesNotExist:
            return self.close({'Error': 'Room does not exists'})

        # Send success response
        self.accept()

        # Save user as active
        self.room.active_users.add(self.user)

我的身份验证中间件

class OAuthTokenAuthMiddleware:
    """
    Custom middleware that takes Authorization header and read OAuth token from it.
    """

    def __init__(self, inner):
        # Store the ASGI application we were passed
        self.inner = inner

    def __call__(self, scope):
        temp_token = self.get_token(scope)
        scope['user'] = self.validate_token(temp_token)
        return self.inner(scope)

    @staticmethod
    def get_token(scope) -> str:
        return url_parse.parse_qs(scope['query_string'])[b'token'][0].decode("utf-8")

    @staticmethod
    def validate_token(token):
        try:
            token = TemporaryToken.objects.select_related('user').get(token=token)
            if token.is_active():
                token.delete()
                return token.user
            else:
                return AnonymousUser()
        except ObjectDoesNotExist:
            return AnonymousUser()

和自定义WebsocketCommunicator ccepts query_string以便包含我的一次性令牌

And custom WebsocketCommunicator which accepts query_string in order to include my one time token

class QSWebsocketCommunicator(WebsocketCommunicator):
    def __init__(self, application, path, headers=None, subprotocols=None,
                 query_string: Optional[Union[str, bytes]]=None):
        if isinstance(query_string, str):
            query_string = str.encode(query_string)
        self.scope = {
            "type": "websocket",
            "path": path,
            "headers": headers or [],
            "subprotocols": subprotocols or [],
            "query_string": query_string or ''
        }
        ApplicationCommunicator.__init__(self, application, self.scope)

我的问题是如何在测试/夹具中创建用户,房间等对象,以便可以在Django使用者中访问它们。

My question is how can I create User, Room, etc. objects in tests/fixtures so that I can access them in Django consumer.

还是您有另一个主意我该如何克服呢?

Or do you have another idea how can I overcome this?

推荐答案

使用您提供的代码重现您的问题几乎是不可能的。阅读有关如何创建最小,完整和可验证的示例。但是,我想您应该在测试中使用真实的事务,因为普通的 pytest.mark.django_db 会跳过这些事务,并且本身不会在数据库中存储任何数据。一个有效的示例:

It's pretty much impossible to reproduce your issue using the code you've provided. Read about How to create a Minimal, Complete, and Verifiable example. However, I suppose that you should use real transactions in your test as the plain pytest.mark.django_db will skip the transactions and not store any data in the database per se. A working example:

# routing.py

from django import http
from django.conf.urls import url
from django.contrib.auth.models import User
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.generic.websocket import JsonWebsocketConsumer

class ChatConsumer(JsonWebsocketConsumer):
    def connect(self):
        self.user = self.scope['user']
        print('user in scope, set by middleware:', self.user)
        users = list(User.objects.all())  # no users here
        print('all users in chat consumer:', users)

        if not (self.user is not None and self.user.is_authenticated):
            return self.close({'Error': 'Not authenticated user'})

        # Send success response
        self.accept()


class OAuthTokenAuthMiddleware:
    def __init__(self, inner):
        # Store the ASGI application we were passed
        self.inner = inner

    def __call__(self, scope):
        token = self.get_token(scope)
        print('token in middleware:', token)
        scope['user'] = User.objects.get(username=token)
        return self.inner(scope)

    @staticmethod
    def get_token(scope) -> str:
        d = http.QueryDict(scope['query_string'])
        return d['token']


APP = ProtocolTypeRouter({
    'websocket': OAuthTokenAuthMiddleware(URLRouter([url(r'^websocket/$', ChatConsumer)])),
})

使用用户名垃圾邮件创建用户的示例夹具:

Sample fixture that creates a user with username spam:

@pytest.fixture(scope='function', autouse=True)
def create_user():
    with transaction.atomic():
        User.objects.all().delete()
        user = User.objects.create_user(
            'spam', 'spam@example.com', password='eggs',
            first_name='foo', last_name='bar'
        )
    return user

现在,我标记将测试作为事务性测试,意味着每个查询实际上都已提交。现在,测试用户存储在数据库中,并且在中间件/消费者中进行的查询实际上可以返回有意义的东西:

Now, I mark the test as transactional one, meaning that each query is actually committed. Now the test user is stored into database and the queries made in middleware/consumer can actually return something meaningful:

@pytest.mark.django_db(transaction=True)
@pytest.mark.asyncio
async def test_get_connected_client():
    app = OAuthTokenAuthMiddleware(URLRouter([url(r'^websocket/$', ChatConsumer)]))
    communicator = QSWebsocketCommunicator(app, '/websocket/', query_string='token=spam')
    connected, subprotocol = await communicator.connect()
    assert connected
    await communicator.disconnect()

运行测试会产生所需的结果:

Running test test yields the desired result:

$ pytest -vs
================================== test session starts =================================
platform darwin -- Python 3.6.3, pytest-3.4.0, py-1.5.2, pluggy-0.6.0 -- /Users/hoefling/.virtualenvs/stackoverflow/bin/python
cachedir: .pytest_cache
Django settings: spam.settings (from environment variable)
rootdir: /Users/hoefling/projects/private/stackoverflow/so-49136564/spam, inifile: pytest.ini
plugins: celery-4.1.0, forked-0.2, django-3.1.2, cov-2.5.1, asyncio-0.8.0, xdist-1.22.0, mock-1.6.3, hypothesis-3.44.4
collected 1 item

tests/test_middleware.py::test_get_connected_client Creating test database for alias 'default'...
token in middleware: spam
user in scope: spam
all users in chat consumer: [<User: spam>]
PASSEDDestroying test database for alias 'default'...


=============================== 1 passed in 0.38 seconds ================================

顺便说一句,您无需在周围乱砍WebsocketCommunicator 不再可用,因为它现在能够处理查询字符串,请参见此问题已关闭

Btw you don't need to hack around the WebsocketCommunicator anymore since it is now able to deal with query strings, see this issue closed.

这篇关于Django频道2,在测试中访问数据库的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆