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'], ["<Question: Can I have a test?>"]) 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'], ["<Question: Can I have a test?>"]) 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'], ["<Question: Can I have a second test?>", "<Question: Can I have a first test?>", ]) 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ę.