14- ساخت اولین اپلیکیشن در جنگو(5) (صفحه ادمین):

۰۸ تیر ۱۴۰۴
0 دیدگاه

آموزش نوشتن اولین تست خودکار در Django (بخش ۵)

 

در این آموزش که ادامه‌ی بخش ۴ است، با نحوه‌ی نوشتن تست‌های خودکار در پروژه Django آشنا می‌شویم.
تا اینجا یک اپلیکیشن نظرسنجی ساخته‌ایم، حالا می‌خواهیم آن را با تست‌های خودکار قوی‌تر کنیم تا از صحت عملکرد برنامه مطمئن شویم.


 

تست خودکار چیست و چرا به آن نیاز داریم؟

 

تست خودکار به برنامه‌هایی گفته می‌شود که به صورت خودکار رفتار کد شما را بررسی می‌کنند تا مطمئن شوند همه چیز درست کار می‌کند.
این تست‌ها می‌توانند از بررسی جزئی‌ترین بخش‌های کد (مثل متدهای مدل‌ها) تا عملکرد کلی سایت (مثلاً رفتار کاربر هنگام استفاده از فرم‌ها) را پوشش دهند.

 

اهمیت نوشتن تست‌های خودکار

 

  • صرفه‌جویی در زمان:
    بدون تست خودکار باید هر بار تغییرات کد را با آزمایش دستی و وقت‌گیر بررسی کنید. تست‌های خودکار در چند ثانیه انجام می‌شوند و به سرعت مشکلات را نشان می‌دهند.

  •  
  • پیشگیری از بروز خطا:
    تست‌ها فقط خطاها را نشان نمی‌دهند، بلکه از بازگشت دوباره‌ی آن‌ها جلوگیری می‌کنند و به درک بهتر کد کمک می‌کنند.

  •  
  • کد قابل اعتماد و جذاب برای توسعه‌دهندگان دیگر:
    کدی که تست دارد، اعتماد بیشتری ایجاد می‌کند و توسعه‌دهندگان دیگر راحت‌تر آن را می‌پذیرند.

  •  
  • کمک به تیم‌های برنامه‌نویسی:
    در پروژه‌های تیمی، تست‌ها تضمین می‌کنند که تغییرات هیچ عضو تیم باعث خرابی بخش‌های دیگر نشود.


 

 

استراتژی‌های پایه برای نوشتن تست‌ها

 

  • توسعه مبتنی بر تست (TDD):
    ابتدا تست‌ها نوشته می‌شوند و سپس کد برای پاس کردن آن تست‌ها توسعه می‌یابد. این روش کمک می‌کند طراحی کد بهینه‌تر باشد.

  •  
  • نوشتن تست بعد از توسعه کد:
    معمولاً برنامه‌نویسان ابتدا کد را می‌نویسند و بعد تست می‌سازند. این هم روش مناسبی است و هیچ وقت برای شروع نوشتن تست دیر نیست.


 

 

نوشتن اولین تست خودکار در پروژه نظرسنجی

 

فرض کنیم در متد was_published_recently() در مدل Question مشکلی وجود دارد: این متد باید تنها در صورتی True برگرداند که تاریخ انتشار سوال در گذشته‌ی نزدیک باشد، اما اکنون اگر تاریخ انتشار در آینده باشد هم True برمی‌گرداند که نادرست است.

 

تایید وجود مشکل

 

با استفاده از shell جنگو:

				
					$ python manage.py shell
>>> import datetime
>>> from django.utils import timezone
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> future_question.was_published_recently()
True

				
			

در حالی که سوالی که در آینده منتشر می‌شود نباید اخیراً منتشر شده باشد.

 

 

ایجاد تست برای شناسایی مشکل

 

در فایل polls/tests.py کد زیر را اضافه می‌کنیم:

				
					import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question

class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

				
			

 

اجرای تست

				
					$ python manage.py test polls

				
			

تست شکست می‌خورد چون متد هنوز مشکل دارد و مقدار True برمی‌گرداند.


 

 

اصلاح کد برای رفع خطا

متد was_published_recently را این‌گونه اصلاح می‌کنیم تا تاریخ انتشار سوال فقط در گذشته و طی 24 ساعت اخیر صحیح تشخیص داده شود:

				
					def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now

				
			

تست را دوباره اجرا می‌کنیم که این بار باید موفق باشد.


 

 

نوشتن تست‌های بیشتر برای پوشش کامل‌تر

برای اطمینان از اینکه متد درست برای همه شرایط کار می‌کند، دو تست دیگر اضافه می‌کنیم:

				
					def test_was_published_recently_with_old_question(self):
    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):
    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)

				
			

تست عملکرد ویوها با کلاینت تست جنگو

برای مثال، صفحه‌ی اصلی (index) نظرسنجی‌ها سوالاتی که تاریخ انتشارشان در آینده است را نباید نشان دهد.

 

معرفی کلاینت تست

جنگو ابزاری به نام Client دارد که درخواست‌های HTTP را شبیه‌سازی می‌کند و می‌توان با آن تست‌های سطح نمایشی (View) نوشت.

 

اصلاح ویو IndexView

متد get_queryset را به این صورت اصلاح می‌کنیم:

				
					from django.utils import timezone

def get_queryset(self):
    return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[:5]

				
			

 

نوشتن تست‌های ویو

تابع کمکی برای ایجاد سوالات با offset روزها نسبت به زمان فعلی:

				
					def create_question(question_text, days):
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)

				
			

 

کلاس تست ویو:

				
					from django.urls import reverse
from django.test import TestCase

class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        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 = create_question("Past question.", -30)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(response.context["latest_question_list"], [question])

    def test_future_question(self):
        create_question("Future question.", 30)
        response = self.client.get(reverse("polls:index"))
        self.assertContains(response, "No polls are available.")
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

    def test_future_question_and_past_question(self):
        question = create_question("Past question.", -30)
        create_question("Future question.", 30)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(response.context["latest_question_list"], [question])

    def test_two_past_questions(self):
        question1 = create_question("Past question 1.", -30)
        question2 = create_question("Past question 2.", -5)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(response.context["latest_question_list"], [question2, question1])

				
			

 

 

آموزش تست در Django: بررسی DetailView

در این بخش به بررسی نحوه‌ی نوشتن تست برای کلاس DetailView می‌پردازیم تا اطمینان حاصل کنیم که سوالاتی که هنوز منتشر نشده‌اند (یعنی تاریخ انتشارشان در آینده است)، نمایش داده نمی‌شوند.

 

افزودن محدودیت نمایش در DetailView

در فایل polls/views.py باید متد get_queryset را در DetailView بازنویسی کنیم تا سوالاتی که تاریخ انتشارشان در آینده است از کوئری حذف شوند:

				
					class DetailView(generic.DetailView):
    ...

    def get_queryset(self):
        """
        سوالاتی که تاریخ انتشارشان از زمان فعلی کمتر است را برمی‌گرداند.
        """
        return Question.objects.filter(pub_date__lte=timezone.now())

				
			

 

نوشتن تست برای DetailView

برای اطمینان از عملکرد درست محدودیت فوق، دو تست زیر را در polls/tests.py اضافه می‌کنیم:

				
					class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        بررسی اینکه صفحه سوالی که تاریخ انتشارش در آینده است، 
        پاسخ 404 بازمی‌گرداند.
        """
        future_question = create_question(question_text="Future question.", 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(self):
        """
        بررسی اینکه صفحه سوالی که تاریخ انتشارش در گذشته است، 
        متن سوال را نمایش می‌دهد.
        """
        past_question = create_question(question_text="Past Question.", days=-5)
        url = reverse("polls:detail", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

				
			

 

ایده‌هایی برای تست‌های بیشتر

  • متد مشابه get_queryset را در کلاس ResultsView نیز اضافه کنید و تست‌های مشابهی برای آن بنویسید.
  • از نمایش سوالاتی که هیچ گزینه (Choice) مرتبط ندارند جلوگیری کنید.
  • کاربران مدیر (admin) می‌توانند سوالات منتشر نشده را ببینند ولی کاربران عادی نه.
  • هر تغییر در منطق نمایش باید همراه با نوشتن تست‌های مرتبط باشد تا از بروز خطا جلوگیری شود.

 

اهمیت افزایش تست‌ها و مدیریت آن‌ها

  • ممکن است تعداد تست‌ها زیاد شود و حجم کد تست از کد اصلی بیشتر شود اما این موضوع اشکالی ندارد.
  • تست‌ها به مرور زمان نگهداری و اصلاح می‌شوند و حتی اگر تستی قدیمی و زائد شود، مشکلی ایجاد نمی‌کند.
  • توصیه می‌شود برای هر مدل یا ویو یک کلاس تست جداگانه و برای هر شرط یک متد تست مستقل داشته باشید.
  • نام متدهای تست باید بیانگر عملکردشان باشد تا خوانایی و نگهداری کد تسهیل شود.

 

تست‌های پیشرفته‌تر و ابزارهای مرتبط

  • استفاده از ابزارهای تست «درون مرورگر» مثل Selenium برای بررسی نمایش HTML و رفتار JavaScript.
  • Django کلاس LiveServerTestCase را برای تسهیل استفاده از Selenium فراهم می‌کند.
  • اجرای تست‌ها به صورت خودکار در فرآیند توسعه با Continuous Integration کیفیت پروژه را تضمین می‌کند.
  • بررسی پوشش کد (code coverage) به شناسایی بخش‌های تست نشده و کدهای مرده کمک می‌کند.
  • اطلاعات جامع‌تر درباره تست در مستندات رسمی Django در بخش Testing in Django موجود است.

 

 

نتیجه‌گیری

 

تست‌نویسی خودکار نه تنها کیفیت کد شما را بالا می‌برد بلکه فرآیند توسعه را مطمئن‌تر و سریع‌تر می‌کند. با نوشتن تست‌های مناسب می‌توانید از عملکرد صحیح برنامه خود اطمینان حاصل کرده و مشکلات احتمالی را در مراحل اولیه توسعه شناسایی کنید.

0 دیدگاه