====== Testy w Django ======
Poniżej plik tests.py przygotowany dla aplikacji polls z tutoriala do Django. Działa poprawnie w Django 3.1 oraz 4.0. Jedyna różnica w porównaniu z oryginałem to dodana funkcja fix_votes w modelu Choice:
def fix_votes(self):
elif self.votes == 0:
self.votes = 0
return True
if self.votes < 0:
self.votes = 0
return True
elif self.votes % (ceil(self.votes)-1) != 1:
self.votes = ceil(self.votes)-1
return True
[[https://docs.djangoproject.com/en/4.0/intro/tutorial01/]]
import datetime
from django.test import TestCase
from django.utils import timezone
from django.urls import reverse
# Create your tests here.
from .models import Question, Choice
# ---------------------------------------------
# --------- HELPERS ---------------------------
# ---------------------------------------------
def create_question(question_text,days):
'''
Create question with with given "question_text"
and puslished with "days" from now:
+ positive for questions puslished in the past,
- negative for questions which will be published in the future.
'''
time = timezone.now() + datetime.timedelta(days=days)
return Question.objects.create(question_text=question_text, pub_date=time)
def create_choice(question, choice_text, votes):
'''
Create choice for question with id "question"
with given "answer_text" and "votes" number
of initial votes.
'''
return Choice.objects.create(
question=question,
choice_text=choice_text,
votes=votes,
)
# ---------------------------------------------
# --------- MODEL TESTS -----------------------
# ---------------------------------------------
class QuestionModelTest(TestCase):
def test_question_text_attribute(self):
'''
Test question_text attribute
'''
question = Question(question_text = 'Que?')
self.assertIs(question.question_text, 'Que?')
def test_was_published_recently_with_future_question(self):
'''
was_published_recently returns False for
question published in the future
'''
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date = time)
self.assertIs(future_question.was_published_recently(), False)
def test_was_published_recently_with_old_question(self):
'''
was_published_recently() returns False for questions whose pub_date
is older than 1 day.
'''
time = timezone.now() - datetime.timedelta(days=1, seconds=1)
old_question = Question(pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
'''
was_published_recently() returns True for questions whose pub_date
is within the last day.
'''
time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
recent_question = Question(pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
def test_too_long_question_over_200_characters(self, toolong='safe'):
'''
question_text of lenght over 200 characters should be allowed by Django.
'''
try:
question = Question(question_text='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
except TypeError:
pass
self.assertIs(toolong, 'safe')
class ChoiceModelTest(TestCase):
def test_choice_text_attribute(self):
'''
Test question_text attribute
'''
choice = Choice(choice_text = 'that')
self.assertIs(choice.choice_text, 'that')
def test_votes_attribute(self):
'''
Checks Choice vote attribute.
'''
votes_above_zero = Choice(votes=5)
self.assertIs(votes_above_zero.votes, 5)
def test_fix_votes_with_negative_votes_value(self):
'''
fix_votes() should return 0 votes for choice
instance with -3 votes (which is below zero)
'''
votes_below_zero = Choice(votes=-3)
votes_below_zero.fix_votes()
self.assertIs(votes_below_zero.votes, 0)
def test_fix_votes_with_good_votes_value(self):
'''
fix_votes() should return 3 votes for choice
instance with 3 votes
'''
votes_below_zero = Choice(votes=3)
votes_below_zero.fix_votes()
self.assertIs(votes_below_zero.votes, 3)
def test_fix_votes_with_zero_votes_value(self):
'''
fix_votes() should return 0 votes for choice
instance with 0 votes
'''
votes_below_zero = Choice(votes=0)
votes_below_zero.fix_votes()
self.assertIs(votes_below_zero.votes, 0)
def test_fix_votes_with_float_votes(self):
'''
fix_votes() should return 4 votes for choice
instance with 4.7 votes
'''
votes_float = Choice(votes=4.7)
votes_float.fix_votes()
self.assertIs(votes_float.votes, 4)
def test_too_long_choice_over_200_characters(self, toolong='safe'):
'''
choice_text of over 200 characters long should not be allowed by Django.
'''
try:
choice = Choice(choice_text='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
except TypeError:
pass
self.assertIs(toolong, 'safe')
# ---------------------------------------------
# --------- VIEW TESTS ------------------------
# ---------------------------------------------
class QuestionIndexTestView(TestCase):
def test_no_question(self):
'''
If no questions exists, view should return appropriate message.
'''
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_past_question(self):
'''
Question with the past publication date have to be displayed on the index page.
'''
create_question("Can I have a test?", days=-30)
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Can I have a test?")
self.assertQuerysetEqual(response.context['latest_question_list'],
[""])
def test_future_question(self):
'''
Question with the future publication date should not be listed at index page.
'''
create_question("Back from the future?", days=1)
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "No polls are available")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_past_and_future_question(self):
'''
Only past question should be displayed on the index page.
'''
create_question("Can I have a test?", days=-30)
create_question("Back from the future?", days=1)
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Can I have a test?")
self.assertQuerysetEqual(response.context['latest_question_list'],
[""])
def test_two_past_questions(self):
'''
Both past questions should be
displayed on the index page.
'''
create_question("Can I have a first test?", days=-30)
create_question("Can I have a second test?", days=-20)
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Can I have a first test?")
self.assertQuerysetEqual(response.context['latest_question_list'],
["",
"",
])
class QuestionDetailViewTest(TestCase):
def test_future_question_status_code(self):
'''
Detail view of question from future should return 404.
'''
future_question = create_question(question_text='Future?', days=5)
url = reverse('polls:detail', args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_past_question_status_code(self):
'''
Detail view of question from the past should return 200.
'''
past_question = create_question(question_text='Past?', days=-5)
url = reverse('polls:detail', args=(past_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_past_question_response(self):
'''
Detail view of question from the past should return question_text.
'''
past_question = create_question(question_text='Past?', days=-5)
url = reverse('polls:detail', args=(past_question.id,))
response = self.client.get(url)
self.assertContains(response, past_question.question_text)
----
===== Zapisywanie do bazy danych w testach Django =====
Dodatkowa baza danych w pliku settings.py. Ważne jest, by baza wykorzystywana do testów miała zdefiniowaną samę siebie jako bazę testową.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'produkcja',
'USER': 'produkcja',
'PASSWORD': 'qwerty123',
'HOST': 'localhost', # Or an IP Address that your DB is hosted on
'PORT': '3306',
},
'TEST': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'testowa',
'USER': 'testowa',
'PASSWORD': 'qwerty234',
'HOST': 'localhost', # Or an IP Address that your DB is hosted on
'PORT': '3306',
'TEST': {'NAME': 'testowa'}
}
}
W pliku tests.py należy utworzyć klasę dziecziczącą po TestCase i zdefiniować używaną bazę danych. Nazwa klasy może być dowolna:
class TestCaseD(TestCase):
databases = {'default' ,'TEST'}
Wszystkie testy muszą być egzamplarzami klasy TestCaseD.
Wszystkie "ręczne" zapisy do bazy danych w pliku (lub plikach) tests.py należy opatrzyć parametrem //using//:
jakis_obiekt.save(using='TEST')
Należy utworzyć bazę danych //testowa//, użytkownika //testowa// oraz nadać mu pełne uprawnienia do wszystkich tabel.
Teraz można uruchomić testy. Agrument //--keepdb// jest opcjonalny i pozwala na przejrzenie zawartości bazy danych //testowa// po zakończeniu testów automatycznych. Oczekiwany efekt:
python manage.py migrate --database TEST
[Applying contenttypes.0001_initial... OK] [...]
python manage.py test --debug-mode --keepdb
Found 18 test(s).
Using existing test database for alias 'default'...
Using existing test database for alias 'TEST'...
System check identified no issues (0 silenced).
..........
........
----------------------------------------------------------------------
Ran 18 tests in 6.086s
OK
Preserving test database for alias 'default'...
Preserving test database for alias 'TEST'...
Po każdym wykonaniu testów bez parametru //--keepdb// należy ponownie utworzyć testową bazę danych oraz wykonać migrację.