====== 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ę.