Невозможно аутентифицировать клиента в тесте

Python 3.10.7, Django 4.1.1, Django REST Framework 3.13.1

Я не могу заставить методы django.test.Client login или force_login работать в django.test.TestCase производном тестовом классе. Я имею в виду https://docs.djangoproject.com/en/4.1/topics/testing/tools/

Мой проект, похоже, работает при просмотре в браузере. Представления DRF без аутентификации отображаются как ожидалось, и если я вхожу через администраторский сайт, защищенные представления также отображаются как ожидалось. Предварительная версия фронт-энда, который будет использовать этот API, может читать данные и отображать их без проблем. Локальная среда модульного тестирования Django использует локальную установку SQLite3 для данных. Все тесты, не требующие аутентификации, в настоящее время проходят.

Этот упрощенный тестовый класс надежно отображает проблему:

from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from eventsadmin.models import Address


class AddressesViewTest(TestCase):
    username = "jrandomuser"
    password = "qwerty123"
    user = User.objects.filter(username=username).first()
    if user:
        print("User exists")
    else:
        user = User.objects.create(username=username)
        print("User created")
    user.set_password(password)
    user.save()
    client = Client()


    def setUp(self):
        if self.client.login(username=self.username, password=self.password):
            print("Login successful")
        else:
            print("Login failed")
        Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188")


    def test_addresses(self):
        response = self.client.get(reverse("addresses-list"))
        self.assertContains(response, '"name":"White House"')

Во-первых, я был удивлен тем, что мне пришлось проверять существование User. Несмотря на то, что тестовый фреймворк выдает сообщения о том, что он создает и уничтожает тестовую базу данных для каждого запуска, после того, как тест был запущен один раз, создание User завершается неудачей с нарушением уникального ограничения на имя пользователя. Если я не меняю значение username, тест, как написано здесь, последовательно выдает User exists. Это единственный тест, создающий/получающий User, поэтому я уверен, что он не создается другим тестом.

Настоящей проблемой является setUp. Она постоянно выдает Login failed, а test_addresses не срабатывает при разрешении доступа (что является правильным поведением при попытке доступа к этому представлению без аутентификации). Если я установлю точку останова в последней строке setUp, то в этот момент self.client является экземпляром django.test.Client, а self.username и self.password имеют ожидаемые значения, заданные выше.

Я пробовал заменить вызов login на self.client.force_login(self.user), но в этом случае при достижении этой строки Django выдает django.db.utils.DatabaseError: Save with update_fields did not affect any rows. (трассировка стека происходит по адресу venv/lib/python3.10/site-packages/django/db/models/base.py", line 1001, in _save_table).

Что я делаю неправильно? Как я могу аутентифицироваться в этом контексте, чтобы я мог тестировать представления, требующие аутентификации?

Недавно я проводил кучу тестов, и вот как все мои создают пользователя внутри установки, как в блоке ниже, попробуйте это.

class AddressesViewTest(TestCase):
    def setUp(self):
        self.username = "jrandomuser"
        self.password = "qwerty123"
        user = User.objects.create(username=self.username)
        user.set_password(self.password)
        user.save()

        self.client.login(username=self.username, password=self.password)
        Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188"

    def test_addresses(self):
        response = self.client.get(reverse("addresses-list"))
        self.assertContains(response, '"name":"White House"')

На самом деле я даже не вхожу в систему во время setUp, потому что я хочу убедиться, что представление имеет @login_required, поэтому я делаю это в самом тесте:

class AddressesViewTest(TestCase):
    def setUp(self):
        self.username = "jrandomuser"
        self.password = "qwerty123"
        user = User.objects.create(username=self.username)
        user.set_password(self.password)
        user.save()
        Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188"

    def test_anonymous(self):
        response = testObj.client.get(reverse("addresses-list"))
        testObj.assertEqual(response.status_code, 302, 'address-list @login_required Missing')

    def test_addresses(self):
        self.client.login(username=self.username, password=self.password)
        response = self.client.get(reverse("addresses-list"))
        self.assertContains(response, '"name":"White House"')

Из того, что я заметил, setUp выполняется для каждого теста. Поэтому в моем последнем примере пользователь был бы создан для anonymous, удален или изменен, создан для test_addresses. Поэтому наличие пользователя вне этого блока, вероятно, приводит к тому, что пользователь не удаляется/обращается, что приводит к некоторому странному поведению.
И я знаю, что тесты утверждают, что он удаляет БД каждый раз, без флага --keepdb, но я начинаю сомневаться в этом... потому что я столкнулся с некоторым странным поведением, и это только после того, как я запускаю тест снова и снова... что-то не так наверняка

Друг с большим опытом направил меня на правильный путь, который, похоже, заключается в методе класса setUpTestData, который вызывается только один раз. Сам бы я до этого не додумался, потому что представлял себе classmethod похожим на static в .NET или Java, но, видимо, это не так; мне еще многому предстоит научиться.

Почти все создание тестовых данных, не относящихся к конкретному тесту, должно проходить в setUpTestData, также, по его словам.

Это работает:

from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from eventsadmin.models import Address


class AddressesViewTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = User.objects.create(username="jrandomuser")
        cls.client = Client()
        Address.objects.create(name="White House", address1="1600 Pennsylvania Ave", city="Washington", state="DC", postal_code="37188")

    def setUp(self):
        self.client.force_login(self.user)

    def test_addresses(self):
        response = self.client.get(reverse("eventsadmin:addresses-list"))
        self.assertContains(response, '"name":"White House"')

Как отмечает @nealium, также имеет смысл перенести вызов login или force_login в тест, если есть тесты, где вы не хотите, чтобы Client был аутентифицирован.

Вернуться на верх