Невозможно аутентифицировать клиента в тесте
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 был аутентифицирован.